[
  {
    "path": ".dockerignore",
    "content": ".git\n.github\n.next\nnode_modules\n.env\n.env.*\n*.log\nREADME.md\n.DS_Store\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.idea\n.vscode\n.eslintcache\ncoverage\ndist\n.turbo\npublic/swagger.json\npublic/widget-bundle.js\npublic/widget-bundle.js.map\nuseful-information-for-development\nAPIDOCGUIDE.md\nCHANGELOG.md\ndocker-compose-online.yml\ndocker-compose.yml"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: supernova3339 # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: superdevofficial # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.yml",
    "content": "name: 🐛 Bug Report\ndescription: Found a bug? Help us squash it! Every bug report makes Changerawr better.\ntitle: \"[Bug]: \"\nlabels: [\"bug\", \"triage\"]\nassignees:\n  - Supernova3339\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        # 🐛 Thanks for reporting a bug!\n        \n        Bug reports are super valuable - they help us make Changerawr rock solid for everyone. The more details you provide, the faster we can track down and fix the issue.\n        \n        **Please search existing issues first** to see if this bug has already been reported. If you find it, give it a 👍!\n\n  - type: input\n    id: contact\n    attributes:\n      label: 📧 Contact Details (Optional)\n      description: How can we reach you if we need more info about this bug?\n      placeholder: email@example.com or @username\n    validations:\n      required: false\n\n  - type: textarea\n    id: bug-description\n    attributes:\n      label: 🐛 What happened?\n      description: Describe the bug clearly and concisely\n      placeholder: Tell us what went wrong...\n    validations:\n      required: true\n\n  - type: textarea\n    id: expected-behavior\n    attributes:\n      label: ✅ What should have happened?\n      description: What did you expect to happen instead?\n      placeholder: Describe what you thought would happen...\n    validations:\n      required: true\n\n  - type: textarea\n    id: reproduction-steps\n    attributes:\n      label: 🔄 Steps to Reproduce\n      description: How can we reproduce this bug? Be as specific as possible!\n      placeholder: |\n        1. Go to '...'\n        2. Click on '...'\n        3. Scroll down to '...'\n        4. See error\n      value: |\n        1. \n        2. \n        3. \n        4.\n    validations:\n      required: true\n\n  - type: dropdown\n    id: frequency\n    attributes:\n      label: 📊 How often does this happen?\n      description: Is this bug consistent or intermittent?\n      options:\n        - \"Every time (100%)\"\n        - \"Most of the time (75%+)\"\n        - \"Sometimes (25-75%)\"\n        - \"Rarely (less than 25%)\"\n        - \"Only happened once\"\n      default: 0\n    validations:\n      required: true\n\n  - type: dropdown\n    id: browser\n    attributes:\n      label: 🌐 Browser\n      description: Which browser are you using?\n      multiple: true\n      options:\n        - Chrome\n        - Firefox\n        - Safari\n        - Microsoft Edge\n        - Opera\n        - Other\n\n  - type: dropdown\n    id: device-type\n    attributes:\n      label: 📱 Device Type\n      description: What device were you using?\n      options:\n        - Desktop (Windows)\n        - Desktop (macOS)\n        - Desktop (Linux)\n        - Mobile (iOS)\n        - Mobile (Android)\n        - Tablet (iOS)\n        - Tablet (Android)\n      default: 0\n    validations:\n      required: true\n\n  - type: input\n    id: screen-resolution\n    attributes:\n      label: 🖥️ Screen Resolution (Optional)\n      description: What's your screen resolution? Helpful for UI bugs\n      placeholder: \"e.g., 1920x1080, 1366x768\"\n    validations:\n      required: false\n\n  - type: dropdown\n    id: changerawr-area\n    attributes:\n      label: 🎯 Which part of Changerawr?\n      description: Where in the app did this bug occur?\n      options:\n        - Dashboard / Home\n        - Changelog Editor\n        - Project Settings\n        - User Settings / Profile\n        - Authentication / Login\n        - Embeddable Widget\n        - API / Integrations\n        - Email Notifications\n        - GitHub Integration\n        - Mobile Interface\n        - Deployment/Pre-Deployment\n        - Other / Not Sure\n      default: 10\n    validations:\n      required: true\n\n  - type: dropdown\n    id: severity\n    attributes:\n      label: ⚡ Bug Severity\n      description: How much does this impact your use of Changerawr?\n      options:\n        - \"Low - Minor annoyance, doesn't block my work\"\n        - \"Medium - Inconvenient but I can work around it\"\n        - \"High - Significantly impacts my workflow\"\n        - \"Critical - Completely blocks me from using Changerawr\"\n      default: 0\n    validations:\n      required: true\n\n  - type: textarea\n    id: error-messages\n    attributes:\n      label: ❌ Error Messages\n      description: Any error messages, console errors, or warnings you saw?\n      placeholder: Copy and paste any error messages here...\n      render: text\n    validations:\n      required: false\n\n  - type: textarea\n    id: console-logs\n    attributes:\n      label: 🔍 Console Output (For Developers)\n      description: If you're comfortable with dev tools, paste any relevant console output\n      placeholder: Right-click → Inspect → Console tab → copy any red errors\n      render: shell\n    validations:\n      required: false\n\n  - type: textarea\n    id: additional-context\n    attributes:\n      label: 📝 Additional Context\n      description: Anything else that might be relevant? Screenshots, network conditions, etc.\n      placeholder: Any other details that might help us understand this bug...\n    validations:\n      required: false\n\n  - type: input\n    id: changerawr-version\n    attributes:\n      label: 🦖 Changerawr Version (Optional)\n      description: If you know the version, it helps! Check the about page in administration settings, or your lib/app-info.ts\n      placeholder: \"e.g., v0.3.9\"\n    validations:\n      required: false\n\n  - type: checkboxes\n    id: bug-checklist\n    attributes:\n      label: 📋 Bug Report Checklist\n      description: Quick checks to help us process your report\n      options:\n        - label: I've searched existing issues and this isn't a duplicate\n          required: true\n        - label: I can reproduce this bug consistently\n          required: false\n        - label: I've included steps to reproduce the issue\n          required: true\n        - label: I'd be willing to test a fix if provided\n          required: false\n        - label: I am able to provide a reproduction video in the event that I can not provide reproduction steps ( youtube, loom, etc )\n          required: true\n        - label: I have all required ENV variables configured.\n          required: true\n        - label: I am using docker ( only check this if you are deploying with docker! )\n          required: false\n        - label: I am not using Cloudflare Zero Trust ( it breaks everything! )\n          required: true\n\n  - type: markdown\n    attributes:\n      value: |\n        ---\n        \n        ## 🛠️ What happens next?\n        \n        1. **We'll investigate** - Usually within 24-72 hours\n        2. **Reproduce the bug** - We'll try to recreate the issue\n        3. **Ask questions** - We might need more details\n        4. **Fix it** - Once confirmed, we'll work on a solution\n        5. **Test & Release** - Fix gets tested and deployed\n        \n        **Pro tip**: The best bug reports include clear steps to reproduce the issue. If we can reproduce it, we can fix it! \n        \n        Thanks for helping make Changerawr more stable! 🦖💪\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.yml",
    "content": "name: 🦖 Feature Request\ndescription: Got an idea to make Changerawr even better? We'd love to hear it!\ntitle: \"[Feature]: \"\nlabels: [\"enhancement\", \"feature-request\"]\nassignees:\n  - Supernova3339\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        # 🎉 Thanks for helping make Changerawr better!\n        \n        We built Changerawr because existing tools didn't cut it. Your ideas help us build exactly what developers and teams actually need.\n        \n        **Please search existing issues first** to avoid duplicates. If you find a similar request, give it a 👍 instead!\n\n  - type: input\n    id: contact\n    attributes:\n      label: 📧 Contact Details (Optional)\n      description: How can we reach you if we need to discuss this feature?\n      placeholder: email@example.com or @username\n    validations:\n      required: false\n\n  - type: textarea\n    id: feature-description\n    attributes:\n      label: 🚀 Feature Description\n      description: What feature would you like to see added to Changerawr?\n      placeholder: Be as detailed as you can! What should this feature do?\n    validations:\n      required: true\n\n  - type: textarea\n    id: problem-solving\n    attributes:\n      label: 🎯 Problem This Solves\n      description: What problem or pain point would this feature address?\n      placeholder: Describe the current limitation or frustration this would solve\n    validations:\n      required: true\n\n  - type: textarea\n    id: use-case\n    attributes:\n      label: 📝 Use Case / User Story\n      description: How would you (or others) use this feature?\n      placeholder: \"As a [type of user], I want [feature] so that [benefit]\"\n    validations:\n      required: true\n\n  - type: dropdown\n    id: user-type\n    attributes:\n      label: 👤 What best describes you?\n      description: This helps us understand our user base and prioritize features\n      options:\n        - Solo Developer\n        - Small Team (2-10 people)\n        - Medium Team (11-50 people)\n        - Large Team (50+ people)\n        - Open Source Project Maintainer\n        - Product Manager\n        - Other\n      default: 0\n    validations:\n      required: true\n\n  - type: dropdown\n    id: priority\n    attributes:\n      label: ⚡ How important is this to you?\n      description: Help us understand the impact this would have\n      options:\n        - \"Nice to have - would be cool but not urgent\"\n        - \"Useful - would improve my workflow\"\n        - \"Important - would solve a real problem I face\"\n        - \"Critical - blocking me from using Changerawr effectively\"\n      default: 0\n    validations:\n      required: true\n\n  - type: textarea\n    id: alternatives\n    attributes:\n      label: 🔄 Current Workarounds\n      description: How are you currently handling this without the feature?\n      placeholder: Describe any workarounds you're using or tools you're missing\n    validations:\n      required: false\n\n  - type: textarea\n    id: implementation-ideas\n    attributes:\n      label: 💡 Implementation Ideas (Optional)\n      description: Any thoughts on how this could work? UI mockups, API endpoints, etc.\n      placeholder: Don't worry about technical details - but if you have ideas, we'd love to hear them!\n    validations:\n      required: false\n\n  - type: dropdown\n    id: feature-area\n    attributes:\n      label: 🎨 What area of Changerawr is this for?\n      description: Which part of the platform would this feature affect?\n      multiple: true\n      options:\n        - Dashboard / UI\n        - Content Editor\n        - API / Integrations\n        - Embeddable Widget\n        - Email Notifications\n        - Authentication / Users\n        - GitHub Integration\n        - Analytics / Reporting\n        - Mobile Experience\n        - Performance\n        - Other / Not Sure\n\n  - type: checkboxes\n    id: additional-info\n    attributes:\n      label: 📋 Additional Information\n      description: A few quick checks to help us out\n      options:\n        - label: I've searched existing issues and this isn't a duplicate\n          required: true\n        - label: I'd be willing to test this feature if implemented\n          required: false\n        - label: I'd be interested in contributing to this feature's development\n          required: false\n\n  - type: markdown\n    attributes:\n      value: |\n        ---\n        \n        ## 🙏 What happens next?\n        \n        1. **We'll review your request** - Usually within a few days\n        2. **Community feedback** - Others can vote with 👍 and add comments\n        3. **Discussion** - We might ask follow-up questions\n        4. **Prioritization** - Popular requests with clear use cases get priority\n        5. **Development** - If accepted, we'll add it to our roadmap\n        \n        **Remember**: Changerawr is built by developers for developers. The best feature requests come from real problems you're facing!\n        \n        Thanks for helping make Changerawr rawrsome! 🦖"
  },
  {
    "path": ".github/workflows/build-platform.yml",
    "content": "name: Build Platform Image\n\non:\n  workflow_call:\n    inputs:\n      platform:\n        required: true\n        type: string\n        description: 'Platform to build (linux/amd64 or linux/arm64)'\n      image_name:\n        required: true\n        type: string\n        description: 'Image name'\n      version:\n        required: true\n        type: string\n        description: 'Version label for the Docker image tag'\n      no_cache:\n        required: false\n        type: boolean\n        default: false\n        description: 'Disable Docker layer cache'\n\nenv:\n  REGISTRY: ghcr.io\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    timeout-minutes: 45\n    permissions:\n      contents: read\n      packages: write\n      actions: write\n    steps:\n      - name: Checkout (HEAD of current branch)\n        uses: actions/checkout@v4\n\n      - name: Set up QEMU (for ARM builds)\n        if: inputs.platform == 'linux/arm64'\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Set platform suffix\n        id: platform\n        run: |\n          PLATFORM_SUFFIX=$(echo \"${{ inputs.platform }}\" | sed 's|linux/||')\n          echo \"suffix=$PLATFORM_SUFFIX\" >> $GITHUB_OUTPUT\n\n      - name: Delete GHA Docker cache (force rebuild)\n        if: inputs.no_cache\n        env:\n          GH_TOKEN: ${{ github.token }}\n        run: |\n          echo \"🗑️ Deleting cached Docker layers for scope: ${{ steps.platform.outputs.suffix }}\"\n          gh cache list --repo ${{ github.repository }} --json id,key \\\n            | jq -r '.[] | select(.key | contains(\"${{ steps.platform.outputs.suffix }}\")) | .id' \\\n            | xargs -I{} gh cache delete {} --repo ${{ github.repository }} \\\n            || echo \"No cache entries found or deletion skipped\"\n\n      - name: Build and push\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          platforms: ${{ inputs.platform }}\n          push: true\n          no-cache: ${{ inputs.no_cache }}\n          build-args: |\n            CACHEBUST=${{ inputs.no_cache && github.run_id || '1' }}\n          tags: ${{ env.REGISTRY }}/${{ inputs.image_name }}:${{ inputs.version }}-${{ steps.platform.outputs.suffix }}\n          cache-from: ${{ inputs.no_cache == false && format('type=gha,scope={0}', steps.platform.outputs.suffix) || '' }}\n          cache-to: ${{ inputs.no_cache == false && format('type=gha,mode=max,scope={0}', steps.platform.outputs.suffix) || '' }}\n          labels: |\n            org.opencontainers.image.source=https://github.com/${{ github.repository }}\n            org.opencontainers.image.version=${{ inputs.version }}\n\n      - name: Build complete\n        run: |\n          echo \"✅ Built ${{ inputs.platform }} image: ${{ env.REGISTRY }}/${{ inputs.image_name }}:${{ inputs.version }}-${{ steps.platform.outputs.suffix }}\"\n"
  },
  {
    "path": ".github/workflows/docker-build.yml",
    "content": "name: Build and Push Docker Image\n\non:\n  push:\n    tags:\n      - 'v*'\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'Version to build (e.g., v1.0.4)'\n        required: true\n        type: string\n      force_rebuild:\n        description: 'Force rebuild (ignore cache)'\n        required: false\n        type: boolean\n        default: false\n\nenv:\n  REGISTRY: ghcr.io\n\njobs:\n  info:\n    runs-on: ubuntu-latest\n    outputs:\n      image_name: ${{ steps.meta.outputs.image_name }}\n      version: ${{ steps.meta.outputs.version }}\n    steps:\n      - name: Show build info\n        id: meta\n        run: |\n          IMAGE_NAME=$(echo \"${{ github.repository }}\" | tr '[:upper:]' '[:lower:]')\n\n          if [ -n \"${{ github.event.inputs.version }}\" ]; then\n            VERSION=\"${{ github.event.inputs.version }}\"\n            TRIGGER=\"Manual (workflow_dispatch)\"\n          else\n            VERSION=\"${{ github.ref_name }}\"\n            TRIGGER=\"Tag push\"\n          fi\n\n          echo \"🚀 Docker Build Starting\"\n          echo \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n          echo \"📦 Repository: ${{ github.repository }}\"\n          echo \"🏷️  Version: $VERSION\"\n          echo \"🎯 Trigger: $TRIGGER\"\n          echo \"🐳 Registry: ${{ env.REGISTRY }}\"\n          echo \"\"\n          echo \"📋 Will be tagged as:\"\n          echo \"   • ${{ env.REGISTRY }}/$IMAGE_NAME:$VERSION\"\n          echo \"   • ${{ env.REGISTRY }}/$IMAGE_NAME:latest\"\n          echo \"\"\n          echo \"🏗️  Platforms: linux/amd64, linux/arm64\"\n          echo \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n\n          echo \"image_name=$IMAGE_NAME\" >> $GITHUB_OUTPUT\n          echo \"version=$VERSION\" >> $GITHUB_OUTPUT\n\n  build-amd64:\n    needs: info\n    uses: ./.github/workflows/build-platform.yml\n    with:\n      platform: 'linux/amd64'\n      image_name: ${{ needs.info.outputs.image_name }}\n      version: ${{ needs.info.outputs.version }}\n      no_cache: ${{ inputs.force_rebuild || false }}\n\n  build-arm64:\n    needs: info\n    uses: ./.github/workflows/build-platform.yml\n    with:\n      platform: 'linux/arm64'\n      image_name: ${{ needs.info.outputs.image_name }}\n      version: ${{ needs.info.outputs.version }}\n      no_cache: ${{ inputs.force_rebuild || false }}\n\n  create-manifest:\n    needs: [info, build-amd64, build-arm64]\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n    permissions:\n      contents: read\n      packages: write\n    steps:\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Create and push multi-arch manifest\n        run: |\n          docker buildx imagetools create \\\n            -t ${{ env.REGISTRY }}/${{ needs.info.outputs.image_name }}:${{ needs.info.outputs.version }} \\\n            ${{ env.REGISTRY }}/${{ needs.info.outputs.image_name }}:${{ needs.info.outputs.version }}-amd64 \\\n            ${{ env.REGISTRY }}/${{ needs.info.outputs.image_name }}:${{ needs.info.outputs.version }}-arm64\n\n          docker buildx imagetools create \\\n            -t ${{ env.REGISTRY }}/${{ needs.info.outputs.image_name }}:latest \\\n            ${{ env.REGISTRY }}/${{ needs.info.outputs.image_name }}:${{ needs.info.outputs.version }}-amd64 \\\n            ${{ env.REGISTRY }}/${{ needs.info.outputs.image_name }}:${{ needs.info.outputs.version }}-arm64\n\n      - name: Summary\n        run: |\n          echo \"## 🚀 Docker Image Built Successfully!\" >> $GITHUB_STEP_SUMMARY\n          echo \"**Image:** \\`${{ env.REGISTRY }}/${{ needs.info.outputs.image_name }}\\`\" >> $GITHUB_STEP_SUMMARY\n          echo \"**Version:** \\`${{ needs.info.outputs.version }}\\`\" >> $GITHUB_STEP_SUMMARY\n          echo \"**Tags:** \\`${{ needs.info.outputs.version }}\\`, \\`latest\\`\" >> $GITHUB_STEP_SUMMARY\n          echo \"**Platforms:** linux/amd64, linux/arm64\" >> $GITHUB_STEP_SUMMARY\n          echo \"\" >> $GITHUB_STEP_SUMMARY\n          echo \"**Pull:**\" >> $GITHUB_STEP_SUMMARY\n          echo \"\\`\\`\\`bash\" >> $GITHUB_STEP_SUMMARY\n          echo \"docker pull ${{ env.REGISTRY }}/${{ needs.info.outputs.image_name }}:${{ needs.info.outputs.version }}\" >> $GITHUB_STEP_SUMMARY\n          echo \"\\`\\`\\`\" >> $GITHUB_STEP_SUMMARY\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n.idea/dbnavigator.xml\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.*\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# env files (can opt-in for committing if needed)\n.env\n.api-keys-for-development.sensitive.donotcommitthisever.env.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n\n# mistral experiments\n/app/api/experiments/mistral-ai/chat/route.ts\n/app/experiments/mistral-ai/page.tsx\n\n# widget build files\n/public/widgets\n\n/public/widget-announcement.js\n/public/widget-bundle.js\n/public/widget-classic.js\n/public/widget-floating.js\n/public/widget-modal.js\n# mapping files ( generated )\n/public/widget-announcement.js.map\n/public/widget-bundle.js.map\n/public/widget-classic.js.map\n/public/widget-floating.js.map\n/public/widget-modal.js.map\n\n# sensitive changerawr shenanigans\n.changerawr\n.changerawr/donotdeletethisfolder/donotdeletethisfile.chrcli.json\nnul\nnull\n.chtsredevmscli\n.claude\n\n# sponsorer server\n/sponsor-server\n\n# user requests to add\nCLAUDE.md\n"
  },
  {
    "path": ".idea/.gitignore",
    "content": "# Default ignored files\n/shelf/\n/workspace.xml\n# Editor-based HTTP Client requests\n/httpRequests/\n# Datasource local storage ignored files\n/dataSources/\n/dataSources.local.xml\n"
  },
  {
    "path": ".idea/changerawr.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"WEB_MODULE\" version=\"4\">\n  <component name=\"NewModuleRootManager\">\n    <content url=\"file://$MODULE_DIR$\">\n      <excludeFolder url=\"file://$MODULE_DIR$/.tmp\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/temp\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/tmp\" />\n    </content>\n    <orderEntry type=\"inheritedJdk\" />\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n  </component>\n</module>"
  },
  {
    "path": ".idea/dataSources.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"DataSourceManagerImpl\" format=\"xml\" multifile-model=\"true\">\n    <data-source source=\"LOCAL\" name=\"postgres@localhost\" uuid=\"4fcad316-be2f-4d50-a0f9-b7d894b57b4d\">\n      <driver-ref>postgresql</driver-ref>\n      <synchronize>true</synchronize>\n      <jdbc-driver>org.postgresql.Driver</jdbc-driver>\n      <jdbc-url>jdbc:postgresql://localhost:5432/postgres</jdbc-url>\n      <working-dir>$ProjectFileDir$</working-dir>\n    </data-source>\n    <data-source source=\"LOCAL\" name=\"changerawr@localhost\" uuid=\"0a8987b9-d685-49ea-87ac-49c168eafa51\">\n      <driver-ref>postgresql</driver-ref>\n      <synchronize>true</synchronize>\n      <jdbc-driver>org.postgresql.Driver</jdbc-driver>\n      <jdbc-url>jdbc:postgresql://localhost:5432/changerawr</jdbc-url>\n      <working-dir>$ProjectFileDir$</working-dir>\n    </data-source>\n  </component>\n</project>"
  },
  {
    "path": ".idea/data_source_mapping.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"DataSourcePerFileMappings\">\n    <file url=\"file://$PROJECT_DIR$/prisma/migrations/20251111210147_add_api_key_and_widgets/migration.sql\" value=\"0a8987b9-d685-49ea-87ac-49c168eafa51\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/db-forest-config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"db-tree-configuration\">\n    <option name=\"data\" value=\"----------------------------------------&#10;1:0:4fcad316-be2f-4d50-a0f9-b7d894b57b4d&#10;2:0:0a8987b9-d685-49ea-87ac-49c168eafa51&#10;\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/discord.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"DiscordProjectSettings\">\n    <option name=\"show\" value=\"PROJECT_FILES\" />\n    <option name=\"description\" value=\"\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/inspectionProfiles/Project_Default.xml",
    "content": "<component name=\"InspectionProjectProfileManager\">\n  <profile version=\"1.0\">\n    <option name=\"myName\" value=\"Project Default\" />\n    <inspection_tool class=\"DuplicatedCode\" enabled=\"true\" level=\"WEAK WARNING\" enabled_by_default=\"true\">\n      <Languages>\n        <language minSize=\"106\" name=\"TypeScript\" />\n      </Languages>\n    </inspection_tool>\n    <inspection_tool class=\"Eslint\" enabled=\"true\" level=\"WARNING\" enabled_by_default=\"true\" />\n    <inspection_tool class=\"IncorrectFormatting\" enabled=\"true\" level=\"WEAK WARNING\" enabled_by_default=\"true\" />\n  </profile>\n</component>"
  },
  {
    "path": ".idea/jsLibraryMappings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"JavaScriptLibraryMappings\">\n    <includedPredefinedLibrary name=\"Node.js Core\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/modules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"ProjectModuleManager\">\n    <modules>\n      <module fileurl=\"file://$PROJECT_DIR$/.idea/changerawr.iml\" filepath=\"$PROJECT_DIR$/.idea/changerawr.iml\" />\n    </modules>\n  </component>\n</project>"
  },
  {
    "path": ".idea/sqldialects.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"SqlDialectMappings\">\n    <file url=\"file://$PROJECT_DIR$/prisma/migrations/20251111210147_add_api_key_and_widgets/migration.sql\" dialect=\"PostgreSQL\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/vcs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"VcsDirectoryMappings\">\n    <mapping directory=\"$PROJECT_DIR$\" vcs=\"Git\" />\n  </component>\n</project>"
  },
  {
    "path": "APIDOCGUIDE.md",
    "content": "# API Documentation Generation Guide\n\n## Overview\nWhen writing API documentation for routes, focus on creating clear, consistent, and comprehensive JSDoc comments that can be automatically parsed into OpenAPI/Swagger specifications. This guide outlines the required format and components for documenting API endpoints.\n\n## Documentation Format\nEach route handler should be documented using JSDoc comments with specific tags that map to OpenAPI/Swagger fields.\n\n### Basic Structure\n```typescript\n/**\n * Brief description of the endpoint\n * @method HTTP_METHOD\n * @description Detailed description of what the endpoint does\n * @body Request body schema in JSON format\n * @response Status code and response schema\n * @error Error status codes and descriptions\n * @secure (optional) Security requirements\n */\n```\n\n## Required Tags\n\n### @method\nSpecifies the HTTP method. Must be one of: GET, POST, PUT, DELETE, PATCH\n```typescript\n@method POST\n```\n\n### @description\nDetailed description of the endpoint's functionality\n```typescript\n@description Authenticates a user and returns JWT tokens for subsequent requests\n```\n\n### @body (for POST/PUT/PATCH)\nJSON schema of the request body. Must be valid JSON format.\n```typescript\n@body {\n  \"type\": \"object\",\n  \"required\": [\"email\", \"password\"],\n  \"properties\": {\n    \"email\": {\n      \"type\": \"string\",\n      \"format\": \"email\",\n      \"description\": \"User's email address\"\n    },\n    \"password\": {\n      \"type\": \"string\",\n      \"minLength\": 8,\n      \"description\": \"User's password\"\n    }\n  }\n}\n```\n\n### @response\nResponse schema for successful requests. Include status code and JSON schema.\n```typescript\n@response 200 {\n  \"type\": \"object\",\n  \"properties\": {\n    \"id\": { \"type\": \"string\" },\n    \"name\": { \"type\": \"string\" },\n    \"email\": { \"type\": \"string\" }\n  }\n}\n```\n\n### @error\nError responses with status codes and descriptions.\n```typescript\n@error 400 Validation failed - Invalid input format\n@error 401 Unauthorized - Invalid credentials\n@error 404 Resource not found\n```\n\n### @secure (Optional)\nAuthentication requirements. Default is 'cookieAuth' for cookie-based authentication.\n```typescript\n@secure bearerAuth\n```\n\n## Examples\n\n### GET Endpoint\n```typescript\n/**\n * @method GET\n * @description Retrieves user profile information\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"name\": { \"type\": \"string\" },\n *     \"email\": { \"type\": \"string\" },\n *     \"role\": { \"type\": \"string\" }\n *   }\n * }\n * @error 401 Unauthorized - Please log in\n * @error 404 User not found\n * @secure cookieAuth\n */\nexport async function GET() {\n  // Implementation\n}\n```\n\n### POST Endpoint\n```typescript\n/**\n * @method POST\n * @description Creates a new user account\n * @body {\n *   \"type\": \"object\",\n *   \"required\": [\"email\", \"password\", \"name\"],\n *   \"properties\": {\n *     \"email\": {\n *       \"type\": \"string\",\n *       \"format\": \"email\",\n *       \"description\": \"User's email address\"\n *     },\n *     \"password\": {\n *       \"type\": \"string\",\n *       \"minLength\": 8,\n *       \"description\": \"User's password\"\n *     },\n *     \"name\": {\n *       \"type\": \"string\",\n *       \"description\": \"User's full name\"\n *     }\n *   }\n * }\n * @response 201 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"email\": { \"type\": \"string\" },\n *     \"name\": { \"type\": \"string\" }\n *   }\n * }\n * @error 400 Validation failed\n * @error 409 Email already exists\n */\nexport async function POST() {\n  // Implementation\n}\n```\n\n## Common Response Types\n\n### Paginated Response\n```typescript\n@response 200 {\n  \"type\": \"object\",\n  \"properties\": {\n    \"items\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"string\" },\n          \"title\": { \"type\": \"string\" }\n        }\n      }\n    },\n    \"pagination\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"total\": { \"type\": \"number\" },\n        \"page\": { \"type\": \"number\" },\n        \"pageSize\": { \"type\": \"number\" },\n        \"totalPages\": { \"type\": \"number\" }\n      }\n    }\n  }\n}\n```\n\n### Error Response\n```typescript\n@error 400 {\n  \"type\": \"object\",\n  \"properties\": {\n    \"error\": { \"type\": \"string\" },\n    \"details\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"path\": { \"type\": \"string\" },\n          \"message\": { \"type\": \"string\" }\n        }\n      }\n    }\n  }\n}\n```\n\n## Best Practices\n\n1. **Consistency**: Use consistent naming and formatting across all endpoints\n2. **Completeness**: Document all possible response codes and edge cases\n3. **Validation**: Always include validation requirements in the schema\n4. **Security**: Specify authentication requirements for protected routes\n5. **Examples**: Provide examples for complex request/response schemas\n6. **Descriptions**: Write clear, concise descriptions for all parameters\n\n## Common Schemas\n\nConsider extracting common schemas to reduce duplication:\n\n### User Schema\n```typescript\nconst UserSchema = {\n  \"type\": \"object\",\n  \"properties\": {\n    \"id\": { \"type\": \"string\" },\n    \"email\": { \"type\": \"string\", \"format\": \"email\" },\n    \"name\": { \"type\": \"string\" },\n    \"role\": { \"type\": \"string\", \"enum\": [\"user\", \"admin\"] }\n  }\n}\n```\n\n### Pagination Schema\n```typescript\nconst PaginationSchema = {\n  \"type\": \"object\",\n  \"properties\": {\n    \"page\": { \"type\": \"number\", \"minimum\": 1 },\n    \"pageSize\": { \"type\": \"number\", \"minimum\": 1 },\n    \"total\": { \"type\": \"number\" },\n    \"totalPages\": { \"type\": \"number\" }\n  }\n}\n```\n\nRemember to validate your documentation against the OpenAPI specification to ensure compatibility with Swagger UI and other API documentation tools."
  },
  {
    "path": "CHANGELOG.md",
    "content": "<!-- Generated by Changerawr CLI on 2026-04-16T07:21:32.297Z -->\n# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n## v1.0.6\n\n- **Version 1.0.6 - 2026-04-16**: :::warning\nThere are security vulnerabilities in previous releases, it is recommended to upgrade as soon as possible to this one. There are also a breaking change, set `SETUP_COMPLETE=true` in your .env file as this is how to complete the setup wizard now. I strongly recommend re-rolling all API keys.\n:::\n\n## Features\n- **Slack Integration** - You can now automatically post published entries to your Slack!\n- **Timezones** - Added timezone support per-user and globally, allowing for much more efficient collaboration between users with different time zones!\n- **SAML** - Adds support for SAML to SSO, adding a new method available for configuring single-sign-on.\n- *SSL Certificates for custom domains** - You can now get SSL certificates for custom domains! Just follow the nginx-sidecar documentation to get set up.\n- **IP Whitelisting** - You can now whitelist IP addresses to access the main panel. Note that this requires the nginx-sidecar to be set up.\n- **Version Templates** - Configure custom version format templates\n\n## Improvements\n- **Extended Project Limits ** - Project limits have been extended for entries. If you wish to go beyond, please sponsor my work.\n- **UI Improvements** - Did various UI improvements to the administrative interface & fully redesigned the version picker\n- **Update Bar** - Whenever an update is available, you'll be much more likely to know thanks to the new pop-up.\n\n## Bug Fixes\n- **Redirect Loop** - fixed issues with an infinite redirect loop after setup\n- **Strict Validation Local Domain** - Fixed issues with any domain being blocked for requests, preventing e.g., cloudflareinsghts from working, which subsequently blocked the panel from loading alltogether itself.\n- **System user show-up in admin dashboard** - Fixed an issue where the system user used for audit logs was being counted as a user in the total user count.\n\n## Other\n- **Security Vulnerability patches** - A few vulnerabilities were disclosed to me, these have been patched in this release.\n- **Dependency Updates** - Updated to the latest dependencies, Nodemailer 8, and changerawr universal markdown 1.2.0!\n\n\n## v1.0.5\n\n- **Version 1.0.5 - 2025-11-13**: :::warning\nThere are breaking changes in this release.\n:::\n\nIf you are using widgets at all, you will have to recreate them. If you are not using widgets, this does not affect you at all.\n\n\n\n---\n## Bug Fixes\n\n- **Email validation allowed for uppercase emails** - If you invited a email with any uppercase characters, you would not be able to login using it. This has been fixed!\n\n- **undefined showing up in the create SSO provider modal** - Sometimes, this would show up when you get a callback URL. This has been fixed.\n\n### Features\n\n- **Catch Up!** - Missed too much while you were gone? Now you can catch up! `feature requested by my founder friends @ forento`\n\n- **Manually set when entry has been published** - I'll admit, I hated adding this request. Being transparent with your users is extremely important. However, it is now available for those who need it. Have fun!\n\n- **API Key Permissions + Project-Level API Keys** - Added a full permission system to API keys as well as making the ability to have them be project-level.\n\n- **Migrated from internal engine to package** - Migrated from the internal CUM engine to the package. Also adds support for Tables and discord-flavored SubText!\n\n- **Redid widgets entirely, proving four variants that can be customized to your hearts content!**\n\n\n## Improvements\n\n- **Share on publish** - You can now share your changelog to your email subscribers when you publish an entry!\n\n- **Improved bookmarks usage** - Dedicated pages for bookmarking + QOL controls!\n\n- **You can now view changelog entries individually** - A dedicated page is available now for viewing a specific changelog entry.\n\n- **NextJS Upgrade** - Upgraded to NextJS 16!\n\n## 1.0.4\n\n- **Version 1.0.4 - 2025-?-?**: :::warning\nThere are no breaking changes in this release.\n:::\n\n### Features\n- **Data Importing** - You can now import data from various sources to jump-start your Changerawr project!\n- **CUM** - Changerawr Universal Markdown engine introduces more functionality for the content editor.As of writing, Embeds, Buttons, and Alerts are available! \nProvides a better experience overall, improving the parsing engine, adding tokenization, and improving overall usability!\n\n### Improvements\n- **Redesigned Projects Dashboard** - Updated the dashboard at (/dashboard/projects/projectId) to be less cluttered and more professional.\n- **Updated Projects Fetch Pagination** - Updated the default fetch for projects to 50 entries when listing.\n- **Redesigned Main Dashboard** - Redesigned the dashboard at (/dashboard), it needed a facelift and has been the same since 0.3.0 :(\n- **Content Editor Upgrades** - Introduces the CUM ( Changerawr Universal Markdown ) rendering engine for a better changelog experience.\n- **Improved Modal UI** - Added depth and optical borders to the modal UI. Also adds a \"disableClose\" prop for the rare case in which Changerawr is doing something and needs to let you know of its progress.\n\nhello world!\n\n\n\n## 1.0.3\n\n- **Version 1.0.3 - 2025-07-02**: **Features**\n\n- **Changerawr CLI** - Now it's easier then ever to get started with Changerawr! just do:\n```shell\nnpm install -g changerawr\n```\nYou can find the source code at <https://github.com/changerawr/cli> - would super appreciate a star!\n\n- Native [Pocket ID](https://pocket-id.org) Support. I really love [Pocket ID](https://pocket-id.org) so now you can configure it in three clicks from the installation wizard!\n\n\n**Bug Fixes**\n\n- **Migration Fixes** - Fixed an issue that broke deployment\n- **Setup Wizard Fixes** - Rewrote logic that broke older APIs, wizard will no longer skip steps, allowing you to finish setup completely.\n\n## 1.0.2\n\n- **Version 1.0.2 - 2025-06-24**: **Features**\n\n- **Custom Domains** - Added support for custom domains for a public changelog page. This was painful to implement :(\n- **Scheduled Publishing** - You can now schedule your changelog to be posted at a later date.\n- **Full-Text search** - Adds a command palette ( Ctrl + K / Cmd + K ) to search EVERYTHING :)\n- **Colored Tags** - Add color to your tags to make them pop!\n- **Telemetry** - Adds telemetry so the Changerawr team can collect anonymous usage statistics.\n\n\n**Improvements**\n\n- **UI Improvements** - Updated Request Management interface to provide more information to assist administrators with approving/denying a staff request.\n- **SSO Improvements** - Added a section to user settings so you can see what providers you have connected with!\n\n## 1.0.1\n\n- **Version 1.0.1 - 2025-06-22**: **Bug Fixes**\n\n- **Fixed LOGIN_ATTEMPT issue** - Thanks @aixxo ( on GitHub ) for reporting this! It has been fixed in this release.\n- **Security Vulnerability Patched** - AI assistant API key could be found without authenticating, this has been encrypted and fixed. Thanks to Codycody31 on GitHub for reporting this! You should set a brand new API key if you are using the AI integration.\n- **GitHub Integration scroll fix** - Fixed an issue that prevented you from seeing all of the content, which would get cut off and prevent interaction with the modal. ( you could not insert the content, as an example )\n\n\n**Improvements**\n\n- **HIBP Integration** - When logging in, you will be notified if your password was found in a data breach.\n\n## 1.0.0\n\n- **Version 1.0.0 - 2025-06-15**: **Other**\n\n- **1.0.0 Release** - Thanks for your support of Changerawr! Have fun!\n\n## 0.4.0\n\n- **Version 0.4.0 - 2025-06-13**: **Features**\n\n- **Changelog Analytics** - Detailed analytics of your overall instance and projects! Cookieless, GDPR compliant, and 100% anonymized\n- **Easypanel Integration** - Changerawr can now update automagically if you use Easypanel for deployment!\n- **Automatic OAuth2 Setup** - If you use Easypanel OAuth2, you have the option to automatically setup OAuth2 for SSO if you have all the required variables set beforehand.\n\n\n**Bug Fixes**\n\n- **Bug Fixes** - A multitude of fixes and quirks that sometimes don't work correctly. Hopefully, this is every issue as of now fixed!\n\n\n**Improvements**\n\n- **Redesigned Components** - Redesigned some components\n\n## 0.3.9\n\n- **Version 0.3.9 - 2025-06-10**: **Features**\n\n- **Code Analyzation for GitHub Integration** - Changerawr can now look at your changed code and write a better changelog!\n\n\n**Bug Fixes**\n\n- **Account Deletion Fix** - Fixed an issue that prevented administrators from deleting a user's account. This process will anonymize all their data and actions while preserving their contributions.\n\n\n**Improvements**\n\n- **Redesigned the New Project page** - Gave a much-needed facelift to the new project page so it feels more roomy. Has confetti now, success messages, and more excitement!\n\n## 0.3.8\n\n- **Version 0.3.8 - 2025-06-10**: **Features**\n\n- **Invite Your Team!** - You can now invite your team early in the initial setup wizard, rawrsome!\n\n\n**Bug Fixes**\n\n- **Changelog Editor Fixes** - Fixed the version picker which would let you select versions already used, leading to issues. It will now check for overlapping before allowing you to set a version for your changelog.\n\n\n**Improvements**\n\n- **Component Redesigns** - Redesigned the Version picker and Editor header\n\n## 0.3.7\n\n- **Version 0.3.7 - 2025-06-09**: **Bug Fixes**\n\n- **Theme Switcher Fixes** - Fixed broken theme switcher logic, proper sync and no refreshing now!\n\n\n**Improvements**\n\n- **SSO Improvements** - You can now set all of the URLs for an SSO provider!\n\n## 0.3.6\n\n- **Version 0.3.6 - 2025-06-08**: **Bug Fixes**\n\n- **Fix Audit Log Issues** - Fixed any remaining bugs with audit logs.\n\n\n**Improvements**\n\n- **AI URL Change** - Changed some of the URLs the AI Assistant page links to.\n- **Improved Generation UI For GitHub** - Improved the generation UI and added more options\n\n## 0.3.5\n\n- **Version 0.3.5 - 2025-06-07**: **Features**\n\n- **GitHub integration** - You can now use GitHub with Changerawr!\n\n\n**Other**\n\n- **Routine Maintenance** - Package Updates and Bug Fixes have been triaged successfully with this release.\n\n## 0.3.4\n\n- **Version 0.3.4 - 2025-05-15**: **Features**\n\n- **Requests Manager** - Staff users can now view their permission requests!\n\n\n**Bug Fixes**\n\n- **TagAI Fixes** - Fixed TagAI to be more successful when recommending tags for a user's changelog entry ( e.g. fails less )\n\n## 0.3.3\n\n- **Version 0.3.3 - 2025-05-13**: **Features**\n\n- **Title Generation** - AI can now analyze your writing to pick out the perfect title for your next changelog\n- **AI Assisted Tagging** - AI can now assist you with tagging your changelog\n- **Add New Tags** - You can now add a tag if it doesn't exist\n\n\n**Improvements**\n\n- **Better AI Assistant Design** - Redesigned AI Assistant\n\n## 0.3.1\n\n- **Version 0.3.1 - 2025-05-12**: **Features**\n\n- **New Feature** - Added \"What's New\" model to inform of latest changes from the most recent Changerawr update\n\n\n**Bug Fixes**\n\n- **Bug Fixes** - Fixed a few bugs with tokenization and the AI assistant\n\n## 0.3.2\n\n- **Version 0.3.2 - 2025-05-12**: **Features**\n\n- **'What's New' Modal Available on the About Page** - The \"What's New\" modal is now accessible from the About page.\n\n\n**Improvements**\n\n- **Redesigns** - Redesigned \"What's New\" modal.\n\n## 0.3.0\n\n- **Version 0.3.0 - 2025-05-11**: **Features**\n\n- **AI Assistant** - Adds an AI assistant to Changerawr!\n- **\"What's New\" model added** - Allow administrators to see what's new in Changerawr for the latest update!\n\n\n**Bug Fixes**\n\n- **Fixed Authentication Redirect** - Fixed authentication pages not redirecting once a session was obtained\n\n\n**Improvements**\n\n- **Redone Content Editor** - Redid the entire Content Editor from scratch\n\n"
  },
  {
    "path": "Caddyfile",
    "content": "# Caddyfile for Changerawr with on-demand TLS\n# Caddy automatically obtains and renews Let's Encrypt certificates\n\n{\n    # Global options\n    email {$ACME_EMAIL}\n\n    # Enable on-demand TLS with ask endpoint\n    on_demand_tls {\n        ask http://app:3000/api/domain-check\n    }\n}\n\n# Catch-all for custom domains\n:80, :443 {\n    # Enable on-demand TLS for all incoming requests\n    tls {\n        on_demand\n    }\n\n    # Reverse proxy all requests to the Next.js app\n    reverse_proxy app:3000 {\n        # Pass original host header\n        header_up Host {host}\n\n        # Pass real IP and protocol\n        header_up X-Real-IP {remote_host}\n        header_up X-Forwarded-For {remote_host}\n        header_up X-Forwarded-Proto {scheme}\n        header_up X-Forwarded-Host {host}\n    }\n\n    # Enable compression\n    encode gzip\n\n    # Logging\n    log {\n        output stdout\n        format console\n    }\n}\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:22-alpine AS base\n\n# Install dependencies only when needed\nFROM base AS deps\nWORKDIR /app\n\n# Copy package files\nCOPY package.json package-lock.json* ./\n# CACHEBUST forces npm install to re-run even when package files are unchanged\nARG CACHEBUST=1\nRUN echo \"Cache bust: $CACHEBUST\" && npm install --legacy-peer-deps\n\n# Rebuild the source code only when needed\nFROM base AS builder\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\n\n# Copy the entire project\nCOPY . .\n\n# Generate Prisma client\nRUN npx prisma generate\n\n# Next.js collects completely anonymous telemetry data about general usage.\n# Learn more here: https://nextjs.org/telemetry\n# Uncomment the following line in case you want to disable telemetry during the build.\nENV NEXT_TELEMETRY_DISABLED 1\nENV CI_BUILD_MODE 1\nENV DOCKER_BUILD 1\n\n# Build the app\nRUN npm run build\n\n# Production image, copy all the files and run next\nFROM base AS runner\nWORKDIR /app\n\nENV NODE_ENV production\nENV NEXT_TELEMETRY_DISABLED 1\n\n# Install all dependencies to satisfy entrypoint requirements\nCOPY package.json package-lock.json* ./\nRUN npm install --legacy-peer-deps\n# Install Prisma client with exact version match\nRUN npm uninstall prisma @prisma/client --legacy-peer-deps\nRUN npm install prisma@6.7.0 @prisma/client@6.7.0 --legacy-peer-deps\n# Install tsx explicitly\nRUN npm install -g tsx\n\n# Install esbuild for widget\nRUN npm install esbuild --legacy-peer-deps\n\n# Install JSDOC\nRUN npm install -g jsdoc\n\n# Add bash, nginx, and other dependencies for the entry script\nRUN apk add --no-cache bash wget nginx\n\n# Install nginx-agent from GitHub\nRUN wget -q https://github.com/Changerawr/nginx-agent/archive/refs/heads/master.tar.gz -O /tmp/nginx-agent.tar.gz && \\\n    mkdir -p /nginx-agent && \\\n    tar -xzf /tmp/nginx-agent.tar.gz -C /nginx-agent --strip-components=1 && \\\n    rm /tmp/nginx-agent.tar.gz && \\\n    cd /nginx-agent && \\\n    npm install --production\n\n# Create nginx directories\nRUN mkdir -p /etc/nginx/sites-enabled /etc/nginx/sites-available /etc/ssl/changerawr /var/log/nginx /var/lib/nginx/tmp /run/nginx\n\n# Copy the entire project from the builder stage\nCOPY --from=builder /app .\n\n# Copy maintenance page and server script\nCOPY scripts/maintenance/index.html ./index.html\nCOPY scripts/maintenance/server.js ./scripts/maintenance/server.js\n\n# Copy nginx configuration\nCOPY nginx.conf /etc/nginx/nginx.conf\n\n# Copy and make nginx reload script executable\nCOPY scripts/nginx-reload.sh /usr/local/bin/nginx-reload.sh\nRUN chmod +x /usr/local/bin/nginx-reload.sh\n\n# Ensure the entrypoint script is executable\nRUN chmod +x ./docker-entrypoint.sh\n\nEXPOSE 3000 80 443\n\nENV PORT 3000\nENV HOSTNAME \"0.0.0.0\"\n\n# Use entrypoint for running the build scripts before starting the server\nENTRYPOINT [\"/app/docker-entrypoint.sh\"]\nCMD [\"npm\", \"start\"]"
  },
  {
    "path": "Dockerfile.compose",
    "content": "FROM node:20-alpine AS base\n\n# Install dependencies only when needed\nFROM base AS deps\nWORKDIR /app\n\n# Copy package files\nCOPY package.json package-lock.json* ./\nRUN npm install --legacy-peer-deps\n\n# Rebuild the source code only when needed\nFROM base AS builder\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\n\n# Copy the entire project\nCOPY . .\n\n# Generate Prisma client\nRUN npx prisma generate\n\n# Next.js collects completely anonymous telemetry data about general usage.\nENV NEXT_TELEMETRY_DISABLED 1\nENV DOCKER_BUILD 1\nENV CI_BUILD_MODE 1\n\n# Build the app\nRUN npm run build\n\n# Production image, copy all the files and run next\nFROM base AS runner\nWORKDIR /app\n\nENV NODE_ENV production\nENV NEXT_TELEMETRY_DISABLED 1\n\n# Install all dependencies to satisfy entrypoint requirements\nCOPY package.json package-lock.json* ./\nRUN npm install --legacy-peer-deps\n\n# Install Prisma client with exact version match\nRUN npm uninstall prisma @prisma/client --legacy-peer-deps\nRUN npm install prisma@6.3.1 @prisma/client@6.3.1 --legacy-peer-deps\n\n# Install tsx explicitly\nRUN npm install -g tsx\n\n# Install esbuild for widget\nRUN npm install esbuild --legacy-peer-deps\n\n# Install JSDOC\nRUN npm install -g jsdoc\n\n# Add bash and curl for the entry script and health checks\nRUN apk add --no-cache bash curl\n\n# Copy the entire project from the builder stage\nCOPY --from=builder /app .\n\n# Copy maintenance page and server script\nCOPY scripts/maintenance/index.html ./index.html\nCOPY scripts/maintenance/server.js ./scripts/maintenance/server.js\n\n# Copy entrypoint script and fix line endings\nCOPY docker-entrypoint-compose.sh /app/docker-entrypoint-compose.sh\nRUN sed -i 's/\\r$//' /app/docker-entrypoint-compose.sh && chmod +x /app/docker-entrypoint-compose.sh\n\nEXPOSE 3000\n\nENV PORT 3000\nENV HOSTNAME \"0.0.0.0\"\n\n# Use npm script that explicitly calls bash\nENTRYPOINT [\"bash\", \"docker-entrypoint-compose.sh\"]"
  },
  {
    "path": "LICENSE",
    "content": "CHANGERAWR NON-COMMERCIAL OPEN SOURCE LICENSE\n\nCopyright (c) 2025 Supernova Software, LLC. All rights reserved.\n\nThis software and associated documentation files (the \"Software\") are made available \nunder the terms of this license. The source code is publicly accessible and may be \nfreely used, modified, and distributed subject to the restrictions outlined below.\n\nGRANT OF LICENSE\n\nSupernova Software, LLC grants you a worldwide, royalty-free, non-exclusive license \nto use, copy, modify, merge, publish, and distribute the Software and derivative \nworks thereof, subject to the following conditions.\n\nPERMITTED USES\n\nYou may freely:\n\n1. Use the Software for any purpose, including commercial purposes, provided you \n   do not profit from the Software itself.\n\n2. Modify the Software and create derivative works, including closed-source versions, \n   provided you comply with the terms of this license.\n\n3. Fork the repository and maintain your own version of the Software.\n\n4. Distribute the Software, modified or unmodified, in source or binary form.\n\n5. Deploy the Software on your own infrastructure, including for use in commercial \n   organizations and business operations.\n\n6. Contribute improvements, bug fixes, and features back to the project.\n\nPROHIBITED USES\n\nThe following uses are strictly prohibited unless you are Supernova Software, LLC \nor an entity explicitly authorized in writing by Supernova Software, LLC:\n\n1. Profiting from the Software: You may not sell, rent, lease, license, or otherwise \n   monetize the Software or any derivative work. This includes but is not limited to:\n   - Selling access to the Software as a product or service\n   - Charging fees for hosted instances of the Software\n   - Offering the Software as part of a paid subscription or service\n   - Selling modified or unmodified versions of the Software\n   - Generating revenue directly from providing access to the Software's functionality\n\n2. Billing System Integration: You may not integrate any billing system, payment \n   processor, paywall, subscription mechanism, or any other monetization feature \n   into the Software or derivative works that would allow charging users for the \n   Software itself or its features. This prohibition applies regardless of whether \n   the fork is open source or closed source.\n\n3. Trademark Misuse: You may not use the names \"Changerawr,\" \"Supernova3339,\" or \n   \"Supernova Software, LLC,\" along with any associated logos or branding, in a \n   manner that suggests endorsement or affiliation without express written permission.\n\n4. Copyright Removal: You may not remove, obscure, or alter any copyright notices, \n   license terms, or attribution present in the Software or documentation under any \n   circumstances. This applies even if you use only a portion of the code.\n\nCLARIFICATION OF COMMERCIAL USE\n\nYou MAY use the Software in a commercial environment (e.g., within your business, \nas a tool for your operations, or as part of your internal infrastructure). What \nyou CANNOT do is make money by selling the Software itself or charging others for \naccess to it.\n\nCONTRIBUTIONS\n\nBy submitting code, documentation, or other materials to this project:\n\n1. You grant Supernova Software, LLC an irrevocable, perpetual, worldwide, royalty-free \n   license to use, reproduce, modify, display, distribute, and sublicense your \n   contributions in any manner, including in commercial products.\n\n2. You represent that you have all necessary rights to grant this license and that \n   your contributions do not infringe any third party rights.\n\n3. You acknowledge that Supernova Software, LLC has no obligation to incorporate \n   your contributions and may use them without restriction.\n\nINTELLECTUAL PROPERTY AND ATTRIBUTION\n\nAll copyright and intellectual property rights in the original Software remain with \nSupernova Software, LLC. While the Software is open source and freely available, \nthis license does not transfer ownership of the intellectual property.\n\nAll copies and derivative works must retain the original copyright notice and this \nlicense. Modified versions should clearly indicate what changes were made and by whom.\n\nCRITICAL: Even if you extract or use only a portion of the code from this Software, \nyou MUST retain all original copyright notices in your project. This requirement \napplies to all uses without exception, including closed-source forks, commercial \napplications, and packages published on any registry.\n\nENFORCEMENT\n\nViolation of this license constitutes copyright infringement and breach of contract. \nSupernova Software, LLC reserves the right to pursue all available legal remedies, \nincluding injunctive relief and monetary damages, against any party that violates \nthese terms.\n\nTERMINATION\n\nThis license is effective until terminated. Supernova Software, LLC may terminate \nyour rights under this license immediately if you fail to comply with any term. \nUpon termination:\n\n1. You must immediately cease all use of the Software.\n2. You must destroy all copies of the Software in your possession or control.\n3. You must certify in writing to Supernova Software, LLC that you have complied \n   with these requirements, if requested.\n\nTermination does not limit any other rights or remedies available to Supernova \nSoftware, LLC.\n\nFEATURE GUARANTEE\n\nSupernova Software, LLC guarantees that all features of Changerawr will remain free \nand accessible at no cost, regardless of future developments or service offerings. \nThis guarantee applies to the core Software as distributed in the public repository.\n\nThe sole exception to this guarantee is if Supernova Software, LLC creates an official \nmanaged cloud hosting service. In such case, fees may be charged exclusively to cover:\n- Server and infrastructure costs for the client's virtual private server (VPS)\n- Management and maintenance services\n- Service-based add-ons that do not affect the core Software functionality\n\nService-based add-ons are limited to operational services such as prioritized support, \ndedicated single sign-on (SSO) infrastructure, enhanced SLA agreements, or professional \nservices. Add-ons will NEVER include software features, integrations, or functionality \nthat affects the Changerawr core itself. All software features and integrations remain \nfree in both self-hosted and any potential cloud offerings.\n\nAny such cloud offering would be optional, and the self-hosted version of Changerawr \nwould remain completely free with all features intact.\n\nWARRANTY DISCLAIMER\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EXPRESS, IMPLIED, \nSTATUTORY, OR OTHERWISE. THIS INCLUDES, WITHOUT LIMITATION, ANY WARRANTIES OF \nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, OR NON-INFRINGEMENT.\n\nSUPERNOVA SOFTWARE, LLC MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY, \nRELIABILITY, AVAILABILITY, TIMELINESS, SECURITY, OR ACCURACY OF THE SOFTWARE.\n\nNO WARRANTIES ARE PROVIDED UNLESS EXPLICITLY STATED IN A SEPARATE WRITTEN AGREEMENT \nBETWEEN YOU AND SUPERNOVA SOFTWARE, LLC. THE ABSENCE OF SUCH AN AGREEMENT MEANS \nYOU RECEIVE THE SOFTWARE WITH ABSOLUTELY NO WARRANTY WHATSOEVER.\n\nLIMITATION OF LIABILITY\n\nIN NO EVENT SHALL SUPERNOVA SOFTWARE, LLC BE LIABLE FOR ANY SPECIAL, INCIDENTAL, \nINDIRECT, OR CONSEQUENTIAL DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES \nFOR LOSS OF BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, \nOR ANY OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE \nSOFTWARE, EVEN IF SUPERNOVA SOFTWARE, LLC HAS BEEN ADVISED OF THE POSSIBILITY OF \nSUCH DAMAGES.\n\nGOVERNING LAW\n\nThis license shall be governed by and construed in accordance with the laws of the \njurisdiction in which Supernova Software, LLC is registered, without regard to its \nconflict of law provisions. Any legal action or proceeding arising under this license \nshall be brought exclusively in the courts of that jurisdiction.\n\nSEVERABILITY\n\nIf any provision of this license is found to be unenforceable or invalid, that \nprovision shall be limited or eliminated to the minimum extent necessary so that \nthis license shall otherwise remain in full force and effect.\n\nENTIRE AGREEMENT\n\nThis license constitutes the entire agreement between you and Supernova Software, LLC \nconcerning the Software and supersedes any prior agreements or understandings.\n\nCONTACT\n\nFor licensing inquiries, alternative licensing arrangements, or clarification of \nthese terms, contact Supernova Software, LLC through \nhttps://github.com/Supernova3339/changerawr.\n\nNO WAIVER\n\nThe failure of Supernova Software, LLC to enforce any right or provision of this \nlicense shall not constitute a waiver of such right or provision.\n"
  },
  {
    "path": "PROJECT_INDEX.md",
    "content": "# Project Index: Changerawr\n\nGenerated: 2026-04-15\n\n## Overview\n\nChangerawr is a self-hosted changelog management platform with AI assistance, custom domains, SSO/SAML, Slack/GitHub integrations, and embeddable widgets.\n\n**License**: CNC OSL (Non-Commercial Open Source)\n\n---\n\n## 📁 Project Structure\n\n```\nchangerawr/\n├── app/                    # Next.js App Router (pages + API routes)\n│   ├── (auth)/             # Auth page group (login, register, setup, 2FA)\n│   ├── (email)/            # Email-related pages (unsubscribed)\n│   ├── api/                # 145+ API route handlers\n│   ├── dashboard/          # Authenticated dashboard\n│   │   ├── admin/          # Admin panel (users, system config, SSO, audit)\n│   │   ├── projects/       # Project management & changelog editor\n│   │   └── settings/       # User settings\n│   ├── changelog/          # Public changelog pages + RSS feeds\n│   └── .well-known/        # ACME challenge routes\n├── components/             # React UI components by feature\n├── lib/                    # Business logic, utils, auth, services\n├── hooks/                  # 13 custom React hooks\n├── prisma/schema/          # Modular Prisma schema (6 files)\n├── emails/                 # Email template components\n├── context/                # React context providers (auth, setup)\n├── widgets/                # Embeddable changelog widget source\n├── scripts/                # Build & maintenance scripts\n└── public/                 # Static assets\n```\n\n---\n\n## 🚀 Entry Points\n\n| Path | Purpose |\n|------|---------|\n| `app/layout.tsx` | Root layout with providers |\n| `app/page.tsx` | Root redirect |\n| `app/(auth)/setup/page.tsx` | First-run setup wizard |\n| `lib/api/middleware.ts` | API request middleware (auth, permissions) |\n| `next.config.ts` | Next.js config (React Compiler, Turbopack) |\n| `docker-entrypoint.sh` | Container startup |\n\n---\n\n## 🔐 Authentication System\n\n- **Token**: JWT in `accessToken` cookie — verified via `verifyAccessToken` from `lib/auth/tokens`\n- **Methods**: Password, OAuth, SAML/SSO, Passkey/WebAuthn\n- **Roles**: `ADMIN`, `STAFF`, `VIEWER`\n- **2FA Modes**: `NONE`, `PASSKEY_PLUS_PASSWORD`, `PASSWORD_PLUS_PASSKEY`\n- **CLI Auth**: Token-based code flow at `/api/auth/cli/*`\n\nKey auth files:\n- `lib/auth/tokens.ts` — JWT generation/verification\n- `lib/auth/oauth.ts` — OAuth 2.0 provider integration\n- `lib/auth/saml.ts` — SAML implementation\n- `lib/auth/webauthn.ts` — Passkey/WebAuthn\n- `lib/api/permissions.ts` — Permission checking\n- `lib/api/route-permissions.ts` — Route-level permission config\n\n---\n\n## 📦 Core Modules\n\n### API Layer (`app/api/`)\nRoute groups:\n- `auth/` — Login, register, OAuth, SAML, passkeys, CLI, password reset\n- `admin/` — Config, users, AI settings, SSO providers, API keys, audit logs\n- `projects/[projectId]/` — CRUD, changelog entries, integrations, analytics\n- `changelog/` — Public access, subscriptions, RSS\n- `custom-domains/` — Domain verify, SSL management, browser rules\n- `acme/` — Let's Encrypt certificate issuance/renewal\n- `integrations/slack/` — Slack OAuth callback\n- `config/timezone` — Public effective timezone (no auth)\n- `health` — Health check\n\n### Services Layer (`lib/services/`)\nBusiness logic separated from API handlers:\n- `analytics/` — Analytics data processing\n- `changelog/` — Entry CRUD, publishing, scheduling\n- `email/` — SMTP sending, newsletter management\n- `github/` — Commit sync, tag creation\n- `slack/` — Slack bot notifications\n- `jobs/` — Background job execution (ScheduledJob queue)\n- `projects/` — Project operations\n- `sponsor/` — License/sponsor management\n- `telemetry/` — Telemetry tracking\n- `search/` — Full-text PostgreSQL search\n- `core/markdown/` — Markdown parsing & custom extensions\n\n### Auth (`lib/auth/`)\nSee Authentication System above.\n\n### Utils (`lib/utils/`)\n- `format-date.ts` — Timezone-aware date formatting\n- `cookies.ts` — Cookie helpers\n- `encryption.ts` — Encryption utilities\n- `auditLog.ts` — Audit log helpers\n- `api.ts` — API utilities\n\n### Custom Domains (`lib/custom-domains/`)\n- `service.ts` — Domain management\n- `ssl/` — ACME/Let's Encrypt logic\n- `dns.ts` — DNS verification utilities\n\n---\n\n## 🗄️ Database (Prisma + PostgreSQL)\n\nSchema split across `prisma/schema/`:\n- `base.prisma` — Datasource & generator\n- `users.prisma` — User, OAuth, SAML, Passkey, 2FA, Invite, PasswordReset\n- `projects.prisma` — Project, Changelog, ChangelogEntry, ChangelogTag, Widget\n- `system.prisma` — SystemConfig, ApiKey, AuditLog, ScheduledJob, Analytics, CustomDomain\n- `integrations.prisma` — EmailConfig, SlackIntegration, GitHubIntegration, Subscribers\n- `enums.prisma` — All enum definitions\n\nKey models:\n- `User` — Core user with role, timezone\n- `SystemConfig` — Global app config (timezone, email, AI, Slack OAuth, customDateTemplates as JSONB)\n- `Project` / `Changelog` / `ChangelogEntry` — Main content models\n- `ScheduledJob` — Background job queue (publish, email, SSL renewal, telemetry)\n- `CustomDomain` / `DomainCertificate` — Domain management with SSL\n\n---\n\n## 🧩 Components\n\n```\ncomponents/\n├── changelog/editor/       # Entry editor (AI, versioning, scheduling)\n│   └── VersionSelector.tsx # Version/date template picker\n├── markdown-editor/        # Custom markdown editor with AI\n│   ├── MarkdownEditor.tsx\n│   ├── MarkdownToolbar.tsx\n│   ├── MarkdownPreview.tsx\n│   └── ai/                # AI assistant panel\n├── admin/                  # Admin UI (API keys, audit logs, requests)\n├── analytics/              # Chart components\n├── project/                # Project sidebar, navigation, settings\n│   └── catch-up/          # Feature recap display\n├── sso/                   # SSO configuration UI\n├── setup/                 # First-run setup wizard\n├── settings/              # User security settings\n├── ui/                    # Shadcn/Radix UI primitives\n├── CommandPalette.tsx      # Global command palette\n└── Logo.tsx\n```\n\n---\n\n## 🪝 Hooks (`hooks/`)\n\n| Hook | Purpose |\n|------|---------|\n| `use-timezone.ts` | Resolves effective timezone (user → system → UTC) |\n| `useAIAssistant.ts` | AI writing assistant |\n| `useMarkdownState.ts` | Markdown editor state management |\n| `useEditorHistory.ts` | Undo/redo for editor |\n| `useSlashCommands.ts` | Slash command handling |\n| `useCommandPalette.ts` | Command palette state |\n| `useBookmarks.ts` | Bookmark management |\n| `useChunkedData.ts` | Data chunking for large lists |\n| `useTelemetry.ts` | Telemetry tracking |\n| `useWhatsNew.ts` | What's new modal |\n\n---\n\n## ⚙️ Configuration\n\n| File | Purpose |\n|------|---------|\n| `next.config.ts` | Next.js (React Compiler on, strictMode off, Turbopack) |\n| `tailwind.config.ts` | Tailwind CSS |\n| `components.json` | shadcn/ui component config |\n| `tsconfig.json` | TypeScript (strict, `@/*` alias) |\n| `prisma/schema/` | Database schema |\n| `.env.example` | Required environment variables |\n| `Dockerfile` + `docker-compose.yml` | Container deployment |\n| `Caddyfile` / `nginx.conf` | Reverse proxy configs |\n\n---\n\n## 📚 Documentation\n\n| File | Topic |\n|------|-------|\n| `README.md` | Features, quick start, deployment |\n| `CHANGELOG.md` | Version history |\n| `APIDOCGUIDE.md` | API documentation guide |\n| `ideas.md` | Feature ideas/roadmap |\n| `issues.md` | Known issues |\n| `useful-information-for-development/` | Dev notes (Slack scopes, etc.) |\n\n---\n\n## 🔗 Key Dependencies\n\n| Package | Version | Purpose |\n|---------|---------|---------|\n| next | 16.1.6 | Framework |\n| react | 19.2.4 | UI library |\n| prisma | 6.7.0 | ORM |\n| jose | — | JWT tokens |\n| @node-saml/node-saml | — | SAML/SSO |\n| @simplewebauthn/* | — | Passkeys |\n| @tiptap/react | — | Rich text editing |\n| @tanstack/react-query | — | Server state |\n| @scalar/nextjs-api-reference | — | API docs UI |\n| recharts | — | Analytics charts |\n| framer-motion | — | Animations |\n| zod | — | Schema validation |\n| react-hook-form | — | Form handling |\n| nodemailer | — | Email sending |\n| @slack/bolt | — | Slack integration |\n\n---\n\n## 📝 Quick Start\n\n1. `cp .env.example .env` and fill in required vars\n2. `npm install`\n3. `npx prisma migrate dev` — run DB migrations\n4. `npx prisma generate` — generate Prisma client\n5. `npm run dev` — starts on port 3001\n6. Visit `/setup` on first run\n\n**Build widget**: `npm run build:widget`\n**API docs**: `npm run generate-swagger` → visit `/api-docs`\n\n---\n\n## 🏗️ Architectural Patterns\n\n1. **Auth**: JWT in `accessToken` cookie; `verifyAccessToken` from `lib/auth/tokens`\n2. **Permissions**: Role-based, configured in `lib/api/route-permissions.ts`\n3. **Services**: Business logic in `lib/services/`, not in route handlers\n4. **Timezone**: User.timezone → SystemConfig.timezone → UTC; use `useTimezone()` hook client-side\n5. **Admin layout**: `/^\\/dashboard\\/admin\\/system/` pattern catches sub-pages\n6. **SystemConfig**: Single row, full object sent on PATCH — new fields need defaults\n7. **Background jobs**: `ScheduledJob` model polled by cron endpoints\n8. **Custom domains**: DNS verification + Let's Encrypt via ACME protocol\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <img src=\"public/logo.png\" alt=\"logo\" /><br/>\n  <strong>Ship, Change, Rawr</strong>\n</p>\n\n\n[![Version](https://img.shields.io/badge/version-1.0.5-blue.svg)](https://github.com/supernova3339/changerawr)\n[![Status](https://img.shields.io/badge/status-Production%20Ready-green.svg)](https://github.com/supernova3339/changerawr)\n[![License](https://img.shields.io/badge/license-CNC%20OSL-purple.svg)](LICENSE)\n\n# What is Changerawr?\n\nChangerawr lets you write down what you changed, then share those changes with people. You write entries about updates you made, and Changerawr gives you ways to display them - like widgets for your website, public pages people can visit, or APIs to use however you want. \\\nYou can think of it as a **Changelog Management System** [CMS]\n\nIf you don't know what a changelog is, check out [betterauth](https://www.better-auth.com/changelogs) for an example!\n\n## ✨ Why Changerawr?\n\n**Developer-focused.** Headless API, beautiful documentation, SDKs, integrations, and a CLI.\n\n**Fully customizable.** Do things your way. No vendor lock-in, no forced workflows.\n\n**For everyone.** Whether you're a solo developer, small business, or enterprise team - Changerawr scales with you. ( yes, this means you can use it for commercial usage! just please do reach out if you do, I would love to know how your using Changerawr! )\n\n## 🚀 Features\n\n- **📝 Beautiful Content Editor** - Write changelogs that look professional\n- **🤖 AI-Powered** - Let AI help you write better changelog entries\n- **📡 Headless API** - Beautifully documented REST API for full control\n- **🧩 SDKs** - Pre-built libraries for popular languages\n- **🎨 Embeddable Widget** - Drop a changelog widget anywhere on your site\n- **📧 Email Notifications** - Keep users informed of updates\n- **🏷️ Tags & Versioning** - Organize entries exactly how you want\n- **🔗 Multiple Integrations** - Connect with your existing tools\n- **🔐 Modern Authentication** - Custom-built auth with passkey support\n- **🖥️ Desktop-First Design** - Built for desktop use (mobile works, but it's quirky)\n- **🔍 Full-Text Search** - Search everything, instantly\n- -**🌐 Custom Domains** - Link a custom domain to your changelog\n\n## 🚀 Quick Start\n\n### Prerequisites\n\n- Node.js 22+\n- PostgreSQL database\n\n### Installation\n\n```bash\n# Clone the repository\ngit clone https://github.com/supernova3339/changerawr.git\ncd changerawr\n\n# Install dependencies\nnpm install\n\n# Set up environment\ncp .env.example .env.local\n# Edit .env.local with your settings\n\n# Set up database\nnpx prisma generate\nnpx prisma migrate deploy\n\n# Build the widget\nnpm run build:widget\n\n# Start development server\nnpm run dev\n```\n\nVisit [http://localhost:3000](http://localhost:3000) and you're ready to go!\n\n### Docker Setup\n\n```bash\ndocker-compose up --build\n```\n\n## ⚙️ Configuration\n\n### Environment Variables\n\n```bash\n# Database\nDATABASE_URL=\"postgresql://postgres@localhost:5432/changerawr?schema=public\"\n\n# Authentication\nJWT_ACCESS_SECRET=\"your-jwt-secret-key\"\nNEXT_PUBLIC_APP_URL=\"http://localhost:3000\"\n\n# GitHub Integration (optional)\nGITHUB_ENCRYPTION_KEY=\"your-github-encryption-key\"\n\n# Analytics\nANALYTICS_SALT=\"your-secure-random-salt-here\"\n```\n\n## 📦 Widget Integration\n\nThe easiest way to add changelogs to your site - perfect for non-technical users:\n\n```html\n<!-- Basic widget -->\n<script \n  src=\"https://your-changerawr.com/api/widget/your-project-id\" \n  data-theme=\"light\"\n  async\n></script>\n\n<!-- Popup widget -->\n<button id=\"updates-btn\">What's New?</button>\n<script \n  src=\"https://your-changerawr.com/api/widget/your-project-id\" \n  data-popup=\"true\"\n  data-trigger=\"updates-btn\"\n  async\n></script>\n```\n\n### Widget Options\n\n| Option | Type    |     Default     | Description |\n|--------|---------|:---------------:|-------------|\n| `data-theme` | string  |     \"light\"     | Theme: \"light\" or \"dark\" |\n| `data-position` | string  | \"bottom-right\"  | Popup position |\n| `data-max-height` | string  |     \"400px\"     | Maximum height |\n| `data-popup` | boolean |      false      | Enable popup mode |\n| `data-trigger` | string  |      null       | Button ID or \"immediate\" |\n | `data-max-entries` | number  |        3        | Amount of entries to display, min 3 max 10\n\n## 🛠️ Tech Stack\n\n**Built with modern, reliable technologies:**\n\n- **Next.js 16** - React framework with App Router\n- **Prisma ORM** - Type-safe database access\n- **PostgreSQL** - Robust, scalable database\n- **Shadcn/UI** - Beautiful, accessible UI components\n- **TypeScript** - Full type safety throughout\n\n## 🏗️ Development\n\n### Available Scripts\n\n```bash\nnpm run dev              # Development server\nnpm run build            # Production build\nnpm run start            # Start built development serer\nnpm run start:prod       # Start production server\nnpm run start:prod:win   # Start production server ( Windows )\nnpm run build:widget     # Build embeddable widget\nnpm run generate-swagger # Generate API docs\nnpm run lint             # Code linting ( next 16 will depc this - note )\nnpm run maintenance      # Run the maintenance page\nnpm run start:with-maintenance # Runs maintenance page and the main server\nnpm run prisma:studio # Database viewer and manager \n\n```\n\n### Project Structure\n\n```\nchangerawr/\n├── app/                 # Next.js App Router\n│   ├── (auth)/         # Auth pages\n│   ├── (email)/        # Newsletter related pages\n│   ├── api/            # API endpoints\n│   ├── api-docs/       # API Documentation\n|   ├── changelog/      # Changelog pages (public/custom-domain)\n│   ├── cli/            # Internal pages used to interface with the Changerawr CLI\n│   └── dashboard/      # Main app\n├── components/         # React components\n├── lib/               # Core utilities\n├── prisma/            # Database schema\n├── widgets/           # Widget source\n├── scripts/           # Build scripts\n└── emails/            # Email templates\n```\n\n## 🚢 Deployment\n\n### Docker (Recommended)\n\n```bash\n# Build\ndocker build -t changerawr .\n\n# Run\ndocker run -p 3000:3000 \\\n  -e DATABASE_URL=\"your-database-url\" \\\n  -e JWT_ACCESS_SECRET=\"your-secret\" \\\n  -e NEXT_PUBLIC_APP_URL=\"your-app-url\" \\\n  -e GITHUB_ENCRYPTION_KEY=\"your-encryption-key-32-chars\" \\\n  -e ANALYTICS_SALT=\"your-analytics-salt\" \\\n  changerawr\n```\n\n### Manual Deployment\n\n```bash\nnpm run build\nnpx prisma migrate deploy\nnpm run build:widget\nnpm run generate-swagger\nnpm start:with-maintenance\n```\n\n## 🎯 Features in Detail\n\n### AI-Powered Writing\nLet AI help you craft professional changelog entries that your users will actually want to read.\n\n### Custom Authentication\nBuilt from scratch with modern features like passkeys. No third-party restrictions, full control.\n\n### Developer-First API\nClean, well-documented REST API with SDKs for popular languages. Build exactly what you need.\n\n### Email Notifications\nKeep your users in the loop with beautiful email updates when you ship new features.\n\n### Full Customization\nTags, versioning - organize your changelogs exactly how your team works.\n\n## 🤝 Contributing\n\nWe welcome contributions! Whether it's:\n\n- 🐛 Bug fixes\n- ✨ New features\n- 📖 Documentation improvements\n- 🎨 UI/UX enhancements\n\n1. Fork the repo\n2. Create a feature branch\n3. Make your changes\n4. Submit a pull request\n\n## 📄 License\n\nNon-Commercial Open Source License - see [LICENSE](LICENSE) for details.\n\nChangerawr is open source and free to use, modify, and fork (including closed-source versions). You can use it commercially in your business, but you cannot profit from selling the software itself or charge users for access to it. All features remain free forever.\n\n## 🙋‍♂️ Support\n\n- 🐛 **Issues**: [GitHub Issues](https://github.com/supernova3339/changerawr/issues)\n- 💬 **Discussions**: [GitHub Discussions](https://github.com/supernova3339/changerawr/discussions)\n\n---\n\n**Built by developers, for developers.**\n"
  },
  {
    "path": "app/(auth)/forgot-password/layout.tsx",
    "content": "import React from 'react';\nimport { Metadata } from 'next';\n\nexport const metadata: Metadata = {\n    title: 'Forgot Password - Changerawr',\n    description: 'Reset your password for Changerawr',\n};\n\nexport default function ForgotPasswordLayout({\n                                                 children,\n                                             }: {\n    children: React.ReactNode;\n}) {\n    return (\n        <div className=\"min-h-screen bg-gradient-to-b from-slate-100 to-slate-200 dark:from-slate-900 dark:to-slate-800 flex flex-col items-center justify-center p-4\">\n            <div className=\"w-full max-w-md\">\n                <div className=\"mb-6 text-center\">\n                    <div className=\"inline-block\">\n                        <h1 className=\"text-2xl font-bold\">Changerawr</h1>\n                    </div>\n                </div>\n                {children}\n                <p className=\"text-center text-sm text-muted-foreground mt-8\">\n                    &copy; {new Date().getFullYear()} Changerawr. All rights reserved.\n                </p>\n            </div>\n        </div>\n    );\n}"
  },
  {
    "path": "app/(auth)/forgot-password/page.tsx",
    "content": "'use client';\n\nimport React, { useState, useEffect, useRef } from 'react';\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport { useForm } from 'react-hook-form';\nimport { z } from 'zod';\nimport Link from 'next/link';\nimport {\n    Card,\n    CardContent,\n    CardFooter,\n} from \"@/components/ui/card\";\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport { Alert, AlertDescription } from '@/components/ui/alert';\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { toast } from '@/hooks/use-toast';\nimport {\n    ArrowLeft,\n    Loader2,\n    CheckCircle2,\n    AlarmClock,\n    Mail,\n    RefreshCw,\n    Copy,\n    HelpCircle,\n    MailCheck,\n    ExternalLink\n} from 'lucide-react';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport confetti from 'canvas-confetti';\n\nconst forgotPasswordSchema = z.object({\n    email: z.string().email('Please enter a valid email address'),\n});\n\ntype ForgotPasswordFormValues = z.infer<typeof forgotPasswordSchema>;\n\n// Email providers and their URLs\nconst emailProviders = {\n    'gmail.com': 'https://mail.google.com',\n    'outlook.com': 'https://outlook.live.com',\n    'hotmail.com': 'https://outlook.live.com',\n    'yahoo.com': 'https://mail.yahoo.com',\n    'icloud.com': 'https://www.icloud.com/mail',\n    'protonmail.com': 'https://mail.proton.me',\n    'naver.com': 'https://mail.naver.com/'\n};\n\n// Smart confetti function\nconst fireConfetti = (type: 'success' | 'resend' = 'success') => {\n    const isMobile = window.innerWidth < 768;\n    const defaults = {\n        startVelocity: 30,\n        spread: 360,\n        ticks: 60,\n        zIndex: 0,\n        disableForReducedMotion: true\n    };\n\n    // Check if reduced motion is preferred\n    const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n    if (prefersReducedMotion) {\n        // Only show minimal confetti for users who prefer reduced motion\n        confetti({\n            ...defaults,\n            particleCount: 20,\n            gravity: 1,\n            origin: { y: 0.6, x: 0.5 }\n        });\n        return;\n    }\n\n    if (type === 'success') {\n        // Initial burst from the center\n        confetti({\n            ...defaults,\n            particleCount: isMobile ? 50 : 100,\n            origin: { y: 0.6, x: 0.5 }\n        });\n\n        // Create cannon effect\n        setTimeout(() => {\n            confetti({\n                ...defaults,\n                particleCount: isMobile ? 25 : 50,\n                angle: 60,\n                spread: 50,\n                origin: { x: 0, y: 0.6 }\n            });\n\n            confetti({\n                ...defaults,\n                particleCount: isMobile ? 25 : 50,\n                angle: 120,\n                spread: 50,\n                origin: { x: 1, y: 0.6 }\n            });\n        }, 250);\n\n        // Final smaller bursts\n        setTimeout(() => {\n            confetti({\n                ...defaults,\n                particleCount: isMobile ? 15 : 30,\n                angle: 90,\n                gravity: 1.2,\n                origin: { x: 0.5, y: 0.7 }\n            });\n        }, 400);\n    } else {\n        // Simpler confetti for resend\n        confetti({\n            ...defaults,\n            particleCount: isMobile ? 30 : 50,\n            origin: { y: 0.6, x: 0.5 },\n            gravity: 1.2\n        });\n    }\n};\n\nexport default function ForgotPasswordPage() {\n    const [isSubmitting, setIsSubmitting] = useState(false);\n    const [isEmailSent, setIsEmailSent] = useState(false);\n    const [sentEmail, setSentEmail] = useState('');\n    const [error, setError] = useState('');\n    const [countdown, setCountdown] = useState(0);\n    const [emailProvider, setEmailProvider] = useState<string | null>(null);\n    const [lastTyped, setLastTyped] = useState(0);\n    const [isCopied, setIsCopied] = useState(false);\n    const wrapperRef = useRef<HTMLDivElement>(null);\n\n    const {\n        register,\n        handleSubmit,\n        watch,\n        formState: { errors, isValid },\n        reset,\n    } = useForm<ForgotPasswordFormValues>({\n        resolver: zodResolver(forgotPasswordSchema),\n        mode: 'onChange',\n    });\n\n    const email = watch('email', '');\n\n    // Handle countdown for resend functionality\n    useEffect(() => {\n        if (countdown > 0) {\n            const timer = setTimeout(() => setCountdown(countdown - 1), 1000);\n            return () => clearTimeout(timer);\n        }\n    }, [countdown]);\n\n    // Detect email provider for quick link\n    useEffect(() => {\n        if (sentEmail) {\n            const domain = sentEmail.split('@')[1]?.toLowerCase();\n            const provider = domain && Object.keys(emailProviders).find(key =>\n                domain === key || domain?.endsWith(`.${key}`)\n            );\n\n            setEmailProvider(provider || null);\n        }\n    }, [sentEmail]);\n\n    // Handle email typing suggestions\n    useEffect(() => {\n        if (email && !isSubmitting && !isEmailSent) {\n            setLastTyped(Date.now());\n        }\n    }, [email, isSubmitting, isEmailSent]);\n\n    // Scroll to top when success view is shown\n    useEffect(() => {\n        if (isEmailSent && wrapperRef.current) {\n            wrapperRef.current.scrollIntoView({ behavior: 'smooth' });\n        }\n    }, [isEmailSent]);\n\n    const onSubmit = async (data: ForgotPasswordFormValues) => {\n        setError('');\n        setIsSubmitting(true);\n\n        try {\n            const response = await fetch('/api/auth/forgot-password', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify(data),\n            });\n\n            if (!response.ok) {\n                const errorData = await response.json();\n                throw new Error(errorData.error || 'Failed to request password reset');\n            }\n\n            // Set 60-second countdown for resend button\n            setCountdown(60);\n            setSentEmail(data.email);\n            setIsEmailSent(true);\n\n            // Smart confetti - delayed to match animation\n            setTimeout(() => fireConfetti('success'), 200);\n\n            toast({\n                title: 'Reset Email Sent',\n                description: 'Check your inbox for the password reset link',\n                variant: 'success',\n            });\n        } catch (error) {\n            setError(error instanceof Error ? error.message : 'Something went wrong');\n            toast({\n                title: 'Error',\n                description: error instanceof Error ? error.message : 'Something went wrong',\n                variant: 'destructive',\n            });\n        } finally {\n            setIsSubmitting(false);\n        }\n    };\n\n    const handleResend = () => {\n\n        // Simpler confetti for resend\n        setTimeout(() => fireConfetti('resend'), 100);\n\n        onSubmit({ email: sentEmail });\n    };\n\n    const handleTryDifferentEmail = () => {\n        setIsEmailSent(false);\n        setError('');\n        reset();\n        setTimeout(() => {\n            document.getElementById('email')?.focus();\n        }, 300);\n    };\n\n    const copyToClipboard = () => {\n        if (sentEmail) {\n            navigator.clipboard.writeText(sentEmail);\n            setIsCopied(true);\n            toast({\n                title: 'Copied',\n                description: 'Email address copied to clipboard',\n            });\n\n            // Reset the copied state after 2 seconds\n            setTimeout(() => setIsCopied(false), 2000);\n        }\n    };\n\n    const getEmailDomainSuggestion = () => {\n        if (!email || email.includes('@') || Date.now() - lastTyped < 1000) return null;\n\n        const commonDomains = ['gmail.com', 'outlook.com', 'yahoo.com', 'icloud.com'];\n        return commonDomains[0]; // Suggest the first common domain\n    };\n\n    const suggestion = getEmailDomainSuggestion();\n\n    return (\n        <div ref={wrapperRef}>\n            <AnimatePresence mode=\"wait\">\n                {isEmailSent ? (\n                    <motion.div\n                        key=\"success\"\n                        initial={{ opacity: 0, scale: 0.8, y: 20 }}\n                        animate={{\n                            opacity: 1,\n                            scale: 1,\n                            y: 0,\n                            transition: {\n                                type: \"spring\",\n                                stiffness: 400,\n                                damping: 30\n                            }\n                        }}\n                        exit={{ opacity: 0, scale: 0.8, y: -20 }}\n                        className=\"w-full max-w-sm mx-auto text-center\"\n                    >\n                        <motion.div\n                            className=\"mb-8\"\n                            initial={{ scale: 0 }}\n                            animate={{\n                                scale: 1,\n                                transition: {\n                                    type: \"spring\",\n                                    stiffness: 300,\n                                    delay: 0.2\n                                }\n                            }}\n                        >\n                            <div className=\"w-24 h-24 bg-gradient-to-br from-green-100 to-green-200 dark:from-green-900/30 dark:to-green-800/30 rounded-full flex items-center justify-center mx-auto shadow-md\">\n                                <motion.div\n                                    animate={{\n                                        rotate: [0, 10, -10, 10, 0],\n                                        scale: [1, 1.1, 1]\n                                    }}\n                                    transition={{\n                                        duration: 0.5,\n                                        delay: 0.3\n                                    }}\n                                >\n                                    <MailCheck className=\"h-12 w-12 text-green-600 dark:text-green-400\" strokeWidth={1.5} />\n                                </motion.div>\n                            </div>\n                        </motion.div>\n\n                        <motion.div\n                            initial={{ opacity: 0, y: 20 }}\n                            animate={{\n                                opacity: 1,\n                                y: 0,\n                                transition: { delay: 0.3 }\n                            }}\n                        >\n                            <h2 className=\"text-2xl font-bold mb-3\">Check your inbox</h2>\n\n                            <p className=\"text-muted-foreground mb-1\">\n                                We&apos;ve sent a password reset link to:\n                            </p>\n                            <div className=\"font-medium text-lg mb-1 break-all flex items-center justify-center gap-2\">\n                                <span>{sentEmail}</span>\n                                <TooltipProvider>\n                                    <Tooltip>\n                                        <TooltipTrigger asChild>\n                                            <Button\n                                                variant=\"ghost\"\n                                                size=\"sm\"\n                                                className=\"h-8 w-8 p-0\"\n                                                onClick={copyToClipboard}\n                                            >\n                                                {isCopied ? (\n                                                    <CheckCircle2 className=\"h-4 w-4 text-green-500\" />\n                                                ) : (\n                                                    <Copy className=\"h-4 w-4\" />\n                                                )}\n                                            </Button>\n                                        </TooltipTrigger>\n                                        <TooltipContent>\n                                            <p>Copy email address</p>\n                                        </TooltipContent>\n                                    </Tooltip>\n                                </TooltipProvider>\n                            </div>\n\n                            {emailProvider && (\n                                <motion.div\n                                    initial={{ opacity: 0, height: 0 }}\n                                    animate={{\n                                        opacity: 1,\n                                        height: 'auto',\n                                        transition: { delay: 0.4 }\n                                    }}\n                                    className=\"mb-8\"\n                                >\n                                    <Button\n                                        variant=\"outline\"\n                                        className=\"mt-2\"\n                                        onClick={() => window.open(emailProviders[emailProvider as keyof typeof emailProviders], '_blank')}\n                                    >\n                                        <ExternalLink className=\"mr-2 h-4 w-4\" />\n                                        Open {emailProvider.charAt(0).toUpperCase() + emailProvider.slice(1)}\n                                    </Button>\n                                </motion.div>\n                            )}\n                        </motion.div>\n\n                        <motion.div\n                            className=\"space-y-3\"\n                            initial={{ opacity: 0 }}\n                            animate={{\n                                opacity: 1,\n                                transition: { delay: 0.5 }\n                            }}\n                        >\n                            <Button\n                                variant=\"outline\"\n                                onClick={handleResend}\n                                disabled={countdown > 0}\n                                className=\"w-full h-11 relative overflow-hidden group\"\n                            >\n                                {countdown > 0 ? (\n                                    <div className=\"flex items-center\">\n                                        <AlarmClock className=\"mr-2 h-4 w-4 animate-pulse\" />\n                                        <span>Resend in {countdown}s</span>\n                                        <div\n                                            className=\"absolute bottom-0 left-0 h-1 bg-primary transition-all duration-1000 ease-linear\"\n                                            style={{ width: `${(countdown / 60) * 100}%` }}\n                                        />\n                                    </div>\n                                ) : (\n                                    <div className=\"flex items-center\">\n                                        <RefreshCw className=\"mr-2 h-4 w-4 group-hover:rotate-180 transition-transform duration-500\" />\n                                        <span>Resend Email</span>\n                                    </div>\n                                )}\n                            </Button>\n\n                            <Button\n                                variant=\"secondary\"\n                                onClick={handleTryDifferentEmail}\n                                className=\"w-full h-11\"\n                            >\n                                <Mail className=\"mr-2 h-4 w-4\" />\n                                Try a different email\n                            </Button>\n\n                            <div className=\"pt-6\">\n                                <Button\n                                    variant=\"ghost\"\n                                    asChild\n                                    className=\"text-sm text-muted-foreground hover:text-foreground\"\n                                >\n                                    <Link href=\"/login\">\n                                        <ArrowLeft className=\"mr-2 h-4 w-4\" />\n                                        Back to Login\n                                    </Link>\n                                </Button>\n                            </div>\n                        </motion.div>\n\n                        <motion.div\n                            initial={{ opacity: 0 }}\n                            animate={{ opacity: 0.8 }}\n                            transition={{ delay: 0.8 }}\n                            className=\"mt-8 text-xs text-muted-foreground\"\n                        >\n                            <p>Didn&apos;t receive the email? Check your spam folder or try another email address.</p>\n                        </motion.div>\n                    </motion.div>\n                ) : (\n                    <motion.div\n                        key=\"form\"\n                        initial={{ opacity: 0, y: 10 }}\n                        animate={{ opacity: 1, y: 0 }}\n                        exit={{ opacity: 0, y: -10 }}\n                        transition={{ duration: 0.2 }}\n                        className=\"w-full\"\n                    >\n                        <Card className=\"w-full shadow-lg border-t-4 border-t-primary overflow-hidden\">\n                            <CardContent className=\"pt-6\">\n                                <div className=\"space-y-4\">\n                                    <motion.div\n                                        className=\"text-center space-y-2\"\n                                        initial={{ y: -10, opacity: 0 }}\n                                        animate={{ y: 0, opacity: 1 }}\n                                        transition={{ duration: 0.3 }}\n                                    >\n                                        <h1 className=\"text-2xl font-bold\">Forgot Password</h1>\n                                        <p className=\"text-sm text-muted-foreground\">\n                                            Enter your email address and we&apos;ll send you a link to reset your password\n                                        </p>\n                                    </motion.div>\n\n                                    <AnimatePresence>\n                                        {error && (\n                                            <motion.div\n                                                initial={{ opacity: 0, height: 0 }}\n                                                animate={{ opacity: 1, height: 'auto' }}\n                                                exit={{ opacity: 0, height: 0 }}\n                                            >\n                                                <Alert variant=\"destructive\">\n                                                    <AlertDescription>{error}</AlertDescription>\n                                                </Alert>\n                                            </motion.div>\n                                        )}\n                                    </AnimatePresence>\n\n                                    <form onSubmit={handleSubmit(onSubmit)} className=\"space-y-4\">\n                                        <motion.div\n                                            className=\"space-y-2\"\n                                            initial={{ x: -10, opacity: 0 }}\n                                            animate={{ x: 0, opacity: 1 }}\n                                            transition={{ duration: 0.3, delay: 0.1 }}\n                                        >\n                                            <div className=\"flex justify-between items-center\">\n                                                <Label htmlFor=\"email\">Email Address</Label>\n                                                <TooltipProvider>\n                                                    <Tooltip>\n                                                        <TooltipTrigger asChild>\n                                                            <Button variant=\"ghost\" size=\"sm\" className=\"h-6 w-6 p-0\">\n                                                                <HelpCircle className=\"h-4 w-4 text-muted-foreground\" />\n                                                            </Button>\n                                                        </TooltipTrigger>\n                                                        <TooltipContent>\n                                                            <p className=\"max-w-xs\">Enter the email address you used to register</p>\n                                                        </TooltipContent>\n                                                    </Tooltip>\n                                                </TooltipProvider>\n                                            </div>\n\n                                            <div className=\"relative group\">\n                                                <Mail className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors duration-200\" />\n                                                <Input\n                                                    id=\"email\"\n                                                    type=\"email\"\n                                                    placeholder=\"your@email.com\"\n                                                    {...register('email')}\n                                                    autoComplete=\"email\"\n                                                    autoFocus\n                                                    className={`h-11 pl-10 pr-20 ${errors.email ? 'border-destructive focus-visible:ring-destructive/20' : 'focus-visible:ring-primary/20'} transition-all duration-200`}\n                                                />\n\n                                                {suggestion && (\n                                                    <div className=\"absolute right-3 top-1/2 transform -translate-y-1/2\">\n                                                        <Button\n                                                            type=\"button\"\n                                                            variant=\"ghost\"\n                                                            size=\"sm\"\n                                                            className=\"h-6 text-xs px-2 text-muted-foreground hover:text-foreground\"\n                                                            onClick={() => {\n                                                                const value = `${email}@${suggestion}`;\n                                                                reset({ email: value });\n                                                            }}\n                                                        >\n                                                            @{suggestion}\n                                                        </Button>\n                                                    </div>\n                                                )}\n                                            </div>\n\n                                            <AnimatePresence>\n                                                {errors.email && (\n                                                    <motion.p\n                                                        className=\"text-sm text-destructive flex items-center gap-1 mt-1\"\n                                                        initial={{ opacity: 0, height: 0, y: -10 }}\n                                                        animate={{ opacity: 1, height: 'auto', y: 0 }}\n                                                        exit={{ opacity: 0, height: 0, y: -10 }}\n                                                    >\n                                                        <span className=\"inline-block\">⚠️</span>\n                                                        {errors.email.message}\n                                                    </motion.p>\n                                                )}\n                                            </AnimatePresence>\n                                        </motion.div>\n\n                                        <motion.div\n                                            initial={{ y: 10, opacity: 0 }}\n                                            animate={{ y: 0, opacity: 1 }}\n                                            transition={{ duration: 0.3, delay: 0.2 }}\n                                        >\n                                            <Button\n                                                type=\"submit\"\n                                                className={`\n                          w-full h-11 relative overflow-hidden\n                          ${isValid ? 'bg-primary hover:bg-primary/90' : 'bg-primary/70'}\n                          transition-all duration-300\n                        `}\n                                                disabled={isSubmitting || !isValid}\n                                            >\n                                                {isSubmitting ? (\n                                                    <>\n                                                        <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                                                        Sending...\n                                                    </>\n                                                ) : (\n                                                    <>\n                                                        <Mail className=\"mr-2 h-4 w-4\" />\n                                                        Send Reset Link\n                                                    </>\n                                                )}\n\n                          {/*                      {isValid && !isSubmitting && (*/}\n                          {/*                          <span className=\"absolute right-0 top-0 h-full w-12 -skew-x-12 overflow-hidden flex justify-center items-center\">*/}\n                          {/*  <motion.div*/}\n                          {/*      className=\"bg-white/20 h-8 w-8 rounded-full\"*/}\n                          {/*      initial={{ x: -100 }}*/}\n                          {/*      animate={{ x: 150 }}*/}\n                          {/*      transition={{*/}\n                          {/*          repeat: Infinity,*/}\n                          {/*          duration: 2,*/}\n                          {/*          ease: \"easeInOut\",*/}\n                          {/*          repeatDelay: 1*/}\n                          {/*      }}*/}\n                          {/*  />*/}\n                          {/*</span>*/}\n                          {/*                      )}*/}\n                                            </Button>\n                                        </motion.div>\n                                    </form>\n                                </div>\n                            </CardContent>\n\n                            <CardFooter className=\"flex justify-center pb-6\">\n                                <motion.div\n                                    initial={{ opacity: 0 }}\n                                    animate={{ opacity: 1 }}\n                                    transition={{ delay: 0.3 }}\n                                >\n                                    <Button\n                                        variant=\"ghost\"\n                                        asChild\n                                        size=\"sm\"\n                                        className=\"text-sm text-muted-foreground hover:text-foreground\"\n                                    >\n                                        <Link href=\"/login\">\n                                            <ArrowLeft className=\"mr-2 h-4 w-4\" />\n                                            Back to Login\n                                        </Link>\n                                    </Button>\n                                </motion.div>\n                            </CardFooter>\n                        </Card>\n                    </motion.div>\n                )}\n            </AnimatePresence>\n        </div>\n    );\n}"
  },
  {
    "path": "app/(auth)/layout.tsx",
    "content": "'use client'\n\nimport { useAuth } from '@/context/auth'\nimport { useRouter, usePathname } from 'next/navigation'\nimport React, { useEffect } from 'react'\n\nexport default function AuthLayout({\n                                       children,\n                                   }: {\n    children: React.ReactNode\n}) {\n    const { user, isLoading } = useAuth()\n    const router = useRouter()\n    const pathname = usePathname()\n\n    useEffect(() => {\n        if (!isLoading && user) {\n            router.replace('/dashboard')\n        }\n    }, [user, isLoading, router, pathname])\n\n    if (isLoading) {\n        return (\n            <div className=\"min-h-screen flex items-center justify-center bg-gray-50/30 dark:bg-background\">\n                <div className=\"animate-pulse\">Loading...</div>\n            </div>\n        )\n    }\n\n    return (\n        <div className=\"min-h-screen bg-gray-50/30\">\n            {children}\n        </div>\n    )\n}"
  },
  {
    "path": "app/(auth)/login/layout.tsx",
    "content": "import React from 'react';\nimport { Metadata } from 'next';\n\nexport const metadata: Metadata = {\n    title: 'Login - Changerawr',\n    description: 'Login to your Changerawr account',\n};\n\nexport default function RegisterLayout({\n                                           children,\n                                       }: {\n    children: React.ReactNode;\n}) {\n    return (\n        <div className=\"min-h-screen bg-gradient-to-b from-slate-100 to-slate-200 dark:from-slate-900 dark:to-slate-800 flex flex-col items-center justify-center p-4\">\n            <div className=\"w-full max-w-md\">\n                <div className=\"mb-6 text-center\">\n                    <div className=\"inline-block\">\n                        <h1 className=\"text-2xl font-bold\">Changerawr</h1>\n                    </div>\n                </div>\n                {children}\n                <p className=\"text-center text-sm text-muted-foreground mt-8\">\n                    &copy; {new Date().getFullYear()} Changerawr. All rights reserved.\n                </p>\n            </div>\n        </div>\n    );\n}"
  },
  {
    "path": "app/(auth)/login/page.tsx",
    "content": "'use client'\n\nimport React, {useEffect, useState} from 'react'\nimport {useForm} from 'react-hook-form'\nimport {zodResolver} from '@hookform/resolvers/zod'\nimport {z} from 'zod'\nimport {useAuth} from '@/context/auth'\nimport Link from \"next/link\"\nimport {useQuery} from '@tanstack/react-query'\nimport {motion, AnimatePresence} from 'framer-motion'\nimport confetti from 'canvas-confetti'\nimport {\n    startAuthentication,\n    browserSupportsWebAuthn,\n} from '@simplewebauthn/browser'\n\n// UI Components\nimport {Input} from '@/components/ui/input'\nimport {Button} from '@/components/ui/button'\nimport {Label} from '@/components/ui/label'\nimport {Alert, AlertTitle, AlertDescription} from '@/components/ui/alert'\nimport {Avatar, AvatarImage, AvatarFallback} from \"@/components/ui/avatar\"\nimport {Card, CardContent, CardFooter} from '@/components/ui/card'\nimport {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from \"@/components/ui/tooltip\"\n\n// Icons\nimport {\n    ArrowLeft,\n    User,\n    Fingerprint,\n    Eye,\n    EyeOff,\n    Loader2,\n    Lock,\n    Mail,\n    CheckCircle2,\n    AlertTriangle,\n    Shield,\n    RefreshCw,\n    ArrowRight,\n    Key\n} from 'lucide-react'\nimport {ProviderLogo} from \"@/components/sso/ProviderLogo\";\n\nconst emailSchema = z.object({\n    email: z.string().email('Please enter a valid email')\n})\n\nconst passwordSchema = z.object({\n    password: z.string().min(1, 'Please enter your password')\n})\n\ntype EmailForm = z.infer<typeof emailSchema>\ntype PasswordForm = z.infer<typeof passwordSchema>\n\ninterface UserPreview {\n    name: string | null\n    email: string\n    avatarUrl: string\n}\n\ninterface OAuthProvider {\n    id: string\n    name: string\n    enabled: boolean\n    isDefault: boolean\n}\n\ninterface PasswordBreachData {\n    breachCount: number\n    resetUrl: string\n}\n\n// Smart confetti function from registration page\nconst fireConfetti = () => {\n    const isMobile = window.innerWidth < 768;\n    const defaults = {\n        startVelocity: 30,\n        spread: 360,\n        ticks: 60,\n        zIndex: 0,\n        disableForReducedMotion: true\n    };\n\n    // Check if reduced motion is preferred\n    const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n    if (prefersReducedMotion) {\n        // Only show minimal confetti for users who prefer reduced motion\n        confetti({\n            ...defaults,\n            particleCount: 20,\n            gravity: 1,\n            origin: {y: 0.6, x: 0.5}\n        });\n        return;\n    }\n\n    // Initial burst from the center\n    confetti({\n        ...defaults,\n        particleCount: isMobile ? 50 : 100,\n        origin: {y: 0.6, x: 0.5}\n    });\n\n    // Create cannon effect\n    setTimeout(() => {\n        confetti({\n            ...defaults,\n            particleCount: isMobile ? 25 : 50,\n            angle: 60,\n            spread: 50,\n            origin: {x: 0, y: 0.6}\n        });\n\n        confetti({\n            ...defaults,\n            particleCount: isMobile ? 25 : 50,\n            angle: 120,\n            spread: 50,\n            origin: {x: 1, y: 0.6}\n        });\n    }, 250);\n\n    // Final smaller bursts\n    setTimeout(() => {\n        confetti({\n            ...defaults,\n            particleCount: isMobile ? 15 : 30,\n            angle: 90,\n            gravity: 1.2,\n            origin: {x: 0.5, y: 0.7}\n        });\n    }, 400);\n};\n\nexport default function LoginPage() {\n    const {user, isLoading: authLoading} = useAuth()\n    const [error, setError] = useState('')\n    const [step, setStep] = useState<'email' | 'password' | 'breach-warning'>('email')\n    const [userPreview, setUserPreview] = useState<UserPreview | null>(null)\n    const [passwordBreach, setPasswordBreach] = useState<PasswordBreachData | null>(null)\n    const [supportsWebAuthn, setSupportsWebAuthn] = useState(false)\n    const [isAuthenticating, setIsAuthenticating] = useState(false)\n    const [showPassword, setShowPassword] = useState(false)\n    const [isSuccess, setIsSuccess] = useState(false)\n    const [redirectTo, setRedirectTo] = useState('/dashboard')\n\n    // Fetch OAuth providers\n    const {data: oauthProviders, isLoading: isLoadingProviders} = useQuery({\n        queryKey: ['oauthProviders'],\n        queryFn: async () => {\n            try {\n                const response = await fetch('/api/auth/oauth/providers')\n                if (!response.ok) return []\n                const data = await response.json()\n                return data.providers\n            } catch (error) {\n                console.error('Failed to fetch OAuth providers:', error)\n                return []\n            }\n        },\n        staleTime: 60000 // 1 minute\n    })\n\n    // Fetch SAML providers\n    const {data: samlProviders, isLoading: isLoadingSAMLProviders} = useQuery({\n        queryKey: ['samlProviders'],\n        queryFn: async () => {\n            try {\n                const response = await fetch('/api/auth/saml/providers')\n                if (!response.ok) return []\n                const data = await response.json()\n                return data.providers\n            } catch (error) {\n                console.error('Failed to fetch SAML providers:', error)\n                return []\n            }\n        },\n        staleTime: 60000\n    })\n\n    const emailForm = useForm<EmailForm>({\n        resolver: zodResolver(emailSchema),\n        defaultValues: {\n            email: ''\n        }\n    })\n\n    const passwordForm = useForm<PasswordForm>({\n        resolver: zodResolver(passwordSchema),\n        defaultValues: {\n            password: ''\n        }\n    })\n\n    useEffect(() => {\n        setSupportsWebAuthn(browserSupportsWebAuthn())\n    }, [])\n\n    useEffect(() => {\n        const searchParams = new URLSearchParams(window.location.search)\n        const redirectParam = searchParams.get('redirectTo') || searchParams.get('from')\n        if (redirectParam) {\n            setRedirectTo(redirectParam)\n        }\n\n        const handleOAuthRedirect = async () => {\n            const oauthComplete = searchParams.get('oauth_complete')\n\n            if (oauthComplete === 'true') {\n                try {\n                    // Show success state and confetti, then redirect\n                    setIsSuccess(true)\n                    setTimeout(() => {\n                        fireConfetti()\n                    }, 300)\n\n                    // Redirect with window.location after a short delay\n                    setTimeout(() => {\n                        window.location.href = redirectTo || '/dashboard'\n                    }, 1500)\n                } catch (err) {\n                    console.error('OAuth redirect error:', err)\n                    setError('Failed to complete login')\n                }\n            }\n        }\n\n        if (user && !authLoading) {\n            // Show success state and confetti, then redirect\n            setIsSuccess(true)\n            setTimeout(() => {\n                fireConfetti()\n            }, 300)\n\n            setTimeout(() => {\n                window.location.href = redirectTo\n            }, 1500)\n        } else {\n            // Check for OAuth redirects only if not already logged in\n            handleOAuthRedirect()\n        }\n\n        // Check for error in URL (typically from OAuth callback)\n        const errorParam = searchParams.get('error')\n        if (errorParam) {\n            setError(decodeURIComponent(errorParam))\n        }\n    }, [user, authLoading, redirectTo])\n\n    const formatBreachCount = (count: number): string => {\n        if (count >= 1000000) {\n            return `${(count / 1000000).toFixed(1)}M`;\n        } else if (count >= 1000) {\n            return `${(count / 1000).toFixed(1)}K`;\n        }\n        return count.toLocaleString();\n    };\n\n    const onEmailSubmit = async (data: EmailForm) => {\n        try {\n            setError('')\n            const response = await fetch('/api/auth/preview', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({email: data.email.toLowerCase()})\n            })\n\n            if (!response.ok) {\n                throw new Error('Authentication failed')\n            }\n\n            const userData = await response.json()\n            setUserPreview(userData)\n            setStep('password')\n        } catch (err: unknown) {\n            setError('Unable to find your account')\n            console.log(err)\n        }\n    }\n\n    const onPasswordSubmit = async (data: PasswordForm) => {\n        try {\n            setError('')\n            if (!userPreview) return\n\n            const response = await fetch('/api/auth/login', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({\n                    email: userPreview.email,\n                    password: data.password,\n                    bypassBreachWarning: false // Initial attempt without bypass\n                }),\n                credentials: 'include'\n            })\n\n            const responseData = await response.json()\n\n            // Handle password breach warning\n            if (response.status === 422 && responseData.error === 'password_breached') {\n                setPasswordBreach({\n                    breachCount: responseData.breachCount,\n                    resetUrl: responseData.resetUrl\n                });\n                setStep('breach-warning')\n                return;\n            }\n\n            // Handle 2FA requirement\n            if (response.status === 403 && responseData.requiresSecondFactor) {\n                sessionStorage.setItem('2faSessionToken', responseData.sessionToken)\n                sessionStorage.setItem('2faType', responseData.secondFactorType)\n                window.location.href = '/two-factor'\n                return\n            }\n\n            if (!response.ok) {\n                throw new Error(responseData.error || 'Authentication failed')\n            }\n\n            // Success state with confetti\n            setIsSuccess(true)\n            setTimeout(() => {\n                fireConfetti()\n            }, 300)\n\n            // Redirect with window.location\n            setTimeout(() => {\n                window.location.href = redirectTo\n            }, 1500)\n        } catch (error) {\n            setError(error instanceof Error ? error.message : 'Authentication failed')\n            passwordForm.reset()\n        }\n    }\n\n    // Handle continuing despite breach warning\n    const handleContinueWithBreachedPassword = async () => {\n        try {\n            setError('')\n            if (!userPreview) return\n\n            const response = await fetch('/api/auth/login', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({\n                    email: userPreview.email,\n                    password: passwordForm.getValues('password'),\n                    bypassBreachWarning: true // Bypass the breach warning\n                }),\n                credentials: 'include'\n            })\n\n            const responseData = await response.json()\n\n            // Handle 2FA requirement\n            if (response.status === 403 && responseData.requiresSecondFactor) {\n                sessionStorage.setItem('2faSessionToken', responseData.sessionToken)\n                sessionStorage.setItem('2faType', responseData.secondFactorType)\n                window.location.href = '/two-factor'\n                return\n            }\n\n            if (!response.ok) {\n                throw new Error(responseData.error || 'Authentication failed')\n            }\n\n            // Success state with confetti\n            setIsSuccess(true)\n            setTimeout(() => {\n                fireConfetti()\n            }, 300)\n\n            // Redirect with window.location\n            setTimeout(() => {\n                window.location.href = redirectTo\n            }, 1500)\n        } catch (error) {\n            setError(error instanceof Error ? error.message : 'Authentication failed')\n            setStep('password') // Go back to password form\n            setPasswordBreach(null)\n        }\n    }\n\n    // Handle password reset\n    const handlePasswordReset = () => {\n        if (passwordBreach?.resetUrl && userPreview?.email) {\n            window.location.href = `${passwordBreach.resetUrl}?email=${encodeURIComponent(userPreview.email)}`;\n        }\n    }\n\n    const handleBack = () => {\n        if (step === 'breach-warning') {\n            setStep('password')\n            setPasswordBreach(null)\n        } else {\n            setStep('email')\n            setError('')\n            passwordForm.reset()\n            emailForm.reset()\n            setUserPreview(null)\n        }\n    }\n\n    const handleOAuthLogin = (provider: OAuthProvider) => {\n        // Create a URL-friendly version of the provider name\n        const providerNameForUrl = provider.name\n            .toLowerCase()\n            .replace(/\\s+/g, '') // Remove all whitespace\n            .replace(/[^a-z0-9]/g, '') // Remove any non-alphanumeric characters\n\n        // Include redirectTo parameter in the OAuth URL\n        window.location.href = `/api/auth/oauth/authorize/${providerNameForUrl}?redirect=${encodeURIComponent(redirectTo)}`\n    }\n\n    const handleSAMLLogin = (provider: {id: string; name: string; isDefault: boolean}) => {\n        const providerNameForUrl = provider.name\n            .toLowerCase()\n            .replace(/\\s+/g, '-')\n            .replace(/[^a-z0-9-]/g, '')\n        window.location.href = `/api/auth/saml/authorize/${providerNameForUrl}?redirect=${encodeURIComponent(redirectTo)}`\n    }\n\n    const handlePasskeyLogin = async () => {\n        try {\n            setError('')\n            setIsAuthenticating(true)\n\n            // Get authentication options\n            const optionsResponse = await fetch('/api/auth/passkeys/authenticate/options', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({\n                    email: userPreview?.email || emailForm.getValues('email') || undefined\n                }),\n            })\n\n            if (!optionsResponse.ok) {\n                throw new Error('Failed to get authentication options')\n            }\n\n            const {options, challenge} = await optionsResponse.json()\n\n            // Start WebAuthn authentication\n            const authenticationResponse = await startAuthentication(options)\n\n            // Verify with server\n            const verifyResponse = await fetch('/api/auth/passkeys/authenticate/verify', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({\n                    response: authenticationResponse,\n                    challenge,\n                }),\n            })\n\n            if (!verifyResponse.ok) {\n                const errorData = await verifyResponse.json()\n                throw new Error(errorData.error || 'Authentication failed')\n            }\n\n            const verifyData = await verifyResponse.json()\n\n            // Check if 2FA is required\n            if (verifyData.requiresSecondFactor) {\n                sessionStorage.setItem('2faSessionToken', verifyData.sessionToken)\n                sessionStorage.setItem('2faType', verifyData.secondFactorType)\n                window.location.href = '/two-factor'\n                return\n            }\n\n            // Success state with confetti\n            setIsSuccess(true)\n            setTimeout(() => {\n                fireConfetti()\n            }, 300)\n\n            // Redirect with window.location\n            setTimeout(() => {\n                window.location.href = redirectTo\n            }, 1500)\n        } catch (err) {\n            console.error('Passkey login error:', err)\n            setError(err instanceof Error ? err.message : 'Failed to authenticate with passkey')\n        } finally {\n            setIsAuthenticating(false)\n        }\n    }\n\n    const togglePasswordVisibility = () => {\n        setShowPassword(!showPassword)\n    }\n\n    if (authLoading) {\n        return (\n            <div className=\"flex flex-col items-center justify-center h-full\">\n                <div className=\"w-14 h-14 bg-muted/30 rounded-full flex items-center justify-center\">\n                    <Loader2 className=\"h-8 w-8 animate-spin text-primary\"/>\n                </div>\n                <p className=\"text-muted-foreground mt-4\">Loading...</p>\n            </div>\n        )\n    }\n\n    return (\n        <div>\n            <AnimatePresence mode=\"wait\">\n                {isSuccess ? (\n                    <motion.div\n                        key=\"success\"\n                        initial={{opacity: 0}}\n                        animate={{opacity: 1}}\n                        className=\"w-full max-w-sm mx-auto text-center\"\n                    >\n                        <div className=\"mb-8\">\n                            <div\n                                className=\"w-24 h-24 bg-gradient-to-br from-green-100 to-green-200 dark:from-green-900/30 dark:to-green-800/30 rounded-full flex items-center justify-center mx-auto shadow-md\">\n                                <CheckCircle2 className=\"h-12 w-12 text-green-600 dark:text-green-400\"\n                                              strokeWidth={1.5}/>\n                            </div>\n                        </div>\n\n                        <div>\n                            <h2 className=\"text-2xl font-bold mb-2\">Login Successful</h2>\n                            <p className=\"text-muted-foreground mb-6\">\n                                You&apos;ve signed in successfully. Redirecting you now...\n                            </p>\n                        </div>\n                    </motion.div>\n                ) : (\n                    <div className=\"w-full max-w-sm mx-auto\">\n                        <Card className=\"w-full shadow-lg border-t-4 border-t-primary\">\n                            <CardContent className=\"pt-6\">\n                                <AnimatePresence mode=\"wait\">\n                                    {step === 'email' ? (\n                                        <motion.div\n                                            key=\"email\"\n                                            initial={{opacity: 0}}\n                                            animate={{opacity: 1}}\n                                            exit={{opacity: 0}}\n                                            className=\"space-y-6\"\n                                        >\n                                            <div className=\"text-center space-y-2\">\n                                                <h1 className=\"text-2xl font-bold\">Sign in to Changerawr</h1>\n                                                <p className=\"text-sm text-muted-foreground\">\n                                                    Enter your email to get started\n                                                </p>\n                                            </div>\n\n                                            {error && (\n                                                <Alert variant=\"destructive\">\n                                                    <AlertDescription>{error}</AlertDescription>\n                                                </Alert>\n                                            )}\n\n                                            <form onSubmit={emailForm.handleSubmit(onEmailSubmit)}\n                                                  className=\"space-y-4\">\n                                                <div className=\"space-y-2\">\n                                                    <Label htmlFor=\"email\">Email address</Label>\n                                                    <div className=\"relative\">\n                                                        <Input\n                                                            id=\"email\"\n                                                            {...emailForm.register('email')}\n                                                            type=\"email\"\n                                                            placeholder=\"you@example.com\"\n                                                            className=\"h-11 pl-10\"\n                                                            autoComplete=\"email\"\n                                                            autoFocus\n                                                            startIcon={<Mail/>}\n                                                        />\n                                                    </div>\n                                                    {emailForm.formState.errors.email && (\n                                                        <p className=\"text-sm text-destructive mt-1\">\n                                                            {emailForm.formState.errors.email.message}\n                                                        </p>\n                                                    )}\n                                                </div>\n\n                                                <Button\n                                                    type=\"submit\"\n                                                    className=\"w-full h-11\"\n                                                    disabled={emailForm.formState.isSubmitting}\n                                                >\n                                                    {emailForm.formState.isSubmitting ? (\n                                                        <span className=\"flex items-center gap-2\">\n                                                            <Loader2 className=\"h-4 w-4 animate-spin\"/>\n                                                            Checking...\n                                                        </span>\n                                                    ) : (\n                                                        'Continue'\n                                                    )}\n                                                </Button>\n                                            </form>\n\n                                            {/* Auth Options */}\n                                            {(supportsWebAuthn || (!isLoadingProviders && oauthProviders && oauthProviders.length > 0) || (!isLoadingSAMLProviders && samlProviders && samlProviders.length > 0)) && (\n                                                <div>\n                                                    <div className=\"relative my-6\">\n                                                        <div className=\"absolute inset-0 flex items-center\">\n                                                            <span className=\"w-full border-t\"/>\n                                                        </div>\n                                                        <div className=\"relative flex justify-center text-xs uppercase\">\n                                                            <span className=\"bg-background px-2 text-muted-foreground\">\n                                                                Or continue with\n                                                            </span>\n                                                        </div>\n                                                    </div>\n\n                                                    <div className=\"flex flex-col gap-3 mt-6\">\n                                                        {/* Passkey Button */}\n                                                        {supportsWebAuthn && (\n                                                            <Button\n                                                                type=\"button\"\n                                                                variant=\"outline\"\n                                                                className=\"w-full h-11\"\n                                                                onClick={handlePasskeyLogin}\n                                                                disabled={isAuthenticating}\n                                                            >\n                                                                {isAuthenticating ? (\n                                                                    <span className=\"flex items-center gap-2\">\n                                                                        <Loader2 className=\"h-4 w-4 animate-spin\"/>\n                                                                        Authenticating...\n                                                                    </span>\n                                                                ) : (\n                                                                    <>\n                                                                        <Fingerprint className=\"mr-2 h-4 w-4\"/>\n                                                                        Sign in with Passkey\n                                                                    </>\n                                                                )}\n                                                            </Button>\n                                                        )}\n\n                                                        {/* OAuth Provider Buttons */}\n                                                        {!isLoadingProviders && oauthProviders && oauthProviders.map((provider: OAuthProvider) => (\n                                                            <Button\n                                                                key={provider.id}\n                                                                variant=\"outline\"\n                                                                type=\"button\"\n                                                                className=\"w-full h-11 relative pl-10\"\n                                                                onClick={() => handleOAuthLogin(provider)}\n                                                            >\n                                                                <span\n                                                                    className=\"absolute left-3 top-1/2 transform -translate-y-1/2\">\n                                                                    <ProviderLogo providerName={provider.name}\n                                                                                  size=\"sm\"/>\n                                                                </span>\n                                                                <span>Continue with {provider.name}</span>\n                                                            </Button>\n                                                        ))}\n\n                                                        {/* SAML Provider Buttons */}\n                                                        {!isLoadingSAMLProviders && samlProviders && samlProviders.map((provider: {id: string; name: string; isDefault: boolean}) => (\n                                                            <Button\n                                                                key={provider.id}\n                                                                variant=\"outline\"\n                                                                type=\"button\"\n                                                                className=\"w-full h-11 relative pl-10\"\n                                                                onClick={() => handleSAMLLogin(provider)}\n                                                            >\n                                                                <span\n                                                                    className=\"absolute left-3 top-1/2 transform -translate-y-1/2\">\n                                                                    <ProviderLogo providerName={provider.name}\n                                                                                  size=\"sm\"/>\n                                                                </span>\n                                                                <span>Continue with {provider.name}</span>\n                                                            </Button>\n                                                        ))}\n                                                    </div>\n                                                </div>\n                                            )}\n                                        </motion.div>\n                                    ) : step === 'password' ? (\n                                        <motion.div\n                                            key=\"password\"\n                                            initial={{opacity: 0}}\n                                            animate={{opacity: 1}}\n                                            exit={{opacity: 0}}\n                                            className=\"space-y-6\"\n                                        >\n                                            <Button\n                                                variant=\"ghost\"\n                                                className=\"p-0 h-auto text-muted-foreground hover:text-foreground mb-2\"\n                                                onClick={handleBack}\n                                            >\n                                                <ArrowLeft size={16} className=\"mr-2\"/>\n                                                Back\n                                            </Button>\n\n                                            <div className=\"flex flex-col items-center space-y-4\">\n                                                <Avatar\n                                                    className=\"h-20 w-20 bg-gradient-to-br from-primary/20 to-primary/5 rounded-lg shadow-sm\">\n                                                    <AvatarImage\n                                                        src={userPreview?.avatarUrl}\n                                                        alt={userPreview?.name || \"User avatar\"}\n                                                    />\n                                                    <AvatarFallback className=\"rounded-lg\">\n                                                        <User className=\"h-10 w-10 text-primary\"/>\n                                                    </AvatarFallback>\n                                                </Avatar>\n                                                <div className=\"space-y-1 text-center\">\n                                                    <h2 className=\"text-xl font-semibold\">\n                                                        Welcome back{userPreview?.name ? `, ${userPreview.name}` : ''}\n                                                    </h2>\n                                                    <p className=\"text-sm text-muted-foreground\">{userPreview?.email}</p>\n                                                </div>\n                                            </div>\n\n                                            {error && (\n                                                <Alert variant=\"destructive\">\n                                                    <AlertDescription>{error}</AlertDescription>\n                                                </Alert>\n                                            )}\n\n                                            <form onSubmit={passwordForm.handleSubmit(onPasswordSubmit)}\n                                                  className=\"space-y-4\">\n                                                <div className=\"space-y-2\">\n                                                    <div className=\"flex items-center justify-between\">\n                                                        <Label htmlFor=\"password\">Password</Label>\n                                                        <Link\n                                                            href=\"/forgot-password\"\n                                                            className=\"text-xs font-medium text-primary hover:underline\"\n                                                        >\n                                                            Forgot password?\n                                                        </Link>\n                                                    </div>\n                                                    <div className=\"relative\">\n                                                        <Lock\n                                                            className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground\"/>\n                                                        <Input\n                                                            id=\"password\"\n                                                            {...passwordForm.register('password')}\n                                                            type={showPassword ? 'text' : 'password'}\n                                                            placeholder=\"••••••••\"\n                                                            className=\"h-11 pl-10 pr-10\"\n                                                            autoComplete=\"current-password\"\n                                                            autoFocus\n                                                            startIcon={<Key/>}\n                                                        />\n                                                        <Button\n                                                            type=\"button\" variant=\"ghost\"\n                                                            size=\"sm\"\n                                                            className=\"absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent\"\n                                                            onClick={togglePasswordVisibility}\n                                                        >\n                                                            {showPassword ? (\n                                                                <EyeOff className=\"h-4 w-4 text-muted-foreground\"/>\n                                                            ) : (\n                                                                <Eye className=\"h-4 w-4 text-muted-foreground\"/>\n                                                            )}\n                                                        </Button>\n                                                    </div>\n                                                    {passwordForm.formState.errors.password && (\n                                                        <p className=\"text-sm text-destructive mt-1\">\n                                                            {passwordForm.formState.errors.password.message}\n                                                        </p>\n                                                    )}\n                                                </div>\n\n                                                <Button\n                                                    type=\"submit\"\n                                                    className=\"w-full h-11\"\n                                                    disabled={passwordForm.formState.isSubmitting}\n                                                >\n                                                    {passwordForm.formState.isSubmitting ? (\n                                                        <span className=\"flex items-center gap-2\">\n                                                            <Loader2 className=\"h-4 w-4 animate-spin\"/>\n                                                            Signing in...\n                                                        </span>\n                                                    ) : (\n                                                        'Sign in'\n                                                    )}\n                                                </Button>\n                                            </form>\n\n                                            {/* Show passkey option in password step too */}\n                                            {supportsWebAuthn && (\n                                                <div>\n                                                    <div className=\"relative my-6\">\n                                                        <div className=\"absolute inset-0 flex items-center\">\n                                                            <span className=\"w-full border-t\"/>\n                                                        </div>\n                                                        <div className=\"relative flex justify-center text-xs uppercase\">\n                                                            <span className=\"bg-background px-2 text-muted-foreground\">\n                                                                Or\n                                                            </span>\n                                                        </div>\n                                                    </div>\n\n                                                    <Button\n                                                        type=\"button\"\n                                                        variant=\"outline\"\n                                                        className=\"w-full h-11\"\n                                                        onClick={handlePasskeyLogin}\n                                                        disabled={isAuthenticating}\n                                                    >\n                                                        {isAuthenticating ? (\n                                                            <span className=\"flex items-center gap-2\">\n                                                                <Loader2 className=\"h-4 w-4 animate-spin\"/>\n                                                                Authenticating...\n                                                            </span>\n                                                        ) : (\n                                                            <>\n                                                                <Fingerprint className=\"mr-2 h-4 w-4\"/>\n                                                                Use Passkey Instead\n                                                            </>\n                                                        )}\n                                                    </Button>\n                                                </div>\n                                            )}\n                                        </motion.div>\n                                    ) : step === 'breach-warning' ? (\n                                        <motion.div\n                                            key=\"breach-warning\"\n                                            initial={{opacity: 0, scale: 0.95}}\n                                            animate={{opacity: 1, scale: 1}}\n                                            exit={{opacity: 0, scale: 0.95}}\n                                            className=\"space-y-6\"\n                                        >\n                                            <Button\n                                                variant=\"ghost\"\n                                                className=\"p-0 h-auto text-muted-foreground hover:text-foreground mb-2\"\n                                                onClick={handleBack}\n                                            >\n                                                <ArrowLeft size={16} className=\"mr-2\"/>\n                                                Back\n                                            </Button>\n\n                                            <div className=\"text-center space-y-4\">\n                                                <div\n                                                    className=\"w-16 h-16 bg-gradient-to-br from-amber-100 to-red-100 dark:from-amber-900/30 dark:to-red-900/30 rounded-full flex items-center justify-center mx-auto\">\n                                                    <AlertTriangle\n                                                        className=\"h-8 w-8 text-amber-600 dark:text-amber-400\"/>\n                                                </div>\n                                                <div>\n                                                    <h2 className=\"text-xl font-bold text-amber-900 dark:text-amber-100\">\n                                                        Password Security Alert\n                                                    </h2>\n                                                    <p className=\"text-sm text-muted-foreground mt-2\">\n                                                        Your password was found in a data breach\n                                                    </p>\n                                                </div>\n                                            </div>\n\n                                            <Alert\n                                                variant=\"warning\"\n                                                icon={<Shield className=\"h-4 w-4\"/>}\n                                                className=\"border-amber-200 dark:border-amber-800\"\n                                            >\n                                                <AlertTitle>Security Notice</AlertTitle>\n                                                <AlertDescription>\n                                                    This password has appeared in{' '}\n                                                    <span className=\"font-semibold text-red-600 dark:text-red-400\">\n                                                        {passwordBreach ? formatBreachCount(passwordBreach.breachCount) : '0'} known data breach{passwordBreach && passwordBreach.breachCount === 1 ? '' : 'es'}\n                                                    </span>\n                                                    . Using it puts your account at risk.\n                                                </AlertDescription>\n                                            </Alert>\n\n                                            <div className=\"space-y-3\">\n                                                <h4 className=\"font-medium text-sm\">What does this mean?</h4>\n                                                <ul className=\"text-sm text-muted-foreground space-y-1 list-disc list-inside\">\n                                                    <li>Your password has been compromised in past security incidents\n                                                    </li>\n                                                    <li>Attackers may have access to this password</li>\n                                                    <li>Your account security could be at risk</li>\n                                                </ul>\n                                            </div>\n\n                                            <div className=\"grid gap-3\">\n                                                <Button\n                                                    onClick={handlePasswordReset}\n                                                    className=\"w-full h-11 bg-green-600 hover:bg-green-700 text-white\"\n                                                    disabled={passwordForm.formState.isSubmitting}\n                                                >\n                                                    <RefreshCw className=\"mr-2 h-4 w-4\"/>\n                                                    Reset Password (Recommended)\n                                                </Button>\n\n                                                <div className=\"relative\">\n                                                    <div className=\"absolute inset-0 flex items-center\">\n                                                        <span className=\"w-full border-t border-muted-foreground/20\"/>\n                                                    </div>\n                                                    <div className=\"relative flex justify-center text-xs uppercase\">\n                                                        <span className=\"bg-background px-2 text-muted-foreground\">\n                                                            Or\n                                                        </span>\n                                                    </div>\n                                                </div>\n\n                                                <Button\n                                                    onClick={handleContinueWithBreachedPassword}\n                                                    variant=\"outline\"\n                                                    className=\"w-full h-11 border-amber-200 text-amber-700 hover:bg-amber-50 dark:border-amber-800 dark:text-amber-300 dark:hover:bg-amber-900/20\"\n                                                    disabled={passwordForm.formState.isSubmitting}\n                                                >\n                                                    <ArrowRight className=\"mr-2 h-4 w-4\"/>\n                                                    Continue Anyway (Not Recommended)\n                                                </Button>\n                                            </div>\n\n                                            <div className=\"text-xs text-muted-foreground text-center space-y-1\">\n                                                <p>\n                                                    Password checking powered by{' '}\n                                                    <a\n                                                        href=\"https://haveibeenpwned.com/Passwords\"\n                                                        target=\"_blank\"\n                                                        rel=\"noopener noreferrer\"\n                                                        className=\"text-primary hover:underline\"\n                                                    >\n                                                        HaveIBeenPwned\n                                                    </a>\n                                                </p>\n                                                <p>Your password is never transmitted - only a secure hash is\n                                                    checked.</p>\n                                            </div>\n                                        </motion.div>\n                                    ) : null}\n                                </AnimatePresence>\n                            </CardContent>\n\n                            <CardFooter className=\"pb-6 flex justify-center\">\n                                {step === 'email' && (\n                                    <div className=\"text-center pt-2\">\n                                        <p className=\"text-sm text-muted-foreground\">\n                                            Don&apos;t have an account?{' '}\n                                            <TooltipProvider>\n                                                <Tooltip>\n                                                    <TooltipTrigger asChild>\n                                                        <Button variant=\"link\" className=\"p-0 h-auto text-primary\"\n                                                                onClick={() => setError(\"Contact your administrator for an invitation to join.\")}>\n                                                            Request access\n                                                        </Button>\n                                                    </TooltipTrigger>\n                                                    <TooltipContent>\n                                                        <p className=\"max-w-xs\">You need an invitation to create an\n                                                            account</p>\n                                                    </TooltipContent>\n                                                </Tooltip>\n                                            </TooltipProvider>\n                                        </p>\n                                    </div>\n                                )}\n                            </CardFooter>\n                        </Card>\n                    </div>\n                )}\n            </AnimatePresence>\n        </div>\n    )\n}"
  },
  {
    "path": "app/(auth)/oauth-callback/layout.tsx",
    "content": "import { cookies, headers } from 'next/headers'\nimport React from \"react\";\n\nexport default async function OAuthCallbackLayout({\n                                                      children,\n                                                  }: {\n    children: React.ReactNode\n}) {\n    const headersList = await headers()\n    const redirectUrl = headersList.get('X-OAuth-Redirect') || '/dashboard'\n\n    // Get user data from auth context\n    const cookieStore = await cookies()\n    const accessToken = cookieStore.get('accessToken')?.value\n\n    // This will be serialized and passed to the client component\n    const oauthData = {\n        redirectTo: redirectUrl,\n        hasToken: !!accessToken\n    }\n\n    return (\n        <div>\n            {/* Hidden div with the OAuth data for the client component */}\n            <script\n                id=\"oauth-data\"\n                type=\"application/json\"\n                dangerouslySetInnerHTML={{\n                    __html: JSON.stringify(oauthData)\n                }}\n            />\n            {children}\n        </div>\n    )\n}"
  },
  {
    "path": "app/(auth)/oauth-callback/page.tsx",
    "content": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport { Loader2 } from 'lucide-react'\n\nexport default function OAuthCallback() {\n    const router = useRouter()\n    const [error, setError] = useState<string | null>(null)\n\n    useEffect(() => {\n        // Extract the redirect URL from the page content\n        const handleOAuthRedirect = () => {\n            try {\n                // Get the JSON content from the pre-rendered response\n                const jsonContent = document.getElementById('oauth-data')?.textContent\n\n                if (jsonContent) {\n                    const data = JSON.parse(jsonContent)\n                    if (data.redirectTo) {\n                        // Navigate to the redirect URL\n                        router.push(data.redirectTo)\n                        return\n                    }\n                }\n\n                // Fallback to dashboard if no redirect found\n                router.push('/dashboard')\n            } catch (err) {\n                console.error('OAuth redirect error:', err)\n                setError('Failed to complete login')\n\n                // Redirect to login with error after a brief delay\n                setTimeout(() => {\n                    router.push(`/login?error=${encodeURIComponent('Failed to complete OAuth login')}`)\n                }, 2000)\n            }\n        }\n\n        handleOAuthRedirect()\n    }, [router])\n\n    return (\n        <div className=\"min-h-screen flex flex-col bg-transparent items-center justify-center\">\n            {error ? (\n                <div className=\"text-center\">\n                    <p className=\"text-destructive\">{error}</p>\n                    <p className=\"text-sm text-muted-foreground mt-2\">Redirecting to login...</p>\n                </div>\n            ) : (\n                <div className=\"text-center\">\n                    <Loader2 className=\"h-8 w-8 animate-spin mx-auto\" />\n                    <p className=\"mt-4\">Completing login...</p>\n                </div>\n            )}\n        </div>\n    )\n}"
  },
  {
    "path": "app/(auth)/register/[token]/layout.tsx",
    "content": "import React from 'react';\nimport { Metadata } from 'next';\n\nexport const metadata: Metadata = {\n    title: 'Register - Changerawr',\n    description: 'Create your Changerawr account',\n};\n\nexport default function RegisterLayout({\n                                           children,\n                                       }: {\n    children: React.ReactNode;\n}) {\n    return (\n        <div className=\"min-h-screen bg-gradient-to-b from-slate-100 to-slate-200 dark:from-slate-900 dark:to-slate-800 flex flex-col items-center justify-center p-4\">\n            <div className=\"w-full max-w-md\">\n                <div className=\"mb-6 text-center\">\n                    <div className=\"inline-block\">\n                        <h1 className=\"text-2xl font-bold\">Changerawr</h1>\n                    </div>\n                </div>\n                {children}\n                <p className=\"text-center text-sm text-muted-foreground mt-8\">\n                    &copy; {new Date().getFullYear()} Changerawr. All rights reserved.\n                </p>\n            </div>\n        </div>\n    );\n}"
  },
  {
    "path": "app/(auth)/register/[token]/page.tsx",
    "content": "'use client'\n\nimport React, {useEffect, useState, use} from 'react'\nimport {useForm} from 'react-hook-form'\nimport {zodResolver} from '@hookform/resolvers/zod'\nimport {z} from 'zod'\nimport {useRouter} from 'next/navigation'\nimport {motion, AnimatePresence} from 'framer-motion'\nimport {Input} from '@/components/ui/input'\nimport {Button} from '@/components/ui/button'\nimport {Label} from '@/components/ui/label'\nimport {AlertCircle, ArrowLeft, CheckCircle2, Eye, EyeOff, Loader2, User, Lock, RefreshCw, Mail, Key} from 'lucide-react'\nimport {Alert, AlertDescription} from '@/components/ui/alert'\nimport {Card, CardContent, CardFooter} from '@/components/ui/card'\nimport {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from \"@/components/ui/tooltip\"\nimport Link from 'next/link'\nimport confetti from 'canvas-confetti'\n\nconst registerSchema = z.object({\n    name: z.string().min(2, 'Name is too short'),\n    password: z.string().min(8, 'Password must be at least 8 characters'),\n    confirmPassword: z.string()\n}).refine((data) => data.password === data.confirmPassword, {\n    message: \"Passwords don't match\",\n    path: [\"confirmPassword\"]\n})\n\ntype RegisterForm = z.infer<typeof registerSchema>\n\ninterface InvitationInfo {\n    email: string\n    role: string\n    expiresAt: string\n}\n\n// Smart confetti function\nconst fireConfetti = () => {\n    const isMobile = window.innerWidth < 768;\n    const defaults = {\n        startVelocity: 30,\n        spread: 360,\n        ticks: 60,\n        zIndex: 0,\n        disableForReducedMotion: true\n    };\n\n    // Check if reduced motion is preferred\n    const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n    if (prefersReducedMotion) {\n        // Only show minimal confetti for users who prefer reduced motion\n        confetti({\n            ...defaults,\n            particleCount: 20,\n            gravity: 1,\n            origin: {y: 0.6, x: 0.5}\n        });\n        return;\n    }\n\n    // Initial burst from the center\n    confetti({\n        ...defaults,\n        particleCount: isMobile ? 50 : 100,\n        origin: {y: 0.6, x: 0.5}\n    });\n\n    // Create cannon effect\n    setTimeout(() => {\n        confetti({\n            ...defaults,\n            particleCount: isMobile ? 25 : 50,\n            angle: 60,\n            spread: 50,\n            origin: {x: 0, y: 0.6}\n        });\n\n        confetti({\n            ...defaults,\n            particleCount: isMobile ? 25 : 50,\n            angle: 120,\n            spread: 50,\n            origin: {x: 1, y: 0.6}\n        });\n    }, 250);\n\n    // Final smaller bursts\n    setTimeout(() => {\n        confetti({\n            ...defaults,\n            particleCount: isMobile ? 15 : 30,\n            angle: 90,\n            gravity: 1.2,\n            origin: {x: 0.5, y: 0.7}\n        });\n    }, 400);\n};\n\nexport default function RegisterPage({params}: { params: Promise<{ token: string }> }) {\n    const {token} = use(params)\n    const [error, setError] = useState('')\n    const [invitation, setInvitation] = useState<InvitationInfo | null>(null)\n    const [isLoading, setIsLoading] = useState(true)\n    const [isSuccess, setIsSuccess] = useState(false)\n    const [showPassword, setShowPassword] = useState(false)\n    const [passwordStrength, setPasswordStrength] = useState(0)\n\n    const router = useRouter()\n\n    const {\n        register,\n        handleSubmit,\n        watch,\n        formState: {errors, isSubmitting, isValid}\n    } = useForm<RegisterForm>({\n        resolver: zodResolver(registerSchema),\n        mode: \"onChange\",\n        defaultValues: {\n            name: '',\n            password: '',\n            confirmPassword: ''\n        }\n    })\n\n    const password = watch('password', '')\n\n    // Calculate password strength\n    useEffect(() => {\n        if (!password) {\n            setPasswordStrength(0);\n            return;\n        }\n\n        let strength = 0;\n\n        // Length check\n        if (password.length >= 8) strength += 1;\n        if (password.length >= 12) strength += 1;\n\n        // Character variety\n        if (/[A-Z]/.test(password)) strength += 1;\n        if (/[a-z]/.test(password)) strength += 1;\n        if (/[0-9]/.test(password)) strength += 1;\n        if (/[^A-Za-z0-9]/.test(password)) strength += 1;\n\n        // Normalize to a scale of 0-3\n        setPasswordStrength(Math.min(3, Math.floor(strength / 2)));\n    }, [password]);\n\n    // Celebration effect\n    useEffect(() => {\n        if (isSuccess) {\n            // Trigger confetti after a short delay\n            setTimeout(() => fireConfetti(), 300);\n        }\n    }, [isSuccess]);\n\n    useEffect(() => {\n        async function validateInvitation() {\n            try {\n                const response = await fetch(`/api/auth/invitation/${token}`)\n                const data = await response.json()\n\n                if (!response.ok) {\n                    console.error('Invitation validation failed:', data)\n                    throw new Error(data.message || 'Invalid invitation')\n                }\n\n                setInvitation(data)\n            } catch (err) {\n                if (err instanceof Error) {\n                    setError(err.message)\n                } else {\n                    setError('This invitation link is invalid or has expired')\n                }\n            } finally {\n                setIsLoading(false)\n            }\n        }\n\n        validateInvitation()\n    }, [token])\n\n    const onSubmit = async (data: RegisterForm) => {\n        try {\n            const response = await fetch('/api/auth/register', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({\n                    token,\n                    name: data.name,\n                    password: data.password\n                })\n            })\n\n            if (!response.ok) {\n                throw new Error('Registration failed')\n            }\n\n            setIsSuccess(true)\n\n            // Redirect to login after 3 seconds\n            setTimeout(() => {\n                router.push('/login')\n            }, 3000)\n        } catch (err: unknown) {\n            setError('Unable to complete registration')\n            console.log(err)\n        }\n    }\n\n    const togglePasswordVisibility = () => {\n        setShowPassword(!showPassword);\n    };\n\n    // Get strength label and color\n    const getStrengthLabel = () => {\n        if (!password) return '';\n        const labels = ['Weak', 'Fair', 'Good', 'Strong'];\n        return labels[passwordStrength];\n    };\n\n    const getStrengthColor = () => {\n        if (!password) return 'bg-muted';\n        const colors = ['bg-destructive', 'bg-orange-500', 'bg-yellow-500', 'bg-green-500'];\n        return colors[passwordStrength];\n    };\n\n    if (isLoading) {\n        return (\n            <div className=\"flex flex-col items-center justify-center h-full\">\n                <div className=\"w-14 h-14 bg-muted/30 rounded-full flex items-center justify-center\">\n                    <Loader2 className=\"h-8 w-8 animate-spin text-primary\"/>\n                </div>\n                <p className=\"text-muted-foreground mt-4\">Validating invitation...</p>\n            </div>\n        )\n    }\n\n\n    return (\n        <div>\n            <AnimatePresence mode=\"wait\">\n                {isSuccess ? (\n                    <motion.div\n                        key=\"success\"\n                        initial={{opacity: 0, scale: 0.8, y: 20}}\n                        animate={{\n                            opacity: 1,\n                            scale: 1,\n                            y: 0,\n                            transition: {\n                                type: \"spring\",\n                                stiffness: 400,\n                                damping: 30\n                            }\n                        }}\n                        exit={{opacity: 0, scale: 0.8, y: -20}}\n                        className=\"w-full max-w-sm mx-auto text-center\"\n                    >\n                        <motion.div\n                            className=\"mb-8\"\n                            initial={{scale: 0}}\n                            animate={{\n                                scale: 1,\n                                transition: {\n                                    type: \"spring\",\n                                    stiffness: 300,\n                                    delay: 0.2\n                                }\n                            }}\n                        >\n                            <div\n                                className=\"w-24 h-24 bg-gradient-to-br from-green-100 to-green-200 dark:from-green-900/30 dark:to-green-800/30 rounded-full flex items-center justify-center mx-auto shadow-md\">\n                                <motion.div\n                                    animate={{\n                                        rotate: [0, 10, -10, 10, 0],\n                                        scale: [1, 1.1, 1]\n                                    }}\n                                    transition={{\n                                        duration: 0.5,\n                                        delay: 0.3\n                                    }}\n                                >\n                                    <CheckCircle2 className=\"h-12 w-12 text-green-600 dark:text-green-400\"\n                                                  strokeWidth={1.5}/>\n                                </motion.div>\n                            </div>\n                        </motion.div>\n\n                        <motion.div\n                            initial={{opacity: 0, y: 20}}\n                            animate={{\n                                opacity: 1,\n                                y: 0,\n                                transition: {delay: 0.3}\n                            }}\n                        >\n                            <h2 className=\"text-2xl font-bold mb-2\">Account Created Successfully</h2>\n\n                            {invitation && (\n                                <p className=\"text-muted-foreground mb-6\">\n                                    Your account for {invitation.email} has been created successfully.\n                                </p>\n                            )}\n                        </motion.div>\n\n                        <motion.div\n                            className=\"space-y-3\"\n                            initial={{opacity: 0}}\n                            animate={{\n                                opacity: 1,\n                                transition: {delay: 0.4}\n                            }}\n                        >\n                            <Button\n                                asChild\n                                className=\"w-full h-11\"\n                            >\n                                <Link href=\"/login\">\n                                    Continue to Login\n                                </Link>\n                            </Button>\n\n                            <div className=\"pt-4\">\n                                <p className=\"text-sm text-muted-foreground\">\n                                    Redirecting to login page in 3 seconds...\n                                </p>\n                            </div>\n                        </motion.div>\n                    </motion.div>\n                ) : !invitation ? (\n                    <motion.div\n                        key=\"error\"\n                        initial={{opacity: 0, y: 10}}\n                        animate={{opacity: 1, y: 0}}\n                        exit={{opacity: 0, y: -10}}\n                        className=\"w-full\"\n                    >\n                        <Card className=\"w-full shadow-lg border-t-4 border-t-destructive\">\n                            <CardContent className=\"pt-6\">\n                                <div className=\"flex flex-col items-center text-center space-y-4\">\n                                    <div\n                                        className=\"w-20 h-20 rounded-full bg-destructive/10 flex items-center justify-center\">\n                                        <AlertCircle className=\"h-10 w-10 text-destructive\"/>\n                                    </div>\n                                    <div className=\"space-y-2\">\n                                        <h2 className=\"text-2xl font-bold\">Invalid Invitation</h2>\n                                        <p className=\"text-muted-foreground\">\n                                            {error || 'This invitation link is invalid or has expired.'}\n                                        </p>\n                                    </div>\n\n                                    <Alert variant=\"destructive\" className=\"mt-4\">\n                                        <AlertDescription>\n                                            Please contact your administrator for a valid invitation link.\n                                        </AlertDescription>\n                                    </Alert>\n                                </div>\n                            </CardContent>\n\n                            <CardFooter className=\"flex justify-center mt-2 pb-6\">\n                                <Button\n                                    variant=\"default\"\n                                    asChild\n                                >\n                                    <Link href=\"/login\">\n                                        Return to Login\n                                    </Link>\n                                </Button>\n                            </CardFooter>\n                        </Card>\n                    </motion.div>\n                ) : (\n                    <motion.div\n                        key=\"form\"\n                        initial={{opacity: 0, y: 10}}\n                        animate={{opacity: 1, y: 0}}\n                        exit={{opacity: 0, y: -10}}\n                        transition={{duration: 0.2}}\n                        className=\"w-full\"\n                    >\n                        <Card className=\"w-full shadow-lg border-t-4 border-t-primary overflow-hidden\">\n                            <CardContent className=\"pt-6\">\n                                <div className=\"space-y-4\">\n                                    <motion.div\n                                        className=\"text-center space-y-2\"\n                                        initial={{y: -10, opacity: 0}}\n                                        animate={{y: 0, opacity: 1}}\n                                        transition={{duration: 0.3}}\n                                    >\n                                        <h1 className=\"text-2xl font-bold\">Complete your registration</h1>\n                                        <p className=\"text-sm text-muted-foreground\">\n                                            Set up your account for {invitation.email}\n                                        </p>\n                                    </motion.div>\n\n                                    <AnimatePresence>\n                                        {error && (\n                                            <motion.div\n                                                initial={{opacity: 0, height: 0}}\n                                                animate={{opacity: 1, height: 'auto'}}\n                                                exit={{opacity: 0, height: 0}}\n                                            >\n                                                <Alert variant=\"destructive\">\n                                                    <AlertDescription>{error}</AlertDescription>\n                                                </Alert>\n                                            </motion.div>\n                                        )}\n                                    </AnimatePresence>\n\n                                    <form onSubmit={handleSubmit(onSubmit)} className=\"space-y-4\">\n                                        <motion.div\n                                            className=\"space-y-2\"\n                                            initial={{x: -10, opacity: 0}}\n                                            animate={{x: 0, opacity: 1}}\n                                            transition={{duration: 0.3, delay: 0.1}}\n                                        >\n                                            <Label htmlFor=\"name\">Full name</Label>\n                                            <div className=\"relative group\">\n                                                <User\n                                                    className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors duration-200\"/>\n                                                <Input\n                                                    id=\"name\"\n                                                    {...register('name')}\n                                                    type=\"text\"\n                                                    placeholder=\"Your name\"\n                                                    className={`h-11 pl-10 ${errors.name ? 'border-destructive focus-visible:ring-destructive/20' : 'focus-visible:ring-primary/20'} transition-all duration-200`}\n                                                    autoFocus\n                                                    startIcon={<User/>}\n                                                />\n                                            </div>\n                                            <AnimatePresence>\n                                                {errors.name && (\n                                                    <motion.p\n                                                        className=\"text-sm text-destructive flex items-center gap-1 mt-1\"\n                                                        initial={{opacity: 0, height: 0, y: -10}}\n                                                        animate={{opacity: 1, height: 'auto', y: 0}}\n                                                        exit={{opacity: 0, height: 0, y: -10}}\n                                                    >\n                                                        <span className=\"inline-block\">⚠️</span>\n                                                        {errors.name.message}\n                                                    </motion.p>\n                                                )}\n                                            </AnimatePresence>\n                                        </motion.div>\n\n                                        <motion.div\n                                            className=\"space-y-2\"\n                                            initial={{x: -10, opacity: 0}}\n                                            animate={{x: 0, opacity: 1}}\n                                            transition={{duration: 0.3, delay: 0.2}}\n                                        >\n                                            <div className=\"flex justify-between items-center\">\n                                                <Label htmlFor=\"password\">Password</Label>\n                                                <TooltipProvider>\n                                                    <Tooltip>\n                                                        <TooltipTrigger asChild>\n                                                            <Button variant=\"ghost\" size=\"sm\" className=\"h-6 w-6 p-0\">\n                                                                <AlertCircle className=\"h-4 w-4 text-muted-foreground\"/>\n                                                            </Button>\n                                                        </TooltipTrigger>\n                                                        <TooltipContent>\n                                                            <p className=\"max-w-xs\">Password should be at least 8\n                                                                characters. Strong passwords include uppercase letters,\n                                                                numbers, and symbols.</p>\n                                                        </TooltipContent>\n                                                    </Tooltip>\n                                                </TooltipProvider>\n                                            </div>\n\n                                            <div className=\"relative group\">\n                                                <Lock\n                                                    className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors duration-200\"/>\n                                                <Input\n                                                    id=\"password\"\n                                                    {...register('password')}\n                                                    type={showPassword ? 'text' : 'password'}\n                                                    placeholder=\"••••••••\"\n                                                    className={`h-11 pl-10 pr-10 ${errors.password ? 'border-destructive focus-visible:ring-destructive/20' : 'focus-visible:ring-primary/20'} transition-all duration-200`}\n                                                    startIcon={<Key/>}\n                                                />\n                                                <Button\n                                                    type=\"button\"\n                                                    variant=\"ghost\"\n                                                    size=\"sm\"\n                                                    className=\"absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent\"\n                                                    onClick={togglePasswordVisibility}\n                                                >\n                                                    {showPassword ? (\n                                                        <EyeOff className=\"h-4 w-4 text-muted-foreground\"/>\n                                                    ) : (\n                                                        <Eye className=\"h-4 w-4 text-muted-foreground\"/>\n                                                    )}\n                                                </Button>\n                                            </div>\n\n                                            {/* Password strength indicator */}\n                                            {password && (\n                                                <div className=\"pt-1\">\n                                                    <div className=\"flex justify-between items-center text-xs mb-1\">\n                                                        <span>Password strength:</span>\n                                                        <span className={\n                                                            passwordStrength === 0 ? \"text-destructive\" :\n                                                                passwordStrength === 1 ? \"text-orange-500\" :\n                                                                    passwordStrength === 2 ? \"text-yellow-500\" :\n                                                                        \"text-green-500\"\n                                                        }>\n                                                            {getStrengthLabel()}\n                                                        </span>\n                                                    </div>\n                                                    <div\n                                                        className=\"h-1.5 w-full bg-muted rounded-full overflow-hidden flex\">\n                                                        <div\n                                                            className={`h-full ${getStrengthColor()} transition-all duration-300 ease-out`}\n                                                            style={{width: `${(passwordStrength + 1) * 25}%`}}\n                                                        ></div>\n                                                    </div>\n                                                </div>\n                                            )}\n\n                                            <AnimatePresence>\n                                                {errors.password && (\n                                                    <motion.p\n                                                        className=\"text-sm text-destructive flex items-center gap-1 mt-1\"\n                                                        initial={{opacity: 0, height: 0, y: -10}}\n                                                        animate={{opacity: 1, height: 'auto', y: 0}}\n                                                        exit={{opacity: 0, height: 0, y: -10}}\n                                                    >\n                                                        <span className=\"inline-block\">⚠️</span>\n                                                        {errors.password.message}\n                                                    </motion.p>\n                                                )}\n                                            </AnimatePresence>\n                                        </motion.div>\n\n                                        <motion.div\n                                            className=\"space-y-2\"\n                                            initial={{x: -10, opacity: 0}}\n                                            animate={{x: 0, opacity: 1}}\n                                            transition={{duration: 0.3, delay: 0.3}}\n                                        >\n                                            <Label htmlFor=\"confirmPassword\">Confirm password</Label>\n                                            <div className=\"relative\">\n                                                <Lock\n                                                    className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground\"/>\n                                                <Input\n                                                    id=\"confirmPassword\"\n                                                    {...register('confirmPassword')}\n                                                    type={showPassword ? 'text' : 'password'}\n                                                    placeholder=\"••••••••\"\n                                                    className={`h-11 pl-10 ${errors.confirmPassword ? 'border-destructive focus-visible:ring-destructive/20' : 'focus-visible:ring-primary/20'} transition-all duration-200`}\n                                                    startIcon={<Key/>}\n                                                />\n                                            </div>\n                                            <AnimatePresence>\n                                                {errors.confirmPassword && (\n                                                    <motion.p\n                                                        className=\"text-sm text-destructive flex items-center gap-1 mt-1\"\n                                                        initial={{opacity: 0, height: 0, y: -10}}\n                                                        animate={{opacity: 1, height: 'auto', y: 0}}\n                                                        exit={{opacity: 0, height: 0, y: -10}}\n                                                    >\n                                                        <span className=\"inline-block\">⚠️</span>\n                                                        {errors.confirmPassword.message}\n                                                    </motion.p>\n                                                )}\n                                            </AnimatePresence>\n                                        </motion.div>\n\n                                        <motion.div\n                                            initial={{y: 10, opacity: 0}}\n                                            animate={{y: 0, opacity: 1}}\n                                            transition={{duration: 0.3, delay: 0.4}}\n                                        >\n                                            <Button\n                                                type=\"submit\"\n                                                className={`\n                                                    w-full h-11 relative overflow-hidden\n                                                    ${isValid ? 'bg-primary hover:bg-primary/90' : 'bg-primary/70'}\n                                                    transition-all duration-300\n                                                `}\n                                                disabled={isSubmitting || !isValid}\n                                            >\n                                                {isSubmitting ? (\n                                                    <span className=\"flex items-center gap-2\">\n                                                        <Loader2 className=\"h-4 w-4 animate-spin\"/>\n                                                        Creating account...\n                                                    </span>\n                                                ) : (\n                                                    <>\n                                                        <RefreshCw className=\"mr-2 h-4 w-4\"/>\n                                                        Create account\n                                                    </>\n                                                )}\n\n                                                {isValid && !isSubmitting && (\n                                                    <span\n                                                        className=\"absolute right-0 top-0 h-full w-12 -skew-x-12 overflow-hidden flex justify-center items-center\">\n                                                        <motion.div\n                                                            className=\"bg-white/20 h-8 w-8 rounded-full\"\n                                                            initial={{x: -100}}\n                                                            animate={{x: 150}}\n                                                            transition={{\n                                                                repeat: Infinity,\n                                                                duration: 2,\n                                                                ease: \"easeInOut\",\n                                                                repeatDelay: 1\n                                                            }}\n                                                        />\n                                                    </span>\n                                                )}\n                                            </Button>\n                                        </motion.div>\n                                    </form>\n                                </div>\n                            </CardContent>\n\n                            <CardFooter className=\"flex justify-center pb-6\">\n                                <motion.div\n                                    initial={{opacity: 0}}\n                                    animate={{opacity: 1}}\n                                    transition={{delay: 0.5}}\n                                >\n                                    <Button\n                                        variant=\"ghost\"\n                                        asChild\n                                        size=\"sm\"\n                                        className=\"text-sm text-muted-foreground hover:text-foreground\"\n                                    >\n                                        <Link href=\"/login\">\n                                            <ArrowLeft className=\"mr-2 h-4 w-4\"/>\n                                            Back to Login\n                                        </Link>\n                                    </Button>\n                                </motion.div>\n                            </CardFooter>\n                        </Card>\n                    </motion.div>\n                )}\n            </AnimatePresence>\n        </div>\n    )\n}"
  },
  {
    "path": "app/(auth)/reset-password/[token]/layout.tsx",
    "content": "import React from 'react';\nimport { Metadata } from 'next';\n\nexport const metadata: Metadata = {\n    title: 'Reset Password - Changerawr',\n    description: 'Create a new password for your Changerawr account',\n};\n\nexport default function ResetPasswordLayout({\n                                                children,\n                                            }: {\n    children: React.ReactNode;\n}) {\n    return (\n        <div className=\"min-h-screen bg-gradient-to-b from-slate-100 to-slate-200 dark:from-slate-900 dark:to-slate-800 flex flex-col items-center justify-center p-4\">\n            <div className=\"w-full max-w-md\">\n                <div className=\"mb-6 text-center\">\n                    <div className=\"inline-block\">\n                        <h1 className=\"text-2xl font-bold\">Changerawr</h1>\n                    </div>\n                </div>\n                {children}\n                <p className=\"text-center text-sm text-muted-foreground mt-8\">\n                    &copy; {new Date().getFullYear()} Changerawr. All rights reserved.\n                </p>\n            </div>\n        </div>\n    );\n}"
  },
  {
    "path": "app/(auth)/reset-password/[token]/page.tsx",
    "content": "import { Suspense } from 'react';\nimport ResetPasswordForm from './reset-password-form';\nimport { LoadingSpinner } from '@/components/loading-spinner';\n\nexport default async function ResetPasswordPage({\n                                                    params,\n                                                }: {\n    params: Promise<{ token: string }>;\n}) {\n    const { token } = await params;\n\n    return (\n        <Suspense fallback={<LoadingSpinner />}>\n            <ResetPasswordForm token={token} />\n        </Suspense>\n    );\n}\n"
  },
  {
    "path": "app/(auth)/reset-password/[token]/reset-password-form.tsx",
    "content": "\n'use client'\n\nimport React, { useState, useEffect } from 'react'\nimport { useForm } from 'react-hook-form'\nimport { zodResolver } from '@hookform/resolvers/zod'\nimport { z } from 'zod'\nimport { useRouter } from 'next/navigation'\nimport { motion, AnimatePresence } from 'framer-motion'\nimport { Input } from '@/components/ui/input'\nimport { Button } from '@/components/ui/button'\nimport { Label } from '@/components/ui/label'\nimport { AlertCircle, ArrowLeft, CheckCircle2, Eye, EyeOff, Loader2, Lock, RefreshCw } from 'lucide-react'\nimport Link from 'next/link'\nimport confetti from 'canvas-confetti'\nimport {\n    Card,\n    CardContent,\n    CardFooter,\n} from '@/components/ui/card'\nimport { useToast } from '@/hooks/use-toast'\nimport { Alert, AlertDescription } from '@/components/ui/alert'\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@/components/ui/tooltip\"\n\n// Password schema with requirements\nconst passwordSchema = z\n    .object({\n        password: z.string().min(8, 'Password must be at least 8 characters'),\n        confirmPassword: z.string(),\n    })\n    .refine((data) => data.password === data.confirmPassword, {\n        message: \"Passwords don't match\",\n        path: ['confirmPassword'],\n    });\n\ntype ResetPasswordForm = z.infer<typeof passwordSchema>;\n\n// Smart confetti function\nconst fireConfetti = () => {\n    const isMobile = window.innerWidth < 768;\n    const defaults = {\n        startVelocity: 30,\n        spread: 360,\n        ticks: 60,\n        zIndex: 0,\n        disableForReducedMotion: true\n    };\n\n    // Check if reduced motion is preferred\n    const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n    if (prefersReducedMotion) {\n        // Only show minimal confetti for users who prefer reduced motion\n        confetti({\n            ...defaults,\n            particleCount: 20,\n            gravity: 1,\n            origin: { y: 0.6, x: 0.5 }\n        });\n        return;\n    }\n\n    // Initial burst from the center\n    confetti({\n        ...defaults,\n        particleCount: isMobile ? 50 : 100,\n        origin: { y: 0.6, x: 0.5 }\n    });\n\n    // Create cannon effect\n    setTimeout(() => {\n        confetti({\n            ...defaults,\n            particleCount: isMobile ? 25 : 50,\n            angle: 60,\n            spread: 50,\n            origin: { x: 0, y: 0.6 }\n        });\n\n        confetti({\n            ...defaults,\n            particleCount: isMobile ? 25 : 50,\n            angle: 120,\n            spread: 50,\n            origin: { x: 1, y: 0.6 }\n        });\n    }, 250);\n\n    // Final smaller bursts\n    setTimeout(() => {\n        confetti({\n            ...defaults,\n            particleCount: isMobile ? 15 : 30,\n            angle: 90,\n            gravity: 1.2,\n            origin: { x: 0.5, y: 0.7 }\n        });\n    }, 400);\n};\n\nexport default function ResetPasswordForm({ token }: { token: string }) {\n    const [isLoading, setIsLoading] = useState(true);\n    const [isSuccess, setIsSuccess] = useState(false);\n    const [isValidToken, setIsValidToken] = useState(false);\n    const [errorMessage, setErrorMessage] = useState('');\n    const [userEmail, setUserEmail] = useState('');\n    const [showPassword, setShowPassword] = useState(false);\n    const [passwordStrength, setPasswordStrength] = useState(0);\n    const wrapperRef = React.useRef<HTMLDivElement>(null);\n\n    const router = useRouter();\n    const { toast } = useToast();\n\n    const {\n        register,\n        handleSubmit,\n        watch,\n        formState: { errors, isSubmitting, isValid },\n    } = useForm<ResetPasswordForm>({\n        resolver: zodResolver(passwordSchema),\n        mode: \"onChange\",\n        defaultValues: {\n            password: '',\n            confirmPassword: '',\n        },\n    });\n\n    const password = watch('password', '');\n\n    // Calculate password strength\n    useEffect(() => {\n        if (!password) {\n            setPasswordStrength(0);\n            return;\n        }\n\n        let strength = 0;\n\n        // Length check\n        if (password.length >= 8) strength += 1;\n        if (password.length >= 12) strength += 1;\n\n        // Character variety\n        if (/[A-Z]/.test(password)) strength += 1;\n        if (/[a-z]/.test(password)) strength += 1;\n        if (/[0-9]/.test(password)) strength += 1;\n        if (/[^A-Za-z0-9]/.test(password)) strength += 1;\n\n        // Normalize to a scale of 0-3\n        setPasswordStrength(Math.min(3, Math.floor(strength / 2)));\n    }, [password]);\n\n    // Scroll to top when success view is shown\n    useEffect(() => {\n        if (isSuccess && wrapperRef.current) {\n            wrapperRef.current.scrollIntoView({ behavior: 'smooth' });\n\n            // Trigger confetti after a short delay\n            setTimeout(() => fireConfetti(), 300);\n        }\n    }, [isSuccess]);\n\n    // Validate token on page load\n    useEffect(() => {\n        const validateToken = async () => {\n            try {\n                const response = await fetch(`/api/auth/reset-password/${token}`);\n                const data = await response.json();\n\n                if (response.ok && data.valid) {\n                    setIsValidToken(true);\n                    if (data.email) {\n                        setUserEmail(data.email);\n                    }\n                } else {\n                    setErrorMessage(data.message || 'Invalid or expired reset token');\n                }\n            } catch {\n                setErrorMessage('Failed to validate reset token');\n            } finally {\n                setIsLoading(false);\n            }\n        };\n\n        validateToken();\n    }, [token]);\n\n    const onSubmit = async (data: ResetPasswordForm) => {\n        try {\n            const response = await fetch(`/api/auth/reset-password/${token}`, {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({\n                    password: data.password,\n                    confirmPassword: data.confirmPassword, // Include confirmPassword\n                }),\n            });\n\n            const responseData = await response.json();\n\n            if (!response.ok) {\n                throw new Error(responseData.error || 'Failed to reset password');\n            }\n\n            setIsSuccess(true);\n\n            toast({\n                title: 'Success',\n                description: 'Your password has been reset successfully',\n                variant: 'success',\n            });\n\n            // Redirect to login after 3 seconds\n            setTimeout(() => {\n                router.push('/login');\n            }, 3000);\n        } catch (error) {\n            toast({\n                title: 'Error',\n                description: error instanceof Error ? error.message : 'An error occurred',\n                variant: 'destructive',\n            });\n        }\n    }\n    const togglePasswordVisibility = () => {\n        setShowPassword(!showPassword);\n    };\n\n    // Get strength label and color\n    const getStrengthLabel = () => {\n        if (!password) return '';\n        const labels = ['Weak', 'Fair', 'Good', 'Strong'];\n        return labels[passwordStrength];\n    };\n\n    const getStrengthColor = () => {\n        if (!password) return 'bg-muted';\n        const colors = ['bg-destructive', 'bg-orange-500', 'bg-yellow-500', 'bg-green-500'];\n        return colors[passwordStrength];\n    };\n\n    // Loading state\n    if (isLoading) {\n        return (\n            <div className=\"flex flex-col items-center justify-center space-y-4\">\n                <div className=\"w-14 h-14 bg-muted/30 rounded-full flex items-center justify-center\">\n                    <Loader2 className=\"h-8 w-8 animate-spin text-primary\" />\n                </div>\n                <p className=\"text-muted-foreground\">Validating reset token...</p>\n            </div>\n        );\n    }\n\n    return (\n        <div ref={wrapperRef}>\n            <AnimatePresence mode=\"wait\">\n                {isSuccess ? (\n                    <motion.div\n                        key=\"success\"\n                        initial={{ opacity: 0, scale: 0.8, y: 20 }}\n                        animate={{\n                            opacity: 1,\n                            scale: 1,\n                            y: 0,\n                            transition: {\n                                type: \"spring\",\n                                stiffness: 400,\n                                damping: 30\n                            }\n                        }}\n                        exit={{ opacity: 0, scale: 0.8, y: -20 }}\n                        className=\"w-full max-w-sm mx-auto text-center\"\n                    >\n                        <motion.div\n                            className=\"mb-8\"\n                            initial={{ scale: 0 }}\n                            animate={{\n                                scale: 1,\n                                transition: {\n                                    type: \"spring\",\n                                    stiffness: 300,\n                                    delay: 0.2\n                                }\n                            }}\n                        >\n                            <div className=\"w-24 h-24 bg-gradient-to-br from-green-100 to-green-200 dark:from-green-900/30 dark:to-green-800/30 rounded-full flex items-center justify-center mx-auto shadow-md\">\n                                <motion.div\n                                    animate={{\n                                        rotate: [0, 10, -10, 10, 0],\n                                        scale: [1, 1.1, 1]\n                                    }}\n                                    transition={{\n                                        duration: 0.5,\n                                        delay: 0.3\n                                    }}\n                                >\n                                    <CheckCircle2 className=\"h-12 w-12 text-green-600 dark:text-green-400\" strokeWidth={1.5} />\n                                </motion.div>\n                            </div>\n                        </motion.div>\n\n                        <motion.div\n                            initial={{ opacity: 0, y: 20 }}\n                            animate={{\n                                opacity: 1,\n                                y: 0,\n                                transition: { delay: 0.3 }\n                            }}\n                        >\n                            <h2 className=\"text-2xl font-bold mb-2\">Password Reset Complete</h2>\n\n                            <p className=\"text-muted-foreground mb-6\">\n                                Your password has been successfully updated.\n                                {userEmail && ` You can now log in to ${userEmail} with your new password.`}\n                            </p>\n                        </motion.div>\n\n                        <motion.div\n                            className=\"space-y-3\"\n                            initial={{ opacity: 0 }}\n                            animate={{\n                                opacity: 1,\n                                transition: { delay: 0.4 }\n                            }}\n                        >\n                            <Button\n                                asChild\n                                className=\"w-full h-11\"\n                            >\n                                <Link href=\"/login\">\n                                    Continue to Login\n                                </Link>\n                            </Button>\n\n                            <div className=\"pt-4\">\n                                <p className=\"text-sm text-muted-foreground\">\n                                    Redirecting to login page in 3 seconds...\n                                </p>\n                            </div>\n                        </motion.div>\n                    </motion.div>\n                ) : isValidToken ? (\n                    <motion.div\n                        key=\"form\"\n                        initial={{ opacity: 0, y: 10 }}\n                        animate={{ opacity: 1, y: 0 }}\n                        exit={{ opacity: 0, y: -10 }}\n                        transition={{ duration: 0.2 }}\n                        className=\"w-full\"\n                    >\n                        <Card className=\"w-full shadow-lg border-t-4 border-t-primary\">\n                            <CardContent className=\"pt-6\">\n                                <div className=\"space-y-4\">\n                                    <motion.div\n                                        className=\"text-center space-y-2\"\n                                        initial={{ y: -10, opacity: 0 }}\n                                        animate={{ y: 0, opacity: 1 }}\n                                        transition={{ duration: 0.3 }}\n                                    >\n                                        <h1 className=\"text-2xl font-bold\">Reset Password</h1>\n                                        <p className=\"text-sm text-muted-foreground\">\n                                            Create a new password for {userEmail || 'your account'}\n                                        </p>\n                                    </motion.div>\n\n                                    <form onSubmit={handleSubmit(onSubmit)} className=\"space-y-4\">\n                                        <motion.div\n                                            className=\"space-y-2\"\n                                            initial={{ x: -10, opacity: 0 }}\n                                            animate={{ x: 0, opacity: 1 }}\n                                            transition={{ duration: 0.3, delay: 0.1 }}\n                                        >\n                                            <div className=\"flex justify-between items-center\">\n                                                <Label htmlFor=\"password\">New Password</Label>\n                                                <TooltipProvider>\n                                                    <Tooltip>\n                                                        <TooltipTrigger asChild>\n                                                            <Button variant=\"ghost\" size=\"sm\" className=\"h-6 w-6 p-0\">\n                                                                <AlertCircle className=\"h-4 w-4 text-muted-foreground\" />\n                                                            </Button>\n                                                        </TooltipTrigger>\n                                                        <TooltipContent>\n                                                            <p className=\"max-w-xs\">Password should be at least 8 characters. Strong passwords include uppercase letters, numbers, and symbols.</p>\n                                                        </TooltipContent>\n                                                    </Tooltip>\n                                                </TooltipProvider>\n                                            </div>\n\n                                            <div className=\"relative group\">\n                                                <Lock className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors duration-200\" />\n                                                <Input\n                                                    id=\"password\"\n                                                    {...register('password')}\n                                                    type={showPassword ? 'text' : 'password'}\n                                                    placeholder=\"••••••••\"\n                                                    className={`h-11 pl-10 pr-10 ${errors.password ? 'border-destructive focus-visible:ring-destructive/20' : 'focus-visible:ring-primary/20'} transition-all duration-200`}\n                                                    autoComplete=\"new-password\"\n                                                    autoFocus\n                                                />\n                                                <Button\n                                                    type=\"button\"\n                                                    variant=\"ghost\"\n                                                    size=\"sm\"\n                                                    className=\"absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent\"\n                                                    onClick={togglePasswordVisibility}\n                                                >\n                                                    {showPassword ? (\n                                                        <EyeOff className=\"h-4 w-4 text-muted-foreground\" />\n                                                    ) : (\n                                                        <Eye className=\"h-4 w-4 text-muted-foreground\" />\n                                                    )}\n                                                </Button>\n                                            </div>\n\n                                            {/* Password strength indicator */}\n                                            {password && (\n                                                <div className=\"pt-1\">\n                                                    <div className=\"flex justify-between items-center text-xs mb-1\">\n                                                        <span>Password strength:</span>\n                                                        <span className={\n                                                            passwordStrength === 0 ? \"text-destructive\" :\n                                                                passwordStrength === 1 ? \"text-orange-500\" :\n                                                                    passwordStrength === 2 ? \"text-yellow-500\" :\n                                                                        \"text-green-500\"\n                                                        }>\n                                                            {getStrengthLabel()}\n                                                        </span>\n                                                    </div>\n                                                    <div className=\"h-1.5 w-full bg-muted rounded-full overflow-hidden flex\">\n                                                        <div\n                                                            className={`h-full ${getStrengthColor()} transition-all duration-300 ease-out`}\n                                                            style={{ width: `${(passwordStrength + 1) * 25}%` }}\n                                                        ></div>\n                                                    </div>\n                                                </div>\n                                            )}\n\n                                            <AnimatePresence>\n                                                {errors.password && (\n                                                    <motion.p\n                                                        className=\"text-sm text-destructive flex items-center gap-1 mt-1\"\n                                                        initial={{ opacity: 0, height: 0, y: -10 }}\n                                                        animate={{ opacity: 1, height: 'auto', y: 0 }}\n                                                        exit={{ opacity: 0, height: 0, y: -10 }}\n                                                    >\n                                                        <span className=\"inline-block\">⚠️</span>\n                                                        {errors.password.message}\n                                                    </motion.p>\n                                                )}\n                                            </AnimatePresence>\n                                        </motion.div>\n\n                                        <motion.div\n                                            className=\"space-y-2\"\n                                            initial={{ x: -10, opacity: 0 }}\n                                            animate={{ x: 0, opacity: 1 }}\n                                            transition={{ duration: 0.3, delay: 0.2 }}\n                                        >\n                                            <Label htmlFor=\"confirmPassword\">Confirm password</Label>\n                                            <div className=\"relative\">\n                                                <Lock className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground\" />\n                                                <Input\n                                                    id=\"confirmPassword\"\n                                                    {...register('confirmPassword')}\n                                                    type={showPassword ? 'text' : 'password'}\n                                                    placeholder=\"••••••••\"\n                                                    className={`h-11 pl-10 ${errors.confirmPassword ? 'border-destructive focus-visible:ring-destructive/20' : 'focus-visible:ring-primary/20'} transition-all duration-200`}\n                                                    autoComplete=\"new-password\"\n                                                />\n                                            </div>\n                                            <AnimatePresence>\n                                                {errors.confirmPassword && (\n                                                    <motion.p\n                                                        className=\"text-sm text-destructive flex items-center gap-1 mt-1\"\n                                                        initial={{ opacity: 0, height: 0, y: -10 }}\n                                                        animate={{ opacity: 1, height: 'auto', y: 0 }}\n                                                        exit={{ opacity: 0, height: 0, y: -10 }}\n                                                    >\n                                                        <span className=\"inline-block\">⚠️</span>\n                                                        {errors.confirmPassword.message}\n                                                    </motion.p>\n                                                )}\n                                            </AnimatePresence>\n                                        </motion.div>\n\n                                        <motion.div\n                                            initial={{ y: 10, opacity: 0 }}\n                                            animate={{ y: 0, opacity: 1 }}\n                                            transition={{ duration: 0.3, delay: 0.3 }}\n                                        >\n                                            <Button\n                                                type=\"submit\"\n                                                className={`\n                                                    w-full h-11 relative overflow-hidden\n                                                    ${isValid ? 'bg-primary hover:bg-primary/90' : 'bg-primary/70'}\n                                                    transition-all duration-300\n                                                `}\n                                                disabled={isSubmitting || !isValid}\n                                            >\n                                                {isSubmitting ? (\n                                                    <span className=\"flex items-center gap-2\">\n                                                        <Loader2 className=\"h-4 w-4 animate-spin\" />\n                                                        Resetting password...\n                                                    </span>\n                                                ) : (\n                                                    <>\n                                                        <RefreshCw className=\"mr-2 h-4 w-4\" />\n                                                        Reset Password\n                                                    </>\n                                                )}\n\n                                                {isValid && !isSubmitting && (\n                                                    <span className=\"absolute right-0 top-0 h-full w-12 -skew-x-12 overflow-hidden flex justify-center items-center\">\n                                                        <motion.div\n                                                            className=\"bg-white/20 h-8 w-8 rounded-full\"\n                                                            initial={{ x: -100 }}\n                                                            animate={{ x: 150 }}\n                                                            transition={{\n                                                                repeat: Infinity,\n                                                                duration: 2,\n                                                                ease: \"easeInOut\",\n                                                                repeatDelay: 1\n                                                            }}\n                                                        />\n                                                    </span>\n                                                )}\n                                            </Button>\n                                        </motion.div>\n                                    </form>\n                                </div>\n                            </CardContent>\n\n                            <CardFooter className=\"flex justify-center pb-6\">\n                                <motion.div\n                                    initial={{ opacity: 0 }}\n                                    animate={{ opacity: 1 }}\n                                    transition={{ delay: 0.4 }}\n                                >\n                                    <Button\n                                        variant=\"ghost\"\n                                        asChild\n                                        size=\"sm\"\n                                        className=\"text-sm text-muted-foreground hover:text-foreground\"\n                                    >\n                                        <Link href=\"/login\">\n                                            <ArrowLeft className=\"mr-2 h-4 w-4\" />\n                                            Back to Login\n                                        </Link>\n                                    </Button>\n                                </motion.div>\n                            </CardFooter>\n                        </Card>\n                    </motion.div>\n                ) : (\n                    <motion.div\n                        key=\"error\"\n                        initial={{ opacity: 0, y: 10 }}\n                        animate={{ opacity: 1, y: 0 }}\n                        exit={{ opacity: 0, y: -10 }}\n                        className=\"w-full\"\n                    >\n                        <Card className=\"w-full shadow-lg border-t-4 border-t-destructive\">\n                            <CardContent className=\"pt-6\">\n                                <div className=\"flex flex-col items-center text-center space-y-4\">\n                                    <div className=\"w-20 h-20 rounded-full bg-destructive/10 flex items-center justify-center\">\n                                        <AlertCircle className=\"h-10 w-10 text-destructive\" />\n                                    </div>\n                                    <div className=\"space-y-2\">\n                                        <h2 className=\"text-2xl font-bold\">Invalid Reset Link</h2>\n                                        <p className=\"text-muted-foreground\">\n                                            {errorMessage || 'This password reset link is invalid or has expired.'}\n                                        </p>\n                                    </div>\n\n                                    <Alert variant=\"destructive\" className=\"mt-4\">\n                                        <AlertDescription>\n                                            Please request a new password reset link.\n                                        </AlertDescription>\n                                    </Alert>\n                                </div>\n                            </CardContent>\n\n                            <CardFooter className=\"flex justify-center mt-2 pb-6\">\n                                <Button\n                                    variant=\"default\"\n                                    asChild\n                                >\n                                    <Link href=\"/forgot-password\">\n                                        Request a new reset link\n                                    </Link>\n                                </Button>\n                            </CardFooter>\n                        </Card>\n                    </motion.div>\n                )}\n            </AnimatePresence>\n        </div>\n    );\n}"
  },
  {
    "path": "app/(auth)/setup/layout.tsx",
    "content": "import React from 'react';\nimport { Metadata } from 'next';\n\nexport const metadata: Metadata = {\n    title: 'Setup - Changerawr',\n    description: 'Initial system setup for Changerawr',\n};\n\nexport default function SetupLayout({\n                                        children,\n                                    }: {\n    children: React.ReactNode;\n}) {\n    return (\n        <div className=\"min-h-screen bg-slate-100 dark:bg-slate-900 flex items-center justify-center p-4\">\n            <div className=\"w-full max-w-lg\">{children}</div>\n        </div>\n    );\n}"
  },
  {
    "path": "app/(auth)/setup/page.tsx",
    "content": "'use client';\n\nimport React, {useState, useEffect} from 'react';\nimport {Loader2, CheckCircle2} from 'lucide-react';\nimport {WelcomeStep} from '@/components/setup/steps/welcome-step';\nimport {AdminStep} from '@/components/setup/steps/admin-step';\nimport {SettingsStep} from '@/components/setup/steps/settings-step';\nimport {OAuthStep} from '@/components/setup/steps/oauth-step';\nimport {TeamStep} from '@/components/setup/steps/team-step'; // New import\nimport {CompletionStep} from '@/components/setup/steps/completion-step';\nimport {Alert, AlertDescription} from '@/components/ui/alert';\nimport {Button} from '@/components/ui/button';\nimport Link from 'next/link';\nimport {SetupProvider, useSetup} from '@/components/setup/setup-context';\nimport {motion, AnimatePresence} from 'framer-motion';\n\nfunction StepIndicator() {\n    const {currentStep} = useSetup();\n    const steps = ['welcome', 'admin', 'settings', 'oauth', 'team', 'complete'];\n    const currentIndex = steps.indexOf(currentStep);\n\n    return (\n        <div className=\"flex justify-center items-center gap-2 mb-6\">\n            {steps.map((step, index) => (\n                <div\n                    key={step}\n                    className={`h-2 rounded-full transition-all ${\n                        index === currentIndex\n                            ? 'bg-primary w-8'\n                            : index < currentIndex\n                                ? 'bg-primary w-2'\n                                : 'bg-muted w-2'\n                    }`}\n                />\n            ))}\n        </div>\n    );\n}\n\nfunction SetupContent() {\n    const {currentStep, goToNextStep, goToPreviousStep, skipCurrentStep} = useSetup();\n\n    return (\n        <div className=\"min-h-screen flex flex-col items-center justify-center p-4 space-y-6\">\n            <StepIndicator/>\n\n            <AnimatePresence mode=\"wait\">\n                <motion.div\n                    key={currentStep}\n                    initial={{opacity: 0, y: 10}}\n                    animate={{opacity: 1, y: 0}}\n                    exit={{opacity: 0, y: -10}}\n                    transition={{duration: 0.3}}\n                    className=\"w-full\"\n                >\n                    {currentStep === 'welcome' && (\n                        <WelcomeStep onNext={goToNextStep}/>\n                    )}\n\n                    {currentStep === 'admin' && (\n                        <AdminStep onNext={goToNextStep} onBack={goToPreviousStep}/>\n                    )}\n\n                    {currentStep === 'settings' && (\n                        <SettingsStep onNext={goToNextStep} onBack={goToPreviousStep}/>\n                    )}\n\n                    {currentStep === 'oauth' && (\n                        <OAuthStep onNext={goToNextStep} onBack={goToPreviousStep}/>\n                    )}\n\n                    {currentStep === 'team' && (\n                        <TeamStep\n                            onNext={goToNextStep}\n                            onBack={goToPreviousStep}\n                            onSkip={skipCurrentStep}\n                        />\n                    )}\n\n                    {currentStep === 'complete' && (\n                        <CompletionStep/>\n                    )}\n                </motion.div>\n            </AnimatePresence>\n        </div>\n    );\n}\n\nexport default function SetupPage() {\n    const [error, setError] = useState<string>('');\n    const [isChecking, setIsChecking] = useState(true);\n    const [canSetup, setCanSetup] = useState(false);\n    const [copied, setCopied] = useState(false);\n\n    const envVariable = 'SETUP_COMPLETE=true';\n\n    const copyToClipboard = async () => {\n        try {\n            await navigator.clipboard.writeText(envVariable);\n            setCopied(true);\n            setTimeout(() => setCopied(false), 2000);\n        } catch (err) {\n            console.error('Failed to copy:', err);\n        }\n    };\n\n    useEffect(() => {\n        const checkSetup = async () => {\n            try {\n                const response = await fetch('/api/setup/status');\n\n                if (!response.ok) {\n                    throw new Error('Failed to check setup status');\n                }\n\n                const data = await response.json();\n\n                setCanSetup(!data.isComplete);\n            } catch (err) {\n                setError(err instanceof Error ? err.message : 'Failed to check setup status');\n            } finally {\n                setIsChecking(false);\n            }\n        };\n\n        checkSetup();\n    }, []);\n\n    if (isChecking) {\n        return (\n            <div className=\"min-h-screen flex items-center justify-center p-4\">\n                <div className=\"flex items-center justify-center gap-2\">\n                    <Loader2 className=\"h-6 w-6 animate-spin text-primary\"/>\n                    <p>Checking setup status...</p>\n                </div>\n            </div>\n        );\n    }\n\n    if (!canSetup) {\n\n        return (\n            <div className=\"min-h-screen flex items-center justify-center p-4\">\n                <div className=\"w-full max-w-md space-y-4\">\n                    <div className=\"text-center space-y-2\">\n                        <CheckCircle2 className=\"h-12 w-12 text-primary mx-auto\"/>\n                        <h1 className=\"text-2xl font-bold\">Setup Already Completed</h1>\n                        <p className=\"text-muted-foreground\">The system has already been configured.</p>\n                    </div>\n\n                    {error && (\n                        <Alert variant=\"destructive\">\n                            <AlertDescription>{error}</AlertDescription>\n                        </Alert>\n                    )}\n\n                    <div className=\"flex items-start space-x-4 p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-900 rounded-lg\">\n                        <div className=\"flex-1\">\n                            <h3 className=\"font-medium text-amber-900 dark:text-amber-100 mb-2\">Next Step</h3>\n                            <p className=\"text-sm text-amber-800 dark:text-amber-200 mb-3\">\n                                Add this to your <code className=\"px-1 py-0.5 bg-amber-100 dark:bg-amber-900 rounded text-xs\">.env</code> file:\n                            </p>\n\n                            <div className=\"flex items-center gap-2\">\n                                <code className=\"flex-1 px-3 py-2 bg-amber-100 dark:bg-amber-900 text-amber-900 dark:text-amber-100 rounded font-mono text-sm\">\n                                    {envVariable}\n                                </code>\n                                <Button\n                                    onClick={copyToClipboard}\n                                    variant=\"outline\"\n                                    size=\"sm\"\n                                    className=\"shrink-0\"\n                                >\n                                    {copied ? 'Copied!' : 'Copy'}\n                                </Button>\n                            </div>\n\n                            <p className=\"text-xs text-amber-700 dark:text-amber-300 mt-2\">\n                                Restart your service after adding this variable.\n                            </p>\n                        </div>\n                    </div>\n\n                    <Button asChild className=\"w-full\">\n                        <Link href=\"/login\">Go to Login</Link>\n                    </Button>\n                </div>\n            </div>\n        );\n    }\n\n    return (\n        <SetupProvider>\n            <SetupContent/>\n        </SetupProvider>\n    );\n}"
  },
  {
    "path": "app/(auth)/two-factor/layout.tsx",
    "content": "import React from 'react';\nimport { Metadata } from 'next';\n\nexport const metadata: Metadata = {\n    title: 'Two-Factor Authentication - Changerawr',\n    description: 'Complete your sign in with two-factor authentication',\n};\n\nexport default function TwoFactorLayout({\n                                            children,\n                                        }: {\n    children: React.ReactNode;\n}) {\n    return children;\n}"
  },
  {
    "path": "app/(auth)/two-factor/page.tsx",
    "content": "'use client'\n\nimport React, { useEffect, useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'\nimport { ArrowLeft, Fingerprint, Lock, Loader2, Shield, CheckCircle2 } from 'lucide-react'\nimport { ErrorAlert } from '@/components/ui/error-alert'\nimport { useForm } from 'react-hook-form'\nimport { zodResolver } from '@hookform/resolvers/zod'\nimport { z } from 'zod'\nimport {\n    startAuthentication,\n    browserSupportsWebAuthn,\n} from '@simplewebauthn/browser'\nimport { motion, AnimatePresence } from 'framer-motion'\nimport { useAuth } from '@/context/auth'\n\nconst passwordSchema = z.object({\n    password: z.string().min(8, 'Password must be at least 8 characters'),\n})\n\ntype PasswordForm = z.infer<typeof passwordSchema>\n\nexport default function TwoFactorPage() {\n    const router = useRouter()\n    const { user, isLoading: authLoading } = useAuth()\n    const [error, setError] = useState('')\n    const [isLoading, setIsLoading] = useState(false)\n    const [secondFactorType, setSecondFactorType] = useState<'password' | 'passkey' | null>(null)\n    const [sessionToken, setSessionToken] = useState<string | null>(null)\n    const [supportsWebAuthn, setSupportsWebAuthn] = useState(false)\n    const [isSuccess, setIsSuccess] = useState(false)\n\n    const passwordForm = useForm<PasswordForm>({\n        resolver: zodResolver(passwordSchema),\n        defaultValues: {\n            password: ''\n        }\n    })\n\n    useEffect(() => {\n        // Get 2FA details from session storage\n        const token = sessionStorage.getItem('2faSessionToken')\n        const type = sessionStorage.getItem('2faType') as 'password' | 'passkey' | null\n\n        if (!token || !type) {\n            router.push('/login')\n            return\n        }\n\n        setSessionToken(token)\n        setSecondFactorType(type)\n        setSupportsWebAuthn(browserSupportsWebAuthn())\n\n        // Clear session storage\n        sessionStorage.removeItem('2faSessionToken')\n        sessionStorage.removeItem('2faType')\n    }, [router])\n\n    // Redirect to dashboard when user is authenticated\n    useEffect(() => {\n        if (user && !authLoading) {\n            router.push('/dashboard')\n        }\n    }, [user, authLoading, router])\n\n    const handlePasswordSubmit = async (data: PasswordForm) => {\n        if (!sessionToken) return\n\n        try {\n            setError('')\n            setIsLoading(true)\n\n            const response = await fetch('/api/auth/login/second-factor', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({\n                    sessionToken,\n                    secondFactorPassword: data.password\n                }),\n                credentials: 'include'\n            })\n\n            if (!response.ok) {\n                const errorData = await response.json()\n                throw new Error(errorData.error || 'Authentication failed')\n            }\n\n            // Success\n            setIsSuccess(true)\n\n            // Force refresh the page to update auth context\n            window.location.href = '/dashboard'\n        } catch (error) {\n            setError(error instanceof Error ? error.message : 'Authentication failed')\n            passwordForm.reset()\n        } finally {\n            setIsLoading(false)\n        }\n    }\n\n    const handlePasskeyVerification = async () => {\n        if (!sessionToken) return\n\n        try {\n            setError('')\n            setIsLoading(true)\n\n            // Get passkey options\n            const optionsResponse = await fetch('/api/auth/passkeys/authenticate/options', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({ sessionToken })\n            })\n\n            if (!optionsResponse.ok) {\n                throw new Error('Failed to get authentication options')\n            }\n\n            const { options, challenge } = await optionsResponse.json()\n\n            // Start WebAuthn authentication\n            const authenticationResponse = await startAuthentication(options)\n\n            // Verify with server\n            const verifyResponse = await fetch('/api/auth/login/second-factor', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({\n                    sessionToken,\n                    passkeyResponse: authenticationResponse,\n                    challenge,\n                    passkeyVerified: true\n                }),\n                credentials: 'include'\n            })\n\n            if (!verifyResponse.ok) {\n                const errorData = await verifyResponse.json()\n                throw new Error(errorData.error || 'Authentication failed')\n            }\n\n            // Success\n            setIsSuccess(true)\n\n            // Force refresh the page to update auth context\n            window.location.href = '/dashboard'\n        } catch (error) {\n            setError(error instanceof Error ? error.message : 'Authentication failed')\n        } finally {\n            setIsLoading(false)\n        }\n    }\n\n    if (!secondFactorType || !sessionToken) {\n        return (\n            <div className=\"min-h-screen flex items-center justify-center bg-background\">\n                <div className=\"flex items-center gap-2\">\n                    <Loader2 className=\"h-4 w-4 animate-spin\" />\n                    <span>Loading...</span>\n                </div>\n            </div>\n        )\n    }\n\n    return (\n        <div className=\"min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-background to-muted/20 p-4\">\n            <motion.div\n                initial={{ opacity: 0, y: 20 }}\n                animate={{ opacity: 1, y: 0 }}\n                transition={{ duration: 0.3 }}\n                className=\"w-full max-w-md\"\n            >\n                <Card className=\"border-2 shadow-lg\">\n                    <CardHeader className=\"space-y-1 text-center\">\n                        <div className=\"mx-auto w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mb-2\">\n                            <Shield className=\"h-6 w-6 text-primary\" />\n                        </div>\n                        <CardTitle className=\"text-2xl\">Two-Factor Authentication</CardTitle>\n                        <CardDescription>\n                            {secondFactorType === 'password'\n                                ? 'Enter your password to complete sign in'\n                                : 'Verify with your passkey to complete sign in'\n                            }\n                        </CardDescription>\n                    </CardHeader>\n                    <CardContent className=\"space-y-4\">\n                        <AnimatePresence mode=\"wait\">\n                            {isSuccess ? (\n                                <motion.div\n                                    key=\"success\"\n                                    initial={{ opacity: 0, scale: 0.95 }}\n                                    animate={{ opacity: 1, scale: 1 }}\n                                    exit={{ opacity: 0, scale: 0.95 }}\n                                    className=\"py-8 text-center\"\n                                >\n                                    <div className=\"mx-auto w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mb-4\">\n                                        <CheckCircle2 className=\"h-8 w-8 text-green-600 dark:text-green-400\" />\n                                    </div>\n                                    <h3 className=\"text-lg font-semibold mb-2\">Authentication Successful</h3>\n                                    <p className=\"text-muted-foreground\">Redirecting to dashboard...</p>\n                                </motion.div>\n                            ) : (\n                                <motion.div\n                                    key=\"form\"\n                                    initial={{ opacity: 0 }}\n                                    animate={{ opacity: 1 }}\n                                    exit={{ opacity: 0 }}\n                                    className=\"space-y-4\"\n                                >\n                                    {error && <ErrorAlert message={error} />}\n\n                                    {secondFactorType === 'password' ? (\n                                        <form onSubmit={passwordForm.handleSubmit(handlePasswordSubmit)} className=\"space-y-4\">\n                                            <div className=\"space-y-2\">\n                                                <Label htmlFor=\"password\">Password</Label>\n                                                <div className=\"relative\">\n                                                    <Lock className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground\" />\n                                                    <Input\n                                                        id=\"password\"\n                                                        type=\"password\"\n                                                        {...passwordForm.register('password')}\n                                                        placeholder=\"Enter your password\"\n                                                        autoComplete=\"current-password\"\n                                                        disabled={isLoading}\n                                                        className=\"pl-10\"\n                                                    />\n                                                </div>\n                                                {passwordForm.formState.errors.password && (\n                                                    <p className=\"text-sm text-destructive\">\n                                                        {passwordForm.formState.errors.password.message}\n                                                    </p>\n                                                )}\n                                            </div>\n                                            <Button\n                                                type=\"submit\"\n                                                className=\"w-full\"\n                                                disabled={isLoading}\n                                                size=\"lg\"\n                                            >\n                                                {isLoading ? (\n                                                    <>\n                                                        <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                                                        Verifying...\n                                                    </>\n                                                ) : (\n                                                    <>\n                                                        <Lock className=\"mr-2 h-4 w-4\" />\n                                                        Verify Password\n                                                    </>\n                                                )}\n                                            </Button>\n                                        </form>\n                                    ) : (\n                                        <div className=\"space-y-4\">\n                                            <div className=\"p-4 rounded-lg bg-muted text-center\">\n                                                <Fingerprint className=\"h-8 w-8 mx-auto mb-2 text-primary\" />\n                                                <p className=\"text-sm text-muted-foreground\">\n                                                    Use your security key or biometric authentication to continue\n                                                </p>\n                                            </div>\n                                            <Button\n                                                onClick={handlePasskeyVerification}\n                                                className=\"w-full\"\n                                                disabled={isLoading || !supportsWebAuthn}\n                                                size=\"lg\"\n                                            >\n                                                {isLoading ? (\n                                                    <>\n                                                        <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                                                        Verifying...\n                                                    </>\n                                                ) : (\n                                                    <>\n                                                        <Fingerprint className=\"mr-2 h-4 w-4\" />\n                                                        Verify with Passkey\n                                                    </>\n                                                )}\n                                            </Button>\n                                            {!supportsWebAuthn && (\n                                                <p className=\"text-sm text-center text-destructive\">\n                                                    Your browser doesn&apos;t support passkeys\n                                                </p>\n                                            )}\n                                        </div>\n                                    )}\n\n                                    <div className=\"relative\">\n                                        <div className=\"absolute inset-0 flex items-center\">\n                                            <span className=\"w-full border-t\" />\n                                        </div>\n                                        <div className=\"relative flex justify-center text-xs uppercase\">\n                                            <span className=\"bg-background px-2 text-muted-foreground\">or</span>\n                                        </div>\n                                    </div>\n\n                                    <Button\n                                        variant=\"outline\"\n                                        className=\"w-full\"\n                                        onClick={() => router.push('/login')}\n                                        disabled={isLoading}\n                                    >\n                                        <ArrowLeft className=\"mr-2 h-4 w-4\" />\n                                        Back to Login\n                                    </Button>\n                                </motion.div>\n                            )}\n                        </AnimatePresence>\n                    </CardContent>\n                </Card>\n\n                <p className=\"text-center text-sm text-muted-foreground mt-6\">\n                    This extra step helps keep your account secure\n                </p>\n            </motion.div>\n        </div>\n    )\n}"
  },
  {
    "path": "app/(email)/unsubscribed/page.tsx",
    "content": "'use client';\n\nimport {JSX, Suspense, useEffect, useState} from 'react';\nimport {useSearchParams} from 'next/navigation';\nimport {CheckCircle} from 'lucide-react';\n\nfunction UnsubscribedContent(): JSX.Element {\n    const searchParams = useSearchParams();\n    const [email, setEmail] = useState<string | null>(null);\n\n    useEffect(() => {\n        const emailParam = searchParams.get('email');\n        if (emailParam) {\n            setEmail(emailParam);\n        }\n    }, [searchParams]);\n\n    return (\n        <div className=\"max-w-md w-full\">\n            <div className=\"bg-white rounded-lg shadow-md p-8 text-center\">\n                <div className=\"w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6\">\n                    <CheckCircle className=\"w-8 h-8 text-green-600\"/>\n                </div>\n\n                <h1 className=\"text-2xl font-bold text-gray-900 mb-4\">\n                    Successfully Unsubscribed\n                </h1>\n\n                {email && (\n                    <p className=\"text-gray-600 mb-6\">\n                        <strong>{email}</strong> has been unsubscribed from email notifications.\n                    </p>\n                )}\n\n                <p className=\"text-gray-600 mb-6\">\n                    You will no longer receive email updates from this changelog.\n                </p>\n\n                <div className=\"space-y-3\">\n                    <p className=\"text-sm text-gray-500\">\n                        You can always resubscribe by visiting the changelog page.\n                    </p>\n                </div>\n            </div>\n        </div>\n    );\n}\n\nexport default function UnsubscribedPage(): JSX.Element {\n    return (\n        <div className=\"min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4\">\n            <Suspense fallback={\n                <div className=\"h-96 w-full max-w-md rounded-lg bg-white/30 backdrop-blur-sm animate-pulse\"/>\n            }>\n                <UnsubscribedContent/>\n            </Suspense>\n        </div>\n    );\n}"
  },
  {
    "path": "app/.well-known/acme-challenge/[token]/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server'\nimport {db} from '@/lib/db'\n\nexport const runtime = 'nodejs'\n\n// Catch-all for [token] route - handles dynamic token parameter\nexport async function GET(\n    request: NextRequest,\n    {params}: { params: Promise<{ token: string }> },\n) {\n    const {token} = await params\n    const hostname = request.headers.get('host')?.split(':')[0] || ''\n\n    console.log(`[acme-challenge/[token]] 🔍 DYNAMIC TOKEN ROUTE REQUEST`)\n    console.log(`[acme-challenge/[token]]    URL: ${request.url}`)\n    console.log(`[acme-challenge/[token]]    Hostname: ${hostname}`)\n    console.log(`[acme-challenge/[token]]    Token param: ${token}`)\n    console.log(`[acme-challenge/[token]]    Method: ${request.method}`)\n    console.log(`[acme-challenge/[token]]    Protocol: ${request.headers.get('x-forwarded-proto') || 'unknown'}`)\n    console.log(`[acme-challenge/[token]]    User-Agent: ${request.headers.get('user-agent')}`)\n    console.log(`[acme-challenge/[token]]    All Headers:`, JSON.stringify(Object.fromEntries(request.headers.entries()), null, 2))\n\n    console.log(`[acme-challenge/[token]] ⏳ Searching for challenge in database...`)\n\n    // Wait up to 10 seconds for the challenge to appear in the database\n    const maxAttempts = 20 // 20 attempts * 500ms = 10 seconds\n    let attempt = 0\n\n    while (attempt < maxAttempts) {\n        try {\n            // Log ALL certificates for debugging\n            const allCerts = await db.domainCertificate.findMany({\n                select: {\n                    id: true,\n                    challengeToken: true,\n                    status: true,\n                    domain: {select: {domain: true}},\n                    createdAt: true,\n                },\n                orderBy: {createdAt: 'desc'},\n                take: 10,\n            })\n\n            console.log(`[acme-challenge/[token]] 📊 Attempt ${attempt + 1}/${maxAttempts} - Found ${allCerts.length} total certificates:`)\n            allCerts.forEach(c => {\n                console.log(`[acme-challenge/[token]]    - ${c.domain.domain}: status=${c.status}, token=${c.challengeToken}`)\n            })\n\n            // Search for matching challenge\n            const cert = await db.domainCertificate.findFirst({\n                where: {\n                    challengeToken: token,\n                    domain: {domain: hostname},\n                },\n                include: {\n                    domain: {select: {domain: true}},\n                },\n            })\n\n            if (cert?.challengeKeyAuth) {\n                console.log(`[acme-challenge/[token]] ✅ FOUND challenge for ${cert.domain.domain}`)\n                console.log(`[acme-challenge/[token]]    Status: ${cert.status}`)\n                console.log(`[acme-challenge/[token]]    Token: ${cert.challengeToken}`)\n                console.log(`[acme-challenge/[token]]    KeyAuth: ${cert.challengeKeyAuth.substring(0, 20)}...`)\n                console.log(`[acme-challenge/[token]] 📤 Returning challenge response`)\n\n                return new NextResponse(cert.challengeKeyAuth, {\n                    status: 200,\n                    headers: {\n                        'Content-Type': 'text/plain; charset=utf-8',\n                        'Cache-Control': 'no-store',\n                    },\n                })\n            }\n\n            // Not found yet, wait and retry\n            attempt++\n            if (attempt < maxAttempts) {\n                console.log(`[acme-challenge/[token]] 💤 Challenge not found yet, waiting 500ms...`)\n                await new Promise(resolve => setTimeout(resolve, 500))\n            }\n        } catch (error) {\n            console.error(`[acme-challenge/[token]] ❌ Database error on attempt ${attempt + 1}:`, error)\n            attempt++\n            if (attempt < maxAttempts) {\n                await new Promise(resolve => setTimeout(resolve, 500))\n            }\n        }\n    }\n\n    console.log(`[acme-challenge/[token]] ❌ Challenge NOT FOUND after ${maxAttempts} attempts`)\n    console.log(`[acme-challenge/[token]]    Looking for: hostname=${hostname}, token=${token}`)\n\n    return new NextResponse('Challenge not found', {\n        status: 404,\n        headers: {'Content-Type': 'text/plain'}\n    })\n}\n"
  },
  {
    "path": "app/.well-known/acme-challenge/[token]/route.ts.disabled",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\n\nexport const runtime = 'nodejs'\n\nconst TOKEN_REGEX = /^[a-zA-Z0-9_-]{20,128}$/\n\nexport async function GET(\n    request: NextRequest,\n    { params }: { params: Promise<{ token: string }> },\n) {\n    const { token } = await params\n\n    console.log(`[acme-challenge] 🔍 Incoming challenge request for token: ${token}`)\n\n    // Validate token format to prevent directory traversal or injection\n    if (!TOKEN_REGEX.test(token)) {\n        console.log(`[acme-challenge] ❌ Invalid token format: ${token}`)\n        return new NextResponse('Invalid token format', { status: 400 })\n    }\n\n    // Get the hostname from the request\n    const hostname = request.headers.get('host')?.split(':')[0] || ''\n    console.log(`[acme-challenge] 🌐 Request from hostname: ${hostname}`)\n\n    try {\n        // Debug: Log ALL certificates for this domain, regardless of status\n        const allCertsForDomain = await db.domainCertificate.findMany({\n            where: {\n                domain: { domain: hostname }\n            },\n            select: {\n                id: true,\n                challengeToken: true,\n                status: true,\n                createdAt: true,\n                domain: { select: { domain: true } },\n            },\n            orderBy: { createdAt: 'desc' },\n            take: 5,\n        })\n        console.log(`[acme-challenge] 📊 ALL certificates for ${hostname} (last 5):`)\n        allCertsForDomain.forEach(c => {\n            console.log(`[acme-challenge]    - ${c.status}: token=${c.challengeToken}, created=${c.createdAt.toISOString()}`)\n        })\n\n        // Debug: Log all pending HTTP01 challenges across ALL domains\n        const allPending = await db.domainCertificate.findMany({\n            where: { status: 'PENDING_HTTP01' },\n            select: {\n                id: true,\n                challengeToken: true,\n                domain: { select: { domain: true } },\n            },\n        })\n        console.log(`[acme-challenge] 📋 Found ${allPending.length} PENDING_HTTP01 challenges in database (all domains):`)\n        allPending.forEach(p => {\n            console.log(`[acme-challenge]    - ${p.domain.domain}: token=${p.challengeToken}`)\n        })\n\n        // Find the certificate challenge for this specific domain and token\n        const cert = await db.domainCertificate.findFirst({\n            where: {\n                challengeToken: token,\n                status: 'PENDING_HTTP01',\n                domain: {\n                    domain: hostname,\n                },\n            },\n            include: {\n                domain: {\n                    select: {\n                        domain: true,\n                    },\n                },\n            },\n        })\n\n        if (!cert?.challengeKeyAuth) {\n            console.log(`[acme-challenge] ❌ Challenge NOT FOUND`)\n            console.log(`[acme-challenge]    Looking for: domain=${hostname}, status=PENDING_HTTP01, token=${token}`)\n            return new NextResponse('Challenge not found', { status: 404 })\n        }\n\n        console.log(`[acme-challenge] ✅ Found matching challenge for ${cert.domain.domain}`)\n        console.log(`[acme-challenge] 📤 Serving key authorization: ${cert.challengeKeyAuth.substring(0, 20)}...`)\n\n        // Let's Encrypt expects exactly this response with no extra whitespace\n        return new NextResponse(cert.challengeKeyAuth, {\n            status: 200,\n            headers: {\n                'Content-Type': 'text/plain; charset=utf-8',\n                'Cache-Control': 'no-store',\n            },\n        })\n    } catch (error) {\n        console.error('[acme-challenge] ❌ Database error:', error)\n        return new NextResponse('Internal server error', { status: 500 })\n    }\n}\n"
  },
  {
    "path": "app/.well-known/acme-challenge/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server'\nimport {db} from '@/lib/db'\n\nexport const runtime = 'nodejs'\n\n// TRUE catch-all that handles ALL ACME challenge requests\nexport async function GET(request: NextRequest) {\n    const url = new URL(request.url)\n    const pathname = url.pathname\n    const hostname = request.headers.get('host')?.split(':')[0] || ''\n\n    // Extract token from path: /.well-known/acme-challenge/TOKEN\n    const pathParts = pathname.split('/')\n    const token = pathParts[pathParts.length - 1]\n\n    console.log(`[acme-challenge] 🔍 CATCH-ALL REQUEST`)\n    console.log(`[acme-challenge]    URL: ${request.url}`)\n    console.log(`[acme-challenge]    Pathname: ${pathname}`)\n    console.log(`[acme-challenge]    Hostname: ${hostname}`)\n    console.log(`[acme-challenge]    Token: ${token}`)\n    console.log(`[acme-challenge]    Method: ${request.method}`)\n    console.log(`[acme-challenge]    Protocol: ${request.headers.get('x-forwarded-proto') || 'unknown'}`)\n    console.log(`[acme-challenge]    User-Agent: ${request.headers.get('user-agent')}`)\n    console.log(`[acme-challenge]    All Headers:`, JSON.stringify(Object.fromEntries(request.headers.entries()), null, 2))\n\n    // If no token in path, return early\n    if (!token || token === 'acme-challenge') {\n        console.log(`[acme-challenge] ❌ No token in path`)\n        return new NextResponse('No token provided', {status: 404})\n    }\n\n    console.log(`[acme-challenge] ⏳ Searching for challenge in database...`)\n\n    // Wait up to 10 seconds for the challenge to appear in the database\n    const maxAttempts = 20 // 20 attempts * 500ms = 10 seconds\n    let attempt = 0\n\n    while (attempt < maxAttempts) {\n        try {\n            // Log ALL certificates for debugging\n            const allCerts = await db.domainCertificate.findMany({\n                select: {\n                    id: true,\n                    challengeToken: true,\n                    status: true,\n                    domain: {select: {domain: true}},\n                    createdAt: true,\n                },\n                orderBy: {createdAt: 'desc'},\n                take: 10,\n            })\n\n            console.log(`[acme-challenge] 📊 Attempt ${attempt + 1}/${maxAttempts} - Found ${allCerts.length} total certificates:`)\n            allCerts.forEach(c => {\n                console.log(`[acme-challenge]    - ${c.domain.domain}: status=${c.status}, token=${c.challengeToken}`)\n            })\n\n            // Search for matching challenge\n            const cert = await db.domainCertificate.findFirst({\n                where: {\n                    challengeToken: token,\n                    domain: {domain: hostname},\n                },\n                include: {\n                    domain: {select: {domain: true}},\n                },\n            })\n\n            if (cert?.challengeKeyAuth) {\n                console.log(`[acme-challenge] ✅ FOUND challenge for ${cert.domain.domain}`)\n                console.log(`[acme-challenge]    Status: ${cert.status}`)\n                console.log(`[acme-challenge]    Token: ${cert.challengeToken}`)\n                console.log(`[acme-challenge]    KeyAuth: ${cert.challengeKeyAuth.substring(0, 20)}...`)\n                console.log(`[acme-challenge] 📤 Returning challenge response`)\n\n                return new NextResponse(cert.challengeKeyAuth, {\n                    status: 200,\n                    headers: {\n                        'Content-Type': 'text/plain; charset=utf-8',\n                        'Cache-Control': 'no-store',\n                    },\n                })\n            }\n\n            // Not found yet, wait and retry\n            attempt++\n            if (attempt < maxAttempts) {\n                console.log(`[acme-challenge] 💤 Challenge not found yet, waiting 500ms...`)\n                await new Promise(resolve => setTimeout(resolve, 500))\n            }\n        } catch (error) {\n            console.error(`[acme-challenge] ❌ Database error on attempt ${attempt + 1}:`, error)\n            attempt++\n            if (attempt < maxAttempts) {\n                await new Promise(resolve => setTimeout(resolve, 500))\n            }\n        }\n    }\n\n    console.log(`[acme-challenge] ❌ Challenge NOT FOUND after ${maxAttempts} attempts`)\n    console.log(`[acme-challenge]    Looking for: hostname=${hostname}, token=${token}`)\n\n    return new NextResponse('Challenge not found', {\n        status: 404,\n        headers: {'Content-Type': 'text/plain'}\n    })\n}\n"
  },
  {
    "path": "app/api/acme/cancel/[certId]/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\n\nexport const runtime = 'nodejs'\n\nexport async function POST(\n    request: NextRequest,\n    { params }: { params: Promise<{ certId: string }> }\n) {\n    const { certId } = await params\n\n    try {\n        // Find the certificate\n        const cert = await db.domainCertificate.findUnique({\n            where: { id: certId },\n        })\n\n        if (!cert) {\n            return NextResponse.json(\n                { error: 'Certificate not found' },\n                { status: 404 }\n            )\n        }\n\n        // Only allow canceling pending certificates\n        if (cert.status !== 'PENDING_HTTP01' && cert.status !== 'PENDING_DNS01') {\n            return NextResponse.json(\n                { error: 'Can only cancel pending certificates' },\n                { status: 400 }\n            )\n        }\n\n        // Mark as failed with a cancellation message\n        await db.domainCertificate.update({\n            where: { id: certId },\n            data: {\n                status: 'FAILED',\n                lastError: 'Certificate issuance cancelled by user',\n            },\n        })\n\n        return NextResponse.json({ success: true })\n    } catch (error) {\n        console.error('[acme/cancel] Error:', error)\n        return NextResponse.json(\n            { error: 'Failed to cancel certificate' },\n            { status: 500 }\n        )\n    }\n}\n"
  },
  {
    "path": "app/api/acme/issue/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\nimport { sslSupported } from '@/lib/custom-domains/ssl/is-supported'\nimport {\n    initiateHttp01Certificate,\n    initiateDns01Certificate,\n} from '@/lib/custom-domains/ssl/service'\n\nexport const runtime = 'nodejs'\n\ninterface IssueRequest {\n    domainId: string\n    challengeType: 'HTTP01' | 'DNS01'\n}\n\nexport async function POST(request: NextRequest) {\n    if (!sslSupported) {\n        return NextResponse.json(\n            { error: 'SSL certificate management is only available in Docker deployments' },\n            { status: 503 },\n        )\n    }\n\n    try {\n        const body: IssueRequest = await request.json()\n\n        if (!body.domainId || !body.challengeType) {\n            return NextResponse.json(\n                { error: 'Missing required fields: domainId, challengeType' },\n                { status: 400 },\n            )\n        }\n\n        if (!['HTTP01', 'DNS01'].includes(body.challengeType)) {\n            return NextResponse.json(\n                { error: 'challengeType must be HTTP01 or DNS01' },\n                { status: 400 },\n            )\n        }\n\n        // Verify domain exists and is verified\n        const domain = await db.customDomain.findUnique({\n            where: { id: body.domainId },\n        })\n\n        if (!domain) {\n            return NextResponse.json(\n                { error: 'Domain not found' },\n                { status: 404 },\n            )\n        }\n\n        if (!domain.verified) {\n            return NextResponse.json(\n                { error: 'Domain must be verified before issuing a certificate' },\n                { status: 400 },\n            )\n        }\n\n        // Check if there's already an active certificate\n        const existingCert = await db.domainCertificate.findFirst({\n            where: {\n                domainId: body.domainId,\n                status: 'ISSUED',\n            },\n        })\n\n        if (existingCert) {\n            return NextResponse.json(\n                {\n                    error: 'Domain already has an active certificate',\n                    certificateId: existingCert.id,\n                    canForceDelete: true\n                },\n                { status: 409 },\n            )\n        }\n\n        // Check for pending/failed certificates and delete ONLY stale ones\n        // Don't delete recent pending certs (< 2 minutes old) as they might be in the middle of validation\n        const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000)\n\n        const staleCerts = await db.domainCertificate.findMany({\n            where: {\n                domainId: body.domainId,\n                status: { in: ['PENDING_HTTP01', 'PENDING_DNS01', 'FAILED'] },\n                createdAt: { lt: twoMinutesAgo },\n            },\n        })\n\n        if (staleCerts.length > 0) {\n            console.log(`[acme/issue] Deleting ${staleCerts.length} stale certificates (older than 2 minutes) for domain ${domain.domain}`)\n            await db.domainCertificate.deleteMany({\n                where: {\n                    domainId: body.domainId,\n                    status: { in: ['PENDING_HTTP01', 'PENDING_DNS01', 'FAILED'] },\n                    createdAt: { lt: twoMinutesAgo },\n                },\n            })\n        }\n\n        // Check if there's STILL a recent pending cert (created in last 2 minutes)\n        const recentPendingCert = await db.domainCertificate.findFirst({\n            where: {\n                domainId: body.domainId,\n                status: { in: ['PENDING_HTTP01', 'PENDING_DNS01'] },\n                createdAt: { gte: twoMinutesAgo },\n            },\n        })\n\n        if (recentPendingCert) {\n            console.log(`[acme/issue] Recent pending certificate already exists (created ${Math.floor((Date.now() - recentPendingCert.createdAt.getTime()) / 1000)}s ago)`)\n            return NextResponse.json(\n                {\n                    error: 'A certificate issuance is already in progress. Please wait 2 minutes before trying again.',\n                    certId: recentPendingCert.id,\n                },\n                { status: 409 },\n            )\n        }\n\n        if (body.challengeType === 'HTTP01') {\n            const certId = await initiateHttp01Certificate(\n                body.domainId,\n                domain.domain,\n            )\n\n            return NextResponse.json({ certId }, { status: 201 })\n        } else {\n            const result = await initiateDns01Certificate(\n                body.domainId,\n                domain.domain,\n            )\n\n            return NextResponse.json(result, { status: 201 })\n        }\n    } catch (error) {\n        console.error('[acme/issue] Error:', error)\n\n        const message = error instanceof Error ? error.message : 'Unknown error'\n\n        return NextResponse.json(\n            { error: message },\n            { status: 500 },\n        )\n    }\n}\n"
  },
  {
    "path": "app/api/acme/renew/[certId]/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\nimport { renewCertificate } from '@/lib/custom-domains/ssl/service'\n\nexport const runtime = 'nodejs'\n\nexport async function POST(\n    request: NextRequest,\n    { params }: { params: Promise<{ certId: string }> }\n) {\n    const { certId } = await params\n\n    try {\n        // Find the certificate with domain info\n        const cert = await db.domainCertificate.findUnique({\n            where: { id: certId },\n            include: { domain: true },\n        })\n\n        if (!cert) {\n            return NextResponse.json(\n                { error: 'Certificate not found' },\n                { status: 404 }\n            )\n        }\n\n        // Only allow renewing issued certificates\n        if (cert.status !== 'ISSUED') {\n            return NextResponse.json(\n                { error: 'Can only renew issued certificates' },\n                { status: 400 }\n            )\n        }\n\n        // Initiate renewal\n        await renewCertificate(cert)\n\n        return NextResponse.json({ success: true })\n    } catch (error) {\n        console.error('[acme/renew] Error:', error)\n        return NextResponse.json(\n            { error: error instanceof Error ? error.message : 'Failed to renew certificate' },\n            { status: 500 }\n        )\n    }\n}\n"
  },
  {
    "path": "app/api/acme/revoke/[certId]/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server'\nimport {db} from '@/lib/db'\nimport {getAcmeClient, isAcmeStagingEnabled} from '@/lib/custom-domains/ssl/acme-account'\nimport {notifyAgent} from '@/lib/custom-domains/ssl/webhook'\n\nexport const runtime = 'nodejs'\n\nexport async function POST(\n    request: NextRequest,\n    {params}: { params: Promise<{ certId: string }> }\n) {\n    const {certId} = await params\n\n    try {\n        // Find the certificate with domain info\n        const cert = await db.domainCertificate.findUnique({\n            where: {id: certId},\n            include: {domain: true},\n        })\n\n        if (!cert) {\n            return NextResponse.json(\n                {error: 'Certificate not found'},\n                {status: 404}\n            )\n        }\n\n        // Only allow revoking issued certificates\n        if (cert.status !== 'ISSUED') {\n            return NextResponse.json(\n                {error: 'Can only revoke issued certificates'},\n                {status: 400}\n            )\n        }\n\n        if (!cert.certificatePem) {\n            return NextResponse.json(\n                {error: 'Certificate data not found'},\n                {status: 400}\n            )\n        }\n\n        // Revoke with Let's Encrypt (skip when using staging/sandbox ACME endpoint)\n        if (!isAcmeStagingEnabled()) {\n            const client = await getAcmeClient()\n            await client.revokeCertificate(cert.certificatePem)\n        }\n\n        // Mark as revoked in database\n        await db.domainCertificate.update({\n            where: {id: certId},\n            data: {\n                status: 'REVOKED',\n                lastError: 'Certificate revoked by user',\n            },\n        })\n\n        // Notify nginx-agent to remove the certificate\n        await notifyAgent({\n            event: 'cert.revoked',\n            domain: cert.domain.domain,\n        })\n\n        return NextResponse.json({success: true})\n    } catch (error) {\n        console.error('[acme/revoke] Error:', error)\n        return NextResponse.json(\n            {error: error instanceof Error ? error.message : 'Failed to revoke certificate'},\n            {status: 500}\n        )\n    }\n}"
  },
  {
    "path": "app/api/acme/status/[certId]/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\nimport { sslSupported } from '@/lib/custom-domains/ssl/is-supported'\n\nexport const runtime = 'nodejs'\n\nexport async function GET(\n    _request: NextRequest,\n    { params }: { params: Promise<{ certId: string }> },\n) {\n    if (!sslSupported) {\n        return NextResponse.json(\n            { error: 'SSL certificate management is only available in Docker deployments' },\n            { status: 503 },\n        )\n    }\n\n    const { certId } = await params\n\n    if (!certId) {\n        return NextResponse.json(\n            { error: 'Certificate ID required' },\n            { status: 400 },\n        )\n    }\n\n    try {\n        const cert = await db.domainCertificate.findUnique({\n            where: { id: certId },\n            include: {\n                domain: {\n                    select: {\n                        domain: true,\n                    },\n                },\n            },\n        })\n\n        if (!cert) {\n            return NextResponse.json(\n                { error: 'Certificate not found' },\n                { status: 404 },\n            )\n        }\n\n        const response: any = {\n            certId: cert.id,\n            domain: cert.domain.domain,\n            status: cert.status,\n            challengeType: cert.challengeType,\n            issuedAt: cert.issuedAt?.toISOString() || null,\n            expiresAt: cert.expiresAt?.toISOString() || null,\n            lastError: cert.lastError || null,\n            renewalAttempts: cert.renewalAttempts,\n        }\n\n        // Include DNS challenge details if status is PENDING_DNS01\n        if (cert.status === 'PENDING_DNS01' && cert.dnsTxtValue) {\n            response.txtName = `_acme-challenge.${cert.domain.domain}`\n            response.txtValue = cert.dnsTxtValue\n        }\n\n        return NextResponse.json(response)\n    } catch (error) {\n        console.error('[acme/status] Error:', error)\n        return NextResponse.json(\n            { error: 'Internal server error' },\n            { status: 500 },\n        )\n    }\n}\n"
  },
  {
    "path": "app/api/acme/verify-dns/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport { sslSupported } from '@/lib/custom-domains/ssl/is-supported'\nimport { completeDns01Certificate } from '@/lib/custom-domains/ssl/service'\nimport { db } from '@/lib/db'\n\nexport const runtime = 'nodejs'\n\ninterface VerifyDnsRequest {\n    certId: string\n}\n\nexport async function POST(request: NextRequest) {\n    console.log('[acme/verify-dns] 🔵 DNS verification request received')\n\n    if (!sslSupported) {\n        console.log('[acme/verify-dns] ❌ SSL not supported (not in Docker deployment)')\n        return NextResponse.json(\n            { error: 'SSL certificate management is only available in Docker deployments' },\n            { status: 503 },\n        )\n    }\n\n    try {\n        const body: VerifyDnsRequest = await request.json()\n        console.log('[acme/verify-dns] 📋 Request body:', body)\n\n        if (!body.certId) {\n            console.log('[acme/verify-dns] ❌ Missing certId in request')\n            return NextResponse.json(\n                { error: 'Missing required field: certId' },\n                { status: 400 },\n            )\n        }\n\n        console.log(`[acme/verify-dns] 🔍 Looking up certificate: ${body.certId}`)\n\n        // Verify cert exists and is in the right state\n        const cert = await db.domainCertificate.findUnique({\n            where: { id: body.certId },\n            include: {\n                domain: true,\n            },\n        })\n\n        if (!cert) {\n            console.log(`[acme/verify-dns] ❌ Certificate not found: ${body.certId}`)\n            return NextResponse.json(\n                { error: 'Certificate not found' },\n                { status: 404 },\n            )\n        }\n\n        console.log(`[acme/verify-dns] ✅ Found certificate for ${cert.domain.domain}`)\n        console.log(`[acme/verify-dns] 📊 Certificate status: ${cert.status}`)\n\n        if (cert.status !== 'PENDING_DNS01') {\n            console.log(`[acme/verify-dns] ❌ Invalid certificate state: ${cert.status} (expected: PENDING_DNS01)`)\n            return NextResponse.json(\n                { error: `Certificate is not in PENDING_DNS01 state (current: ${cert.status})` },\n                { status: 400 },\n            )\n        }\n\n        console.log('[acme/verify-dns] 🚀 Starting DNS-01 completion process...')\n\n        try {\n            // Update status to show we're processing (but keep PENDING_DNS01 status)\n            await db.domainCertificate.update({\n                where: { id: body.certId },\n                data: { lastError: 'Verifying DNS TXT record...' },\n            })\n\n            await completeDns01Certificate(body.certId)\n\n\n            console.log('[acme/verify-dns] ✅ DNS verification successful!')\n            return NextResponse.json({\n                success: true,\n                message: 'Certificate issued successfully',\n            })\n        } catch (error) {\n            const message = error instanceof Error ? error.message : 'Unknown error'\n            console.error('[acme/verify-dns] ⚠️  DNS completion error:', message)\n\n            // If TXT record not propagated, DON'T mark as failed - keep it PENDING so user can retry\n            if (message.includes('not yet propagated') || message.includes('not propagated') || message.includes('DNS validation failed') || message.includes('DNS lookup failed') || message.includes('new order has been created') || message.includes('new TXT value') || message.includes('transient upstream error')) {\n                console.log('[acme/verify-dns] 💤 DNS not propagated yet, keeping status as PENDING_DNS01')\n\n                // Update error message but keep status as PENDING_DNS01\n                await db.domainCertificate.update({\n                    where: { id: body.certId },\n                    data: { lastError: message },\n                }).catch(() => {})\n\n                return NextResponse.json(\n                    {\n                        success: false,\n                        message: message,\n                        hint: 'Refresh SSL setup to see the latest TXT value before retrying.',\n                        retry: true,\n                    },\n                    { status: 202 },\n                )\n            }\n\n            // For other errors (expired orders, etc), mark as FAILED\n            console.error('[acme/verify-dns] ❌ Marking certificate as FAILED')\n            await db.domainCertificate.update({\n                where: { id: body.certId },\n                data: {\n                    status: 'FAILED',\n                    lastError: message,\n                },\n            }).catch(() => {})\n\n            // Other errors are actual failures\n            throw error\n        }\n    } catch (error) {\n        console.error('[acme/verify-dns] ❌ Fatal error:', error)\n        if (error instanceof Error) {\n            console.error('[acme/verify-dns]    Message:', error.message)\n            console.error('[acme/verify-dns]    Stack:', error.stack)\n        }\n\n        const message = error instanceof Error ? error.message : 'Unknown error'\n\n        return NextResponse.json(\n            { error: message },\n            { status: 500 },\n        )\n    }\n}"
  },
  {
    "path": "app/api/admin/ai-settings/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport { PrismaClient } from '@prisma/client'\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog'\n\nconst prisma = new PrismaClient()\n\n// Define proper types for our data structures\ninterface AISettingsResponse {\n    enableAIAssistant: boolean;\n    aiApiKey: boolean | null;\n    aiDefaultModel: string | null;\n}\n\ninterface AISettingsUpdateRequest {\n    enableAIAssistant?: boolean;\n    aiApiKey?: string | null;\n    aiDefaultModel?: string;\n}\n\ninterface SystemConfigUpdate {\n    aiApiProvider: string;\n    enableAIAssistant?: boolean;\n    aiApiKey?: string | null;\n    aiDefaultModel?: string;\n}\n\n/**\n * @method GET\n * @description Retrieve the current AI assistant settings\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"enableAIAssistant\": { \"type\": \"boolean\" },\n *     \"aiApiKey\": { \"type\": \"boolean\" },\n *     \"aiDefaultModel\": { \"type\": \"string\", \"nullable\": true }\n *   }\n * }\n * @error 401 Unauthorized - You must be logged in as an admin\n * @error 403 Forbidden - Insufficient permissions\n * @secure cookieAuth\n */\nexport async function GET() {\n    try {\n        // Validate user authentication and permissions\n        const user = await validateAuthAndGetUser()\n\n        if (!user) {\n            return new NextResponse(\n                JSON.stringify({ error: 'Unauthorized' }),\n                { status: 401 }\n            )\n        }\n\n        // Check if user is an admin\n        if (user.role !== 'ADMIN') {\n            return new NextResponse(\n                JSON.stringify({ error: 'Insufficient permissions' }),\n                { status: 403 }\n            )\n        }\n\n        // Get the system configuration\n        const config = await prisma.systemConfig.findFirst({\n            where: { id: 1 },\n            select: {\n                enableAIAssistant: true,\n                aiApiKey: true,\n                aiDefaultModel: true,\n            }\n        }) || {\n            enableAIAssistant: false,\n            aiApiKey: null,\n            aiDefaultModel: 'copilot-zero',\n        }\n\n        // Return the settings\n        const response: AISettingsResponse = {\n            enableAIAssistant: config.enableAIAssistant,\n            aiApiKey: config.aiApiKey ? true : null, // Only return boolean for security\n            aiDefaultModel: config.aiDefaultModel,\n        };\n\n        return NextResponse.json(response);\n    } catch (error) {\n        console.error('Error fetching AI settings:', error)\n        return new NextResponse(\n            JSON.stringify({ error: 'Failed to fetch AI settings' }),\n            { status: 500 }\n        )\n    }\n}\n\n/**\n * @method POST\n * @description Update AI assistant settings\n * @body {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"enableAIAssistant\": { \"type\": \"boolean\", \"nullable\": true },\n *     \"aiApiKey\": { \"type\": \"string\", \"nullable\": true },\n *     \"aiDefaultModel\": { \"type\": \"string\", \"nullable\": true }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\" },\n *     \"message\": { \"type\": \"string\" }\n *   }\n * }\n * @error 400 Bad Request - Invalid request body\n * @error 401 Unauthorized - You must be logged in as an admin\n * @error 403 Forbidden - Insufficient permissions\n * @secure cookieAuth\n */\nexport async function POST(request: Request) {\n    try {\n        // Validate user authentication and permissions\n        const user = await validateAuthAndGetUser()\n\n        if (!user) {\n            return new NextResponse(\n                JSON.stringify({ error: 'Unauthorized' }),\n                { status: 401 }\n            )\n        }\n\n        // Check if user is an admin\n        if (user.role !== 'ADMIN') {\n            return new NextResponse(\n                JSON.stringify({ error: 'Insufficient permissions' }),\n                { status: 403 }\n            )\n        }\n\n        // Parse request body\n        const body: AISettingsUpdateRequest = await request.json()\n\n        // Validate request body\n        if (typeof body !== 'object') {\n            return new NextResponse(\n                JSON.stringify({ error: 'Invalid request body' }),\n                { status: 400 }\n            )\n        }\n\n        // Extract fields with validation and create properly typed update data object\n        const updateData: SystemConfigUpdate = {\n            // Always set provider to Secton\n            aiApiProvider: 'secton',\n        }\n\n        if (typeof body.enableAIAssistant === 'boolean') {\n            updateData.enableAIAssistant = body.enableAIAssistant\n        }\n\n        if (body.aiApiKey !== undefined) {\n            updateData.aiApiKey = body.aiApiKey\n        }\n\n        if (body.aiDefaultModel) {\n            updateData.aiDefaultModel = body.aiDefaultModel\n        }\n\n        // Update the system configuration\n        await prisma.systemConfig.upsert({\n            where: { id: 1 },\n            update: updateData,\n            create: {\n                id: 1,\n                ...updateData,\n                // Add required default values for create operation\n                defaultInvitationExpiry: 7,\n                requireApprovalForChangelogs: true,\n                maxChangelogEntriesPerProject: 100,\n                enableAnalytics: true,\n                enableNotifications: true,\n                enablePasswordReset: false,\n                updatedAt: new Date(),\n            }\n        })\n\n        // Add audit log entry\n        await prisma.auditLog.create({\n            data: {\n                action: 'UPDATE_AI_SETTINGS',\n                userId: user.id,\n                details: {\n                    enableAIAssistant: updateData.enableAIAssistant,\n                    aiDefaultModel: updateData.aiDefaultModel,\n                    // Don't log the API key for security\n                    aiApiKeyUpdated: updateData.aiApiKey !== undefined\n                }\n            }\n        })\n\n        // Return success response\n        return NextResponse.json({\n            success: true,\n            message: 'AI settings updated successfully'\n        })\n    } catch (error) {\n        console.error('Error updating AI settings:', error)\n        return new NextResponse(\n            JSON.stringify({ error: 'Failed to update AI settings' }),\n            { status: 500 }\n        )\n    }\n}"
  },
  {
    "path": "app/api/admin/ai-settings/test-key/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog'\nimport { createSectonClient } from '@/lib/utils/ai/secton'\n\n// Define proper types for our data structures\ninterface APIKeyRequest {\n    apiKey: string;\n}\n\ninterface APIKeyResponse {\n    valid: boolean;\n    message: string;\n}\n\n/**\n * @method POST\n * @description Test the validity of a Secton AI API key\n * @body {\n *   \"type\": \"object\",\n *   \"required\": [\"apiKey\"],\n *   \"properties\": {\n *     \"apiKey\": { \"type\": \"string\" }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"valid\": { \"type\": \"boolean\" },\n *     \"message\": { \"type\": \"string\" }\n *   }\n * }\n * @error 400 Bad Request - Invalid request body\n * @error 401 Unauthorized - You must be logged in as an admin\n * @error 403 Forbidden - Insufficient permissions\n * @secure cookieAuth\n */\nexport async function POST(request: Request) {\n    try {\n        // Validate user authentication and permissions\n        const user = await validateAuthAndGetUser()\n\n        if (!user) {\n            return new NextResponse(\n                JSON.stringify({ error: 'Unauthorized' }),\n                { status: 401 }\n            )\n        }\n\n        // Check if user is an admin\n        if (user.role !== 'ADMIN') {\n            return new NextResponse(\n                JSON.stringify({ error: 'Insufficient permissions' }),\n                { status: 403 }\n            )\n        }\n\n        // Parse request body\n        const body: APIKeyRequest = await request.json()\n\n        // Validate API key\n        if (!body.apiKey || typeof body.apiKey !== 'string') {\n            return new NextResponse(\n                JSON.stringify({ error: 'API key is required' }),\n                { status: 400 }\n            )\n        }\n\n        // Basic validation - check if it starts with sk_\n        if (!body.apiKey.startsWith('sk_')) {\n            const response: APIKeyResponse = {\n                valid: false,\n                message: 'Invalid API key format. Secton API keys should start with \"sk-_.'\n            };\n\n            return NextResponse.json(response, { status: 400 });\n        }\n\n        // Test the API key using the Secton client\n        try {\n            const client = createSectonClient({\n                apiKey: body.apiKey,\n            });\n\n            // Try to validate the API key\n            const isValid = await client.validateApiKey();\n\n            if (isValid) {\n                const response: APIKeyResponse = {\n                    valid: true,\n                    message: 'API key is valid and working correctly.',\n                };\n\n                return NextResponse.json(response);\n            } else {\n                const response: APIKeyResponse = {\n                    valid: false,\n                    message: 'Invalid API key. Please check your Secton API key and try again.',\n                };\n\n                return NextResponse.json(response, { status: 400 });\n            }\n        } catch (error) {\n            console.error('Error validating Secton API key:', error);\n\n            const response: APIKeyResponse = {\n                valid: false,\n                message: error instanceof Error ? error.message : 'Failed to validate API key.',\n            };\n\n            return NextResponse.json(response, { status: 400 });\n        }\n    } catch (error) {\n        console.error('Error testing API key:', error);\n\n        return new NextResponse(\n            JSON.stringify({\n                error: 'Server error while testing API key'\n            }),\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/admin/analytics/route.ts",
    "content": "import {NextResponse} from 'next/server';\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog';\nimport {getSystemAnalytics, getTimeRange} from '@/lib/utils/analytics';\nimport {db} from '@/lib/db';\nimport {z} from 'zod';\nimport type {AnalyticsPeriod, AnalyticsQueryParams} from '@/lib/types/analytics';\n\nconst analyticsQuerySchema = z.object({\n    period: z.enum(['7d', '30d', '90d', '1y']).optional().default('30d'),\n    startDate: z.string().optional(),\n    endDate: z.string().optional(),\n});\n\n/**\n * @method GET\n * @description Get system-wide analytics data (admin only)\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\" },\n *     \"data\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"totalViews\": { \"type\": \"number\" },\n *         \"uniqueVisitors\": { \"type\": \"number\" },\n *         \"topCountries\": {\n *           \"type\": \"array\",\n *           \"items\": {\n *             \"type\": \"object\",\n *             \"properties\": {\n *               \"country\": { \"type\": \"string\" },\n *               \"count\": { \"type\": \"number\" }\n *             }\n *           }\n *         },\n *         \"dailyViews\": {\n *           \"type\": \"array\",\n *           \"items\": {\n *             \"type\": \"object\",\n *             \"properties\": {\n *               \"date\": { \"type\": \"string\" },\n *               \"views\": { \"type\": \"number\" },\n *               \"uniqueVisitors\": { \"type\": \"number\" }\n *             }\n *           }\n *         },\n *         \"topProjects\": {\n *           \"type\": \"array\",\n *           \"items\": {\n *             \"type\": \"object\",\n *             \"properties\": {\n *               \"projectId\": { \"type\": \"string\" },\n *               \"projectName\": { \"type\": \"string\" },\n *               \"views\": { \"type\": \"number\" },\n *               \"uniqueVisitors\": { \"type\": \"number\" }\n *             }\n *           }\n *         },\n *         \"topEntries\": {\n *           \"type\": \"array\",\n *           \"items\": {\n *             \"type\": \"object\",\n *             \"properties\": {\n *               \"entryId\": { \"type\": \"string\" },\n *               \"title\": { \"type\": \"string\" },\n *               \"views\": { \"type\": \"number\" },\n *               \"uniqueVisitors\": { \"type\": \"number\" }\n *             }\n *           }\n *         },\n *         \"topReferrers\": {\n *           \"type\": \"array\",\n *           \"items\": {\n *             \"type\": \"object\",\n *             \"properties\": {\n *               \"referrer\": { \"type\": \"string\" },\n *               \"count\": { \"type\": \"number\" }\n *             }\n *           }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 400 Invalid parameters\n * @error 403 Unauthorized - User does not have admin role\n * @error 500 Internal server error\n */\nexport async function GET(request: Request) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        // Check if user is admin\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                {\n                    success: false,\n                    error: 'Not authorized to view system analytics'\n                },\n                {status: 403}\n            );\n        }\n\n        // Check if analytics are enabled\n        const systemConfig = await db.systemConfig.findFirst();\n        if (!systemConfig?.enableAnalytics) {\n            return NextResponse.json(\n                {\n                    success: false,\n                    error: 'Analytics are disabled'\n                },\n                {status: 403}\n            );\n        }\n\n        // Parse query parameters\n        const url = new URL(request.url);\n        const queryParams: AnalyticsQueryParams = {\n            period: (url.searchParams.get('period') as AnalyticsPeriod) || '30d',\n            startDate: url.searchParams.get('startDate') || undefined,\n            endDate: url.searchParams.get('endDate') || undefined,\n        };\n\n        const validatedParams = analyticsQuerySchema.parse(queryParams);\n\n        // Determine time range\n        let timeRange;\n        if (validatedParams.startDate && validatedParams.endDate) {\n            timeRange = {\n                start: new Date(validatedParams.startDate),\n                end: new Date(validatedParams.endDate)\n            };\n        } else {\n            timeRange = getTimeRange(validatedParams.period);\n        }\n\n        // Get system-wide analytics data\n        const analyticsData = await getSystemAnalytics(timeRange);\n\n        return NextResponse.json({\n            success: true,\n            data: {\n                ...analyticsData,\n                period: validatedParams.period,\n                timeRange: {\n                    start: timeRange.start.toISOString(),\n                    end: timeRange.end.toISOString()\n                }\n            }\n        });\n\n    } catch (error) {\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {\n                    success: false,\n                    error: 'Invalid parameters',\n                    details: error.errors\n                },\n                {status: 400}\n            );\n        }\n\n        console.error('Error fetching system analytics:', error);\n        return NextResponse.json(\n            {\n                success: false,\n                error: 'Failed to fetch analytics data'\n            },\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/admin/api-keys/[keyId]/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { db } from '@/lib/db';\nimport { z } from 'zod';\nimport { createAuditLog } from '@/lib/utils/auditLog';\n\nconst updateApiKeySchema = z.object({\n    name: z.string().min(1).max(100).optional(),\n    isRevoked: z.boolean().optional(),\n    expiresAt: z.string().datetime().optional().nullable(),\n});\n\n/**\n * Retrieves the details of an API key\n * @method GET\n * @description Returns the details of an API key, including its name, last used date, created date, expiration date, revocation status, and permissions. Requires admin permissions.\n * @body None\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"name\": { \"type\": \"string\" },\n *     \"lastUsed\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"expiresAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"isRevoked\": { \"type\": \"boolean\" },\n *     \"permissions\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n *     \"user\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"email\": { \"type\": \"string\" },\n *         \"name\": { \"type\": \"string\" }\n *       }\n *     }\n *   }\n * }\n * @error 401 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n * @error 404 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n */\nexport async function GET(\n    request: Request,\n    { params }: { params: Promise<{ keyId: string }> }\n): Promise<Response> {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                { error: 'Unauthorized' },\n                { status: 403 }\n            );\n        }\n\n        const apiKey = await db.apiKey.findUnique({\n            where: { id: (await params).keyId },\n            select: {\n                id: true,\n                name: true,\n                lastUsed: true,\n                createdAt: true,\n                expiresAt: true,\n                isRevoked: true,\n                permissions: true,\n                user: {\n                    select: {\n                        id: true,\n                        email: true,\n                        name: true\n                    }\n                }\n            }\n        });\n\n        if (!apiKey) {\n            return NextResponse.json(\n                { error: 'API key not found' },\n                { status: 404 }\n            );\n        }\n\n        return NextResponse.json(apiKey);\n    } catch (error) {\n        console.error('Failed to fetch API key:', error);\n        return NextResponse.json(\n            { error: 'Failed to fetch API key' },\n            { status: 500 }\n        );\n    }\n}\n\n/**\n * Updates an API key's details\n * @method PATCH\n * @description Updates an API key's name, revocation status, or expiration date. Requires admin permissions.\n * @body {\n *   \"type\": \"object\",\n *   \"required\": [\"id\"],\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"name\": { \"type\": \"string\", \"minLength\": 1, \"maxLength\": 100 },\n *     \"isRevoked\": { \"type\": \"boolean\" },\n *     \"expiresAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"name\": { \"type\": \"string\" },\n *     \"lastUsed\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"expiresAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"isRevoked\": { \"type\": \"boolean\" },\n *     \"permissions\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n *     \"user\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"email\": { \"type\": \"string\" },\n *         \"name\": { \"type\": \"string\" }\n *       }\n *     }\n *   }\n * }\n * @error 401 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n * @error 404 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n * @error 400 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" },\n *     \"details\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"message\": { \"type\": \"string\" },\n *           \"path\": { \"type\": \"string\" }\n *         }\n *       }\n *     }\n *   }\n * }\n */\nexport async function PATCH(\n    request: Request,\n    { params }: { params: Promise<{ keyId: string }> }\n): Promise<Response> {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                { error: 'Unauthorized' },\n                { status: 403 }\n            );\n        }\n\n        const body = await request.json();\n        const validatedData = updateApiKeySchema.parse(body);\n        const keyId = (await params).keyId;\n\n        const existingKey = await db.apiKey.findUnique({\n            where: { id: keyId },\n            include: {\n                user: {\n                    select: {\n                        id: true\n                    }\n                }\n            }\n        });\n\n        if (!existingKey) {\n            return NextResponse.json(\n                { error: 'API key not found' },\n                { status: 404 }\n            );\n        }\n\n        // If the key is already revoked, only allow name changes\n        if (existingKey.isRevoked && validatedData.isRevoked === false) {\n            return NextResponse.json(\n                { error: 'Cannot un-revoke an API key' },\n                { status: 400 }\n            );\n        }\n\n        // Track what changed for the audit log\n        const changes: Record<string, { from: unknown; to: unknown }> = {};\n\n        if (validatedData.name && validatedData.name !== existingKey.name) {\n            changes.name = { from: existingKey.name, to: validatedData.name };\n        }\n\n        if (validatedData.isRevoked !== undefined && validatedData.isRevoked !== existingKey.isRevoked) {\n            changes.isRevoked = { from: existingKey.isRevoked, to: validatedData.isRevoked };\n        }\n\n        if (validatedData.expiresAt !== undefined) {\n            const newExpiresAt = validatedData.expiresAt ? new Date(validatedData.expiresAt) : null;\n            const oldExpiresAt = existingKey.expiresAt?.toISOString() || null;\n            const newExpiresAtStr = newExpiresAt?.toISOString() || null;\n\n            if (oldExpiresAt !== newExpiresAtStr) {\n                changes.expiresAt = { from: oldExpiresAt, to: newExpiresAtStr };\n            }\n        }\n\n        const apiKey = await db.apiKey.update({\n            where: { id: keyId },\n            data: {\n                ...(validatedData.name && { name: validatedData.name }),\n                ...(validatedData.isRevoked !== undefined && { isRevoked: validatedData.isRevoked }),\n                ...(validatedData.expiresAt !== undefined && {\n                    expiresAt: validatedData.expiresAt ? new Date(validatedData.expiresAt) : null\n                })\n            },\n            select: {\n                id: true,\n                name: true,\n                lastUsed: true,\n                createdAt: true,\n                expiresAt: true,\n                isRevoked: true,\n                permissions: true\n            }\n        });\n\n        // Determine the audit log action based on what changed\n        if (Object.keys(changes).length > 0) {\n            let auditAction = 'UPDATE_API_KEY';\n\n            // Use a more specific action if only one type of change occurred\n            if (changes.isRevoked && changes.isRevoked.to === true && Object.keys(changes).length === 1) {\n                auditAction = 'REVOKE_API_KEY';\n            } else if (changes.name && Object.keys(changes).length === 1) {\n                auditAction = 'RENAME_API_KEY';\n            }\n\n            try {\n                await createAuditLog(\n                    auditAction,\n                    user.id,\n                    user.id, // Use the admin's ID as the target user ID to avoid foreign key issues\n                    {\n                        apiKeyId: keyId,\n                        apiKeyName: apiKey.name,\n                        changes,\n                        ownerId: existingKey.user.id\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create audit log:', auditLogError);\n                // Continue execution even if audit log creation fails\n            }\n        }\n\n        return NextResponse.json(apiKey);\n    } catch (error) {\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: 'Invalid request data', details: error.errors },\n                { status: 400 }\n            );\n        }\n\n        console.error('Failed to update API key:', error);\n        return NextResponse.json(\n            { error: 'Failed to update API key' },\n            { status: 500 }\n        );\n    }\n}\n\n/**\n * Revokes an API key\n * @method DELETE\n * @description Revokes an API key, making it unable to be used for authentication. Requires admin permissions.\n * @body None\n * @response 200 { \"type\": \"object\", \"properties\": { \"success\": true } }\n * @error 401 { \"type\": \"object\", \"properties\": { \"error\": \"Unauthorized\" } }\n * @error 404 { \"type\": \"object\", \"properties\": { \"error\": \"API key not found\" } }\n */\nexport async function DELETE(\n    request: Request,\n    { params }: { params: Promise<{ keyId: string }> }\n): Promise<Response> {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                { error: 'Unauthorized' },\n                { status: 403 }\n            );\n        }\n\n        const keyId = (await params).keyId;\n        const existingKey = await db.apiKey.findUnique({\n            where: { id: keyId },\n            include: {\n                user: {\n                    select: {\n                        id: true\n                    }\n                }\n            }\n        });\n\n        if (!existingKey) {\n            return NextResponse.json(\n                { error: 'API key not found' },\n                { status: 404 }\n            );\n        }\n\n        // Only allow deletion of revoked keys\n        if (!existingKey.isRevoked) {\n            return NextResponse.json(\n                { error: 'Cannot delete an active API key. Revoke it first.' },\n                { status: 400 }\n            );\n        }\n\n        // Store key details for audit log before deletion\n        const apiKeyDetails = {\n            id: existingKey.id,\n            name: existingKey.name,\n            ownerId: existingKey.user.id\n        };\n\n        // Permanently delete the key\n        await db.apiKey.delete({\n            where: { id: keyId }\n        });\n\n        // Create audit log for deletion\n        try {\n            await createAuditLog(\n                'DELETE_API_KEY',\n                user.id,\n                user.id, // Use the admin's ID as the target user ID to avoid foreign key issues\n                {\n                    apiKeyId: apiKeyDetails.id,\n                    apiKeyName: apiKeyDetails.name,\n                    ownerId: apiKeyDetails.ownerId\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n            // Continue execution even if audit log creation fails\n        }\n\n        return NextResponse.json({ success: true });\n    } catch (error) {\n        console.error('Failed to delete API key:', error);\n        return NextResponse.json(\n            { error: 'Failed to delete API key' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/admin/api-keys/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { z } from 'zod';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { createAuditLog } from '@/lib/utils/auditLog'; // Import the audit log function\nimport { db } from '@/lib/db';\nimport { nanoid } from 'nanoid';\n\n// Validation schemas\nconst createApiKeySchema = z.object({\n    name: z.string().min(1).max(100),\n    expiresAt: z.string().datetime().optional(),\n    permissions: z.array(z.string()).optional(),\n});\n\n/**\n * @method GET\n * @description Fetches a list of API keys for the authenticated user\n * @query {\n *   page: Number, default: 1\n *   pageSize: Number, default: 20\n * }\n * @response 200 {\n *   \"type\": \"array\",\n *   \"items\": {\n *     \"type\": \"object\",\n *     \"properties\": {\n *       \"id\": { \"type\": \"string\" },\n *       \"name\": { \"type\": \"string\" },\n *       \"key\": { \"type\": \"string\" },\n *       \"lastUsed\": { \"type\": \"string\", \"format\": \"date-time\" },\n *       \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *       \"expiresAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *       \"isRevoked\": { \"type\": \"boolean\" },\n *       \"permissions\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n *       \"user\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"email\": { \"type\": \"string\" },\n *           \"name\": { \"type\": \"string\" }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 403 Unauthorized - User does not have 'ADMIN' role\n * @error 500 An unexpected error occurred while fetching API keys\n */\nexport async function GET() {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        // Only admins can list API keys\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                { error: 'Unauthorized' },\n                { status: 403 }\n            );\n        }\n\n        const apiKeys = await db.apiKey.findMany({\n            select: {\n                id: true,\n                name: true,\n                key: true,\n                lastUsed: true,\n                createdAt: true,\n                expiresAt: true,\n                isRevoked: true,\n                permissions: true,\n                user: {\n                    select: {\n                        id: true,\n                        email: true,\n                        name: true\n                    }\n                }\n            },\n            orderBy: {\n                createdAt: 'desc'\n            }\n        });\n\n        // Log the action of viewing API keys\n        try {\n            await createAuditLog(\n                'VIEW_API_KEYS',\n                user.id,\n                user.id,\n                {\n                    apiKeyCount: apiKeys.length || 0\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n            // Continue execution even if audit log creation fails\n        }\n\n        // Mask the key value — full key is only shown on creation\n        const safeApiKeys = apiKeys.map(({ key, ...rest }) => ({\n            ...rest,\n            keyPrefix: key.slice(0, 12) + '...'\n        }));\n\n        return NextResponse.json(safeApiKeys);\n    } catch (error) {\n        console.error('Failed to fetch API keys:', error);\n        return NextResponse.json(\n            { error: 'Failed to fetch API keys' },\n            { status: 500 }\n        );\n    }\n}\n\n/**\n * @method POST\n * @description Creates a new API key for the authenticated user\n * @body {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"name\": { \"type\": \"string\" },\n *     \"expiresAt\": { \"type\": \"string\", \"format\": \"date\" },\n *     \"permissions\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n *   },\n *   \"required\": [\n *     \"name\"\n *   ]\n * }\n * @response 201 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"name\": { \"type\": \"string\" },\n *     \"key\": { \"type\": \"string\" },\n *     \"lastUsed\": { \"type\": \"null\" },\n *     \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"expiresAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"isRevoked\": { \"type\": \"boolean\", \"default\": false },\n *     \"permissions\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n *     \"user\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"email\": { \"type\": \"string\" },\n *         \"name\": { \"type\": \"string\" }\n *       }\n *     }\n *   }\n * }\n * @error 403 Unauthorized - User does not have 'ADMIN' role\n * @error 400 Invalid request data\n * @error 500 An unexpected error occurred while creating an API key\n */\nexport async function POST(request: Request) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        // Only admins can create API keys\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                { error: 'Unauthorized' },\n                { status: 403 }\n            );\n        }\n\n        const body = await request.json();\n        const validatedData = createApiKeySchema.parse(body);\n\n        // Generate a unique API key\n        const apiKeyString = `chr_${nanoid(32)}`;\n\n        const apiKey = await db.apiKey.create({\n            data: {\n                name: validatedData.name,\n                key: apiKeyString,\n                expiresAt: validatedData.expiresAt ? new Date(validatedData.expiresAt) : null,\n                permissions: validatedData.permissions || [],\n                userId: user.id\n            }\n        });\n\n        // Log the action of creating an API key\n        try {\n            await createAuditLog(\n                'CREATE_API_KEY',\n                user.id,    // performedById - the admin user creating the key\n                user.id,    // targetUserId - use the user's ID instead of the API key ID\n                {\n                    apiKeyId: apiKey.id,    // Include the API key ID in the details instead\n                    apiKeyName: apiKey.name,\n                    expiresAt: apiKey.expiresAt?.toISOString() || 'N/A',\n                    permissions: apiKey.permissions || []\n                }\n            );\n        } catch (auditLogError: unknown) {\n            console.error('Failed to create audit log:', (auditLogError as Error).stack);\n            // Continue execution even if audit log creation fails\n        }\n\n        return NextResponse.json({\n            ...apiKey,\n            key: apiKeyString // Only return the full key on creation\n        });\n    } catch (error) {\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: 'Invalid request data', details: error.errors },\n                { status: 400 }\n            );\n        }\n\n        console.error('Failed to create API key:', error);\n        return NextResponse.json(\n            { error: 'Failed to create API key' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/admin/audit-logs/actions/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog'\n\n/**\n * @method GET\n * @description Fetches all unique audit log actions for filter dropdown\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"actions\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"action\": { \"type\": \"string\" },\n *           \"count\": { \"type\": \"number\" }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 403 Unauthorized - User does not have 'ADMIN' role\n * @error 500 An unexpected error occurred while fetching actions\n */\nexport async function GET() {\n    try {\n        // Validate admin access\n        const user = await validateAuthAndGetUser()\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })\n        }\n\n        // Get all unique actions with their counts\n        const actionsWithCounts = await db.auditLog.groupBy({\n            by: ['action'],\n            _count: {\n                action: true\n            },\n            orderBy: {\n                _count: {\n                    action: 'desc'\n                }\n            }\n        })\n\n        // Transform to desired format\n        const actions = actionsWithCounts.map(item => ({\n            action: item.action,\n            count: item._count.action\n        }))\n\n        return NextResponse.json({ actions })\n    } catch (error) {\n        console.error('Error fetching audit log actions:', error)\n        return NextResponse.json(\n            { error: 'Failed to fetch audit log actions' },\n            { status: 500 }\n        )\n    }\n}"
  },
  {
    "path": "app/api/admin/audit-logs/route.ts",
    "content": "import {NextResponse} from 'next/server'\nimport {db} from '@/lib/db'\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog'\n\n// Type definitions\ninterface PreservedUserData {\n    id: string;\n    email: string;\n    name: string | null;\n    role: string;\n    deletedAt: string;\n    deletedBy: string;\n}\n\ninterface AuditLogDetails {\n    [key: string]: unknown;\n\n    _preservedUser?: PreservedUserData;\n    _preservedTargetUser?: PreservedUserData;\n    timestamp?: string;\n    userAgent?: string;\n    ipAddress?: string;\n    // Action-specific details\n    loginMethod?: string;\n    successful?: boolean;\n    sessionId?: string;\n    newUserRole?: string;\n    invitationUsed?: boolean;\n    emailVerified?: boolean;\n    projectName?: string;\n    isPublic?: boolean;\n    initialTeamSize?: number;\n    version?: string;\n    entryCount?: number;\n    hasBreakingChanges?: boolean;\n    itemType?: string;\n    itemCount?: number;\n    confirmed?: boolean;\n    keyName?: string;\n    permissions?: string[];\n    expiresAt?: string;\n    settingsChanged?: string[];\n    previousValues?: Record<string, unknown>;\n    itemId?: string;\n    changeType?: string;\n    metadata?: Record<string, unknown>;\n}\n\ninterface DatabaseAuditLog {\n    id: string;\n    action: string;\n    userId: string | null;\n    targetUserId: string | null;\n    details: AuditLogDetails | null;\n    createdAt: Date;\n    user: {\n        name: string | null;\n        email: string;\n    } | null;\n    targetUser: {\n        name: string | null;\n        email: string;\n    } | null;\n}\n\ninterface ProcessedUserInfo {\n    name: string | null;\n    email: string | null;\n    isDeleted?: boolean;\n}\n\ninterface ProcessedAuditLog extends Omit<DatabaseAuditLog, 'user' | 'targetUser' | 'createdAt'> {\n    createdAt: string; // Converted to ISO string\n    performer: ProcessedUserInfo | null;\n    performer_email: string | null;\n    target: ProcessedUserInfo | null;\n    target_email: string | null;\n}\n\ninterface CsvLogEntry {\n    timestamp: Date;\n    action: string;\n    performer: string;\n    performer_email: string;\n    performer_deleted: string;\n    target: string;\n    target_email: string;\n    target_deleted: string;\n    details: string;\n}\n\ninterface AuditLogsResponse {\n    logs: ProcessedAuditLog[];\n    total: number;\n    pages: number;\n    nextCursor: string | null;\n    hasMore: boolean;\n    currentCount: number;\n}\n\ninterface WhereClause {\n    createdAt?: {\n        gte?: Date;\n        lte?: Date;\n    };\n    action?: string;\n    userId?: string;\n    targetUserId?: string;\n    OR?: Array<{\n        user?: {\n            OR: Array<{\n                name?: { contains: string; mode: 'insensitive' };\n                email?: { contains: string; mode: 'insensitive' };\n            }>;\n        };\n        targetUser?: {\n            OR: Array<{\n                name?: { contains: string; mode: 'insensitive' };\n                email?: { contains: string; mode: 'insensitive' };\n            }>;\n        };\n        action?: { contains: string; mode: 'insensitive' };\n        details?: {\n            path: string[];\n            string_contains: string;\n        };\n    }>;\n}\n\n/**\n * Get user info from log (either from user relation or preserved data)\n */\nfunction getLogUserInfo(log: DatabaseAuditLog, isTarget = false): ProcessedUserInfo | null {\n    const userKey = isTarget ? 'targetUser' : 'user';\n    const preservedKey = isTarget ? '_preservedTargetUser' : '_preservedUser';\n\n    // First try to get from user relation\n    if (log[userKey]) {\n        return {\n            name: log[userKey]?.name || null,\n            email: log[userKey]?.email || null\n        };\n    }\n\n    // If user relation is null, try to get from preserved data\n    if (log.details && log.details[preservedKey]) {\n        const preserved = log.details[preservedKey];\n        return {\n            name: preserved?.name || null,\n            email: preserved?.email || null,\n            isDeleted: true // Flag to indicate this user was deleted\n        };\n    }\n\n    // Return null if no user info available\n    return null;\n}\n\n/**\n * Process logs to include preserved user info and format dates\n */\nfunction processLogs(logs: DatabaseAuditLog[]): ProcessedAuditLog[] {\n    return logs.map(log => {\n        const performer = getLogUserInfo(log, false);\n        const target = getLogUserInfo(log, true);\n\n        return {\n            ...log,\n            createdAt: log.createdAt.toISOString(),\n            performer: performer ? {\n                name: performer.name,\n                email: performer.email,\n                isDeleted: performer.isDeleted || false\n            } : null,\n            performer_email: performer?.email || null,\n            target: target ? {\n                name: target.name,\n                email: target.email,\n                isDeleted: target.isDeleted || false\n            } : null,\n            target_email: target?.email || null\n        };\n    });\n}\n\n/**\n * Convert logs to CSV format\n */\nfunction convertLogsToCSV(logs: DatabaseAuditLog[]): string {\n    const csvContent: CsvLogEntry[] = logs.map(log => {\n        const performer = getLogUserInfo(log, false);\n        const target = getLogUserInfo(log, true);\n\n        return {\n            timestamp: log.createdAt,\n            action: log.action,\n            performer: performer ? (performer.name || performer.email || 'Unknown User') : 'Unknown User',\n            performer_email: performer?.email || 'Unknown',\n            performer_deleted: performer?.isDeleted ? 'Yes' : 'No',\n            target: target ? (target.name || target.email || 'N/A') : 'N/A',\n            target_email: target?.email || 'N/A',\n            target_deleted: target?.isDeleted ? 'Yes' : 'No',\n            details: log.details ? JSON.stringify(log.details) : ''\n        };\n    });\n\n    return [\n        // CSV Headers\n        Object.keys(csvContent[0] || {}).join(','),\n        // CSV Data\n        ...csvContent.map(row =>\n            Object.values(row)\n                .map(val => `\"${String(val).replace(/\"/g, '\"\"')}\"`)\n                .join(',')\n        )\n    ].join('\\n');\n}\n\n/**\n * @method GET\n * @description Fetches audit logs based on filters and pagination or chunking\n * @query {\n *   page: Number, default: 1\n *   pageSize: Number, default: 20\n *   cursor: String, optional, for chunk-based pagination\n *   chunkSize: Number, default: 100, for chunk-based loading\n *   useChunking: boolean, optional, enables chunked data loading\n *   search: String, optional\n *   action: String, optional\n *   from: Date, optional\n *   to: Date, optional\n *   userId: String, optional\n *   targetId: String, optional\n *   export: boolean, optional\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"logs\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"action\": { \"type\": \"string\" },\n *           \"performer\": {\n *             \"type\": \"object\",\n *             \"properties\": {\n *               \"name\": { \"type\": \"string\" },\n *               \"email\": { \"type\": \"string\" },\n *               \"isDeleted\": { \"type\": \"boolean\" }\n *             }\n *           },\n *           \"performer_email\": { \"type\": \"string\" },\n *           \"target\": {\n *             \"type\": \"object\",\n *             \"properties\": {\n *               \"name\": { \"type\": \"string\" },\n *               \"email\": { \"type\": \"string\" },\n *               \"isDeleted\": { \"type\": \"boolean\" }\n *             }\n *           },\n *           \"target_email\": { \"type\": \"string\" },\n *           \"details\": { \"type\": \"object\" },\n *           \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *         }\n *       }\n *     },\n *     \"total\": { \"type\": \"number\" },\n *     \"pages\": { \"type\": \"number\" },\n *     \"nextCursor\": { \"type\": \"string\" },\n *     \"hasMore\": { \"type\": \"boolean\" },\n *     \"currentCount\": { \"type\": \"number\" }\n *   }\n * }\n * @error 403 Unauthorized - User does not have 'ADMIN' role\n * @error 500 An unexpected error occurred while fetching audit logs\n */\nexport async function GET(request: Request): Promise<NextResponse<AuditLogsResponse | { error: string }>> {\n    try {\n        // Validate admin access\n        const user = await validateAuthAndGetUser()\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json({error: 'Unauthorized'}, {status: 403})\n        }\n\n        // Parse query parameters\n        const {searchParams} = new URL(request.url)\n        const useChunking = searchParams.get('useChunking') === 'true'\n        const cursor = searchParams.get('cursor') || ''\n        const chunkSize = Math.min(parseInt(searchParams.get('chunkSize') || '100', 10), 500) // Limit max chunk size\n        const page = parseInt(searchParams.get('page') || '1', 10)\n        const pageSize = parseInt(searchParams.get('pageSize') || '20', 10)\n        const search = searchParams.get('search') || ''\n        const action = searchParams.get('action') || ''\n        const from = searchParams.get('from') || ''\n        const to = searchParams.get('to') || ''\n        const userId = searchParams.get('userId') || ''\n        const targetId = searchParams.get('targetId') || ''\n        const isExport = searchParams.get('export') === 'true'\n\n        // console.log(`🦖 Audit Logs API - Cursor: ${cursor ? cursor.substring(0, 10) + '...' : 'none'}, ChunkSize: ${chunkSize}, UseChunking: ${useChunking}`);\n\n        // Build where clause for filtering\n        const where: WhereClause = {}\n\n        // Date range filter\n        if (from || to) {\n            where.createdAt = {}\n            if (from) {\n                const fromDate = new Date(from);\n                if (!isNaN(fromDate.getTime())) {\n                    where.createdAt.gte = fromDate;\n                    // console.log('🦖 Date filter FROM:', fromDate.toISOString());\n                }\n            }\n            if (to) {\n                const toDate = new Date(to);\n                if (!isNaN(toDate.getTime())) {\n                    where.createdAt.lte = toDate;\n                    // console.log('🦖 Date filter TO:', toDate.toISOString());\n                }\n            }\n        }\n\n        // Action filter\n        if (action) {\n            where.action = action\n            // console.log('🦖 Action filter:', action);\n        }\n\n        // User ID filters\n        if (userId) where.userId = userId\n        if (targetId) where.targetUserId = targetId\n\n        // Search filter - includes preserved user data\n        if (search) {\n            where.OR = [\n                {\n                    user: {\n                        OR: [\n                            {name: {contains: search, mode: 'insensitive'}},\n                            {email: {contains: search, mode: 'insensitive'}}\n                        ]\n                    }\n                },\n                {\n                    targetUser: {\n                        OR: [\n                            {name: {contains: search, mode: 'insensitive'}},\n                            {email: {contains: search, mode: 'insensitive'}}\n                        ]\n                    }\n                },\n                {action: {contains: search, mode: 'insensitive'}},\n                // Search in preserved user data\n                {\n                    details: {\n                        path: ['_preservedUser', 'email'],\n                        string_contains: search\n                    }\n                },\n                {\n                    details: {\n                        path: ['_preservedUser', 'name'],\n                        string_contains: search\n                    }\n                },\n                {\n                    details: {\n                        path: ['_preservedTargetUser', 'email'],\n                        string_contains: search\n                    }\n                },\n                {\n                    details: {\n                        path: ['_preservedTargetUser', 'name'],\n                        string_contains: search\n                    }\n                }\n            ]\n            // console.log('🦖 Search filter applied:', search);\n        }\n\n        // If exporting, return all matching records as CSV\n        if (isExport) {\n            const logs = await db.auditLog.findMany({\n                where,\n                orderBy: {createdAt: 'desc'},\n                include: {\n                    user: {\n                        select: {\n                            name: true,\n                            email: true\n                        }\n                    },\n                    targetUser: {\n                        select: {\n                            name: true,\n                            email: true\n                        }\n                    }\n                }\n            }) as DatabaseAuditLog[]\n\n            const csvContent = convertLogsToCSV(logs);\n\n            return new NextResponse(csvContent, {\n                headers: {\n                    'Content-Type': 'text/csv',\n                    'Content-Disposition': `attachment; filename=\"audit-logs-${new Date().toISOString().split('T')[0]}.csv\"`\n                }\n            }) as NextResponse<AuditLogsResponse | { error: string }>\n        }\n\n        // Get total count for pagination\n        const total = await db.auditLog.count({where})\n        // console.log(`🦖 Total audit logs matching filters: ${total}`);\n\n        // Determine fetching method: chunking or pagination\n        if (useChunking) {\n            let logs: DatabaseAuditLog[];\n\n            if (cursor) {\n                // console.log(`🦖 Using cursor-based fetch with cursor: ${cursor.substring(0, 10)}...`);\n\n                // Get the cursor record to understand its position\n                const cursorRecord = await db.auditLog.findUnique({\n                    where: {id: cursor},\n                    select: {createdAt: true, id: true}\n                });\n\n                if (!cursorRecord) {\n                    // If cursor doesn't exist, fall back to initial fetch\n                    // console.log('🦖 Cursor not found, falling back to initial fetch');\n                    logs = await db.auditLog.findMany({\n                        where,\n                        take: chunkSize,\n                        orderBy: [\n                            {createdAt: 'desc'},\n                            {id: 'desc'}\n                        ],\n                        include: {\n                            user: {select: {name: true, email: true}},\n                            targetUser: {select: {name: true, email: true}}\n                        }\n                    }) as DatabaseAuditLog[]\n                } else {\n                    // Get records before this cursor (older records in desc order)\n                    logs = await db.auditLog.findMany({\n                        where: {\n                            ...where,\n                            OR: [\n                                {\n                                    createdAt: {\n                                        lt: cursorRecord.createdAt\n                                    }\n                                },\n                                {\n                                    createdAt: cursorRecord.createdAt,\n                                    id: {\n                                        lt: cursor\n                                    }\n                                }\n                            ]\n                        },\n                        take: chunkSize,\n                        orderBy: [\n                            {createdAt: 'desc'},\n                            {id: 'desc'}\n                        ],\n                        include: {\n                            user: {select: {name: true, email: true}},\n                            targetUser: {select: {name: true, email: true}}\n                        }\n                    }) as DatabaseAuditLog[]\n                }\n            } else {\n                // console.log(`🦖 Initial fetch without cursor`);\n                // Initial fetch without cursor\n                logs = await db.auditLog.findMany({\n                    where,\n                    take: chunkSize,\n                    orderBy: [\n                        {createdAt: 'desc'},\n                        {id: 'desc'}\n                    ],\n                    include: {\n                        user: {select: {name: true, email: true}},\n                        targetUser: {select: {name: true, email: true}}\n                    }\n                }) as DatabaseAuditLog[]\n            }\n\n            console.log(`🦖 Fetched ${logs.length} logs`);\n\n            // Process logs to include preserved user info\n            const processedLogs = processLogs(logs);\n\n            // Determine next cursor and hasMore\n            const nextCursor = logs.length === chunkSize && logs.length > 0 ? logs[logs.length - 1].id : null;\n            const hasMore = logs.length === chunkSize;\n\n            console.log(`🦖 NextCursor: ${nextCursor ? nextCursor.substring(0, 10) + '...' : 'none'}, HasMore: ${hasMore}`);\n\n            return NextResponse.json({\n                logs: processedLogs,\n                total,\n                pages: Math.ceil(total / chunkSize),\n                nextCursor,\n                hasMore,\n                currentCount: logs.length\n            })\n        } else {\n            // Standard pagination\n            const skip = (page - 1) * pageSize\n\n            const logs = await db.auditLog.findMany({\n                where,\n                take: pageSize,\n                skip,\n                orderBy: [\n                    {createdAt: 'desc'},\n                    {id: 'desc'}\n                ],\n                include: {\n                    user: {select: {name: true, email: true}},\n                    targetUser: {select: {name: true, email: true}}\n                }\n            }) as DatabaseAuditLog[]\n\n            // Process logs to include preserved user info\n            const processedLogs = processLogs(logs);\n\n            return NextResponse.json({\n                logs: processedLogs,\n                total,\n                pages: Math.ceil(total / pageSize),\n                nextCursor: null,\n                hasMore: skip + pageSize < total,\n                currentCount: logs.length\n            })\n        }\n    } catch (error) {\n        // console.error('🦖 Audit logs fetch error:', error)\n        return NextResponse.json(\n            {\n                error: 'Failed to fetch audit logs',\n                details: error instanceof Error ? error.message : 'Unknown error'\n            },\n            {status: 500}\n        )\n    }\n}"
  },
  {
    "path": "app/api/admin/config/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server'\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog'\nimport {createAuditLog} from '@/lib/utils/auditLog'\nimport {db} from '@/lib/db'\nimport {z} from 'zod'\nimport {TelemetryService} from '@/lib/services/telemetry/service'\nimport {TelemetryState} from '@prisma/client'\nimport {SponsorService} from '@/lib/services/sponsor/service'\nimport {notifyAgent} from '@/lib/custom-domains/ssl/webhook'\n\nfunction buildConfigSchema(sponsored: boolean) {\n    return z.object({\n        defaultInvitationExpiry: z.number().min(1).max(30),\n        requireApprovalForChangelogs: z.boolean(),\n        maxChangelogEntriesPerProject: z.number().min(10).max(sponsored ? 999999 : 10000),\n        enableAnalytics: z.boolean(),\n        enableNotifications: z.boolean(),\n        allowTelemetry: z.enum(['prompt', 'enabled', 'disabled']),\n        adminOnlyApiKeyCreation: z.boolean(),\n        timezone: z.string().min(1).max(100).default('UTC'),\n        allowUserTimezone: z.boolean(),\n        customDateTemplates: z.array(z.object({\n            format: z.string().min(1).max(200),\n            label: z.string().min(1).max(100),\n        })).nullable().optional(),\n        panelIpWhitelistEnabled: z.boolean().default(false),\n        panelIpWhitelist: z.array(z.string()).default([]),\n    })\n}\n\n// Helper functions to map telemetry states\nfunction mapTelemetryStateToString(state: TelemetryState): 'prompt' | 'enabled' | 'disabled' {\n    switch (state) {\n        case TelemetryState.PROMPT:\n            return 'prompt';\n        case TelemetryState.ENABLED:\n            return 'enabled';\n        case TelemetryState.DISABLED:\n            return 'disabled';\n        default:\n            return 'prompt';\n    }\n}\n\nfunction mapStringToTelemetryState(state: 'prompt' | 'enabled' | 'disabled'): TelemetryState {\n    switch (state) {\n        case 'prompt':\n            return TelemetryState.PROMPT;\n        case 'enabled':\n            return TelemetryState.ENABLED;\n        case 'disabled':\n            return TelemetryState.DISABLED;\n        default:\n            return TelemetryState.PROMPT;\n    }\n}\n\n/**\n * @method GET\n * @description Fetches the system configuration for the authenticated user\n */\nexport async function GET() {\n    try {\n        const user = await validateAuthAndGetUser()\n\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                {error: 'Not authorized'},\n                {status: 403}\n            )\n        }\n\n        const config = await db.systemConfig.findFirst()\n\n        if (!config) {\n            // Return default configuration if none exists\n            return NextResponse.json({\n                defaultInvitationExpiry: 7,\n                requireApprovalForChangelogs: true,\n                maxChangelogEntriesPerProject: 100,\n                enableAnalytics: true,\n                enableNotifications: true,\n                allowTelemetry: 'prompt',\n                adminOnlyApiKeyCreation: false,\n                timezone: 'UTC',\n                allowUserTimezone: true,\n                customDateTemplates: null,\n                nginxAgentConfigured: !!(process.env.INTERNAL_API_SECRET && process.env.NGINX_AGENT_URL),\n            })\n        }\n\n        // Check sponsor/license status\n        const sponsorStatus = await SponsorService.getLicenseStatus()\n\n        // Map database telemetry state to frontend format\n        const mappedConfig = {\n            defaultInvitationExpiry: config.defaultInvitationExpiry,\n            requireApprovalForChangelogs: config.requireApprovalForChangelogs,\n            maxChangelogEntriesPerProject: config.maxChangelogEntriesPerProject,\n            enableAnalytics: config.enableAnalytics,\n            enableNotifications: config.enableNotifications,\n            allowTelemetry: mapTelemetryStateToString(config.allowTelemetry),\n            adminOnlyApiKeyCreation: config.adminOnlyApiKeyCreation,\n            timezone: config.timezone,\n            allowUserTimezone: config.allowUserTimezone,\n            customDateTemplates: config.customDateTemplates as { format: string; label: string }[] | null,\n            sponsorActive: sponsorStatus.active,\n            telemetryInstanceId: config.telemetryInstanceId,\n            panelIpWhitelistEnabled: config.panelIpWhitelistEnabled,\n            panelIpWhitelist: config.panelIpWhitelist,\n            nginxAgentConfigured: !!(process.env.INTERNAL_API_SECRET && process.env.NGINX_AGENT_URL),\n        }\n\n        return NextResponse.json(mappedConfig)\n    } catch (error) {\n        console.error('Error fetching system configuration:', error)\n        return NextResponse.json(\n            {error: 'Failed to fetch system configuration'},\n            {status: 500}\n        )\n    }\n}\n\n/**\n * @method PATCH\n * @description Updates the system configuration for the authenticated user\n */\nexport async function PATCH(request: NextRequest) {\n    try {\n        const user = await validateAuthAndGetUser()\n\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                {error: 'Not authorized'},\n                {status: 403}\n            )\n        }\n\n        // Extract the requesting admin's IP for the whitelist fail-safe\n        const adminIp = request.headers.get('x-forwarded-for')?.split(',')[0].trim()\n            || request.headers.get('x-real-ip')\n            || null\n\n        const body = await request.json()\n\n        const sponsorStatus = await SponsorService.getLicenseStatus()\n        const systemConfigSchema = buildConfigSchema(sponsorStatus.active)\n        const validatedData = systemConfigSchema.parse(body)\n\n        // Fail-safe: if enabling the whitelist, ensure the admin's own IP is included.\n        // This prevents an admin from accidentally locking themselves out.\n        let whitelistAutoAdded = false\n        if (validatedData.panelIpWhitelistEnabled && adminIp) {\n            const alreadyIncluded = validatedData.panelIpWhitelist.some(entry => {\n                // Simple exact-match check (CIDR ranges handled by the middleware)\n                return entry === adminIp || entry.startsWith(adminIp + '/')\n            })\n            if (!alreadyIncluded) {\n                validatedData.panelIpWhitelist = [...validatedData.panelIpWhitelist, adminIp]\n                whitelistAutoAdded = true\n            }\n        }\n\n        // Get current config to track changes\n        const existingConfig = await db.systemConfig.findFirst()\n        const isNewConfig = !existingConfig\n\n        // Track what changes are being made\n        const changes: Record<string, { from: unknown; to: unknown }> = {};\n\n        if (existingConfig) {\n            // Compare each field and track changes\n            if (validatedData.defaultInvitationExpiry !== existingConfig.defaultInvitationExpiry) {\n                changes.defaultInvitationExpiry = {\n                    from: existingConfig.defaultInvitationExpiry,\n                    to: validatedData.defaultInvitationExpiry\n                }\n            }\n\n            if (validatedData.requireApprovalForChangelogs !== existingConfig.requireApprovalForChangelogs) {\n                changes.requireApprovalForChangelogs = {\n                    from: existingConfig.requireApprovalForChangelogs,\n                    to: validatedData.requireApprovalForChangelogs\n                }\n            }\n\n            if (validatedData.maxChangelogEntriesPerProject !== existingConfig.maxChangelogEntriesPerProject) {\n                changes.maxChangelogEntriesPerProject = {\n                    from: existingConfig.maxChangelogEntriesPerProject,\n                    to: validatedData.maxChangelogEntriesPerProject\n                }\n            }\n\n            if (validatedData.enableAnalytics !== existingConfig.enableAnalytics) {\n                changes.enableAnalytics = {\n                    from: existingConfig.enableAnalytics,\n                    to: validatedData.enableAnalytics\n                }\n            }\n\n            if (validatedData.enableNotifications !== existingConfig.enableNotifications) {\n                changes.enableNotifications = {\n                    from: existingConfig.enableNotifications,\n                    to: validatedData.enableNotifications\n                }\n            }\n\n            const currentTelemetryState = mapTelemetryStateToString(existingConfig.allowTelemetry)\n            if (validatedData.allowTelemetry !== currentTelemetryState) {\n                changes.allowTelemetry = {\n                    from: currentTelemetryState,\n                    to: validatedData.allowTelemetry\n                }\n            }\n\n            if (validatedData.timezone !== existingConfig.timezone) {\n                changes.timezone = {\n                    from: existingConfig.timezone,\n                    to: validatedData.timezone\n                }\n            }\n\n            if (validatedData.allowUserTimezone !== existingConfig.allowUserTimezone) {\n                changes.allowUserTimezone = {\n                    from: existingConfig.allowUserTimezone,\n                    to: validatedData.allowUserTimezone\n                }\n            }\n\n            if (validatedData.panelIpWhitelistEnabled !== existingConfig.panelIpWhitelistEnabled) {\n                changes.panelIpWhitelistEnabled = {\n                    from: existingConfig.panelIpWhitelistEnabled,\n                    to: validatedData.panelIpWhitelistEnabled\n                }\n            }\n\n            if (JSON.stringify([...validatedData.panelIpWhitelist].sort()) !==\n                JSON.stringify([...existingConfig.panelIpWhitelist].sort())) {\n                changes.panelIpWhitelist = {\n                    from: existingConfig.panelIpWhitelist,\n                    to: validatedData.panelIpWhitelist\n                }\n            }\n        }\n\n        // Handle telemetry configuration changes BEFORE updating the database\n        if (changes.allowTelemetry) {\n            try {\n                console.log(`Telemetry state changing from ${changes.allowTelemetry.from} to ${changes.allowTelemetry.to}`)\n\n                const telemetryConfig = await TelemetryService.getTelemetryConfig()\n\n                if (validatedData.allowTelemetry === 'enabled') {\n                    if (!telemetryConfig.instanceId) {\n                        console.log('Enabling telemetry - registering new instance')\n                        // Register instance if enabling telemetry for the first time\n                        const instanceId = await TelemetryService.registerInstance()\n                        console.log('Telemetry enabled with new instance ID:', instanceId)\n\n                        // Update telemetry config with new instance ID\n                        await TelemetryService.updateTelemetryConfig({\n                            allowTelemetry: 'enabled',\n                            instanceId\n                        })\n                    } else if (changes.allowTelemetry.from === 'disabled') {\n                        console.log('Re-enabling telemetry - reactivating instance:', telemetryConfig.instanceId)\n                        // Reactivate existing instance\n                        try {\n                            await TelemetryService.reactivateInstance(telemetryConfig.instanceId)\n                            console.log('Instance reactivated successfully')\n                        } catch (reactivationError) {\n                            console.warn('Failed to reactivate instance, but continuing:', reactivationError)\n                        }\n\n                        // Update telemetry config\n                        await TelemetryService.updateTelemetryConfig({\n                            allowTelemetry: 'enabled',\n                            instanceId: telemetryConfig.instanceId\n                        })\n                        console.log('Telemetry reactivated for instance:', telemetryConfig.instanceId)\n                    } else {\n                        console.log('Telemetry already enabled, just updating config')\n                        await TelemetryService.updateTelemetryConfig({\n                            allowTelemetry: 'enabled',\n                            instanceId: telemetryConfig.instanceId\n                        })\n                    }\n                } else if (validatedData.allowTelemetry === 'disabled' && telemetryConfig.instanceId) {\n                    console.log('Disabling telemetry - deactivating instance:', telemetryConfig.instanceId)\n                    // Deactivate instance if disabling telemetry\n                    try {\n                        await TelemetryService.deactivateInstance(telemetryConfig.instanceId)\n                        console.log('Instance deactivated successfully')\n                    } catch (deactivationError) {\n                        console.warn('Failed to deactivate instance:', deactivationError)\n                    }\n\n                    await TelemetryService.updateTelemetryConfig({\n                        allowTelemetry: 'disabled',\n                        instanceId: telemetryConfig.instanceId\n                    })\n                    console.log('Telemetry disabled')\n                } else {\n                    console.log('Updating telemetry state to:', validatedData.allowTelemetry)\n                    // Just update the state for prompt mode\n                    await TelemetryService.updateTelemetryConfig({\n                        allowTelemetry: validatedData.allowTelemetry,\n                        instanceId: telemetryConfig.instanceId\n                    })\n                }\n            } catch (telemetryError) {\n                console.error('Failed to update telemetry configuration:', telemetryError)\n\n                // Create audit log for telemetry update failure\n                try {\n                    await createAuditLog(\n                        'TELEMETRY_UPDATE_FAILURE',\n                        user.id,\n                        user.id,\n                        {\n                            requestedState: validatedData.allowTelemetry,\n                            error: telemetryError instanceof Error ? telemetryError.message : 'Unknown error'\n                        }\n                    );\n                } catch (auditLogError) {\n                    console.error('Failed to create telemetry failure audit log:', auditLogError);\n                }\n\n                return NextResponse.json(\n                    {\n                        error: 'Failed to update telemetry configuration',\n                        details: telemetryError instanceof Error ? telemetryError.message : 'Unknown error'\n                    },\n                    {status: 500}\n                );\n            }\n        }\n\n        // Map telemetry state to database enum\n        const dbTelemetryState = mapStringToTelemetryState(validatedData.allowTelemetry)\n\n        // Prepare data for database update\n        const configData = {\n            defaultInvitationExpiry: validatedData.defaultInvitationExpiry,\n            requireApprovalForChangelogs: validatedData.requireApprovalForChangelogs,\n            maxChangelogEntriesPerProject: validatedData.maxChangelogEntriesPerProject,\n            enableAnalytics: validatedData.enableAnalytics,\n            enableNotifications: validatedData.enableNotifications,\n            allowTelemetry: dbTelemetryState,\n            adminOnlyApiKeyCreation: validatedData.adminOnlyApiKeyCreation,\n            timezone: validatedData.timezone,\n            allowUserTimezone: validatedData.allowUserTimezone,\n            customDateTemplates: validatedData.customDateTemplates ?? undefined,\n            panelIpWhitelistEnabled: validatedData.panelIpWhitelistEnabled,\n            panelIpWhitelist: validatedData.panelIpWhitelist,\n        }\n\n        // Update the system config in database\n        const config = await db.systemConfig.upsert({\n            where: {id: 1},\n            update: configData,\n            create: {\n                id: 1,\n                ...configData,\n            },\n        })\n\n        // Create appropriate audit log based on operation\n        try {\n            if (isNewConfig) {\n                // This is the initial configuration\n                await createAuditLog(\n                    'CREATE_SYSTEM_CONFIG',\n                    user.id,\n                    user.id,\n                    {\n                        config: {\n                            defaultInvitationExpiry: config.defaultInvitationExpiry,\n                            requireApprovalForChangelogs: config.requireApprovalForChangelogs,\n                            maxChangelogEntriesPerProject: config.maxChangelogEntriesPerProject,\n                            enableAnalytics: config.enableAnalytics,\n                            enableNotifications: config.enableNotifications,\n                            allowTelemetry: mapTelemetryStateToString(config.allowTelemetry),\n                        }\n                    }\n                );\n            } else if (Object.keys(changes).length > 0) {\n                // This is an update to existing configuration\n                await createAuditLog(\n                    'UPDATE_SYSTEM_CONFIG',\n                    user.id,\n                    user.id,\n                    {\n                        changes,\n                        changeCount: Object.keys(changes).length\n                    }\n                );\n            }\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n            // Continue execution even if audit log creation fails\n        }\n\n        // Return the updated config with mapped telemetry state\n        const responseConfig = {\n            defaultInvitationExpiry: config.defaultInvitationExpiry,\n            requireApprovalForChangelogs: config.requireApprovalForChangelogs,\n            maxChangelogEntriesPerProject: config.maxChangelogEntriesPerProject,\n            enableAnalytics: config.enableAnalytics,\n            enableNotifications: config.enableNotifications,\n            allowTelemetry: mapTelemetryStateToString(config.allowTelemetry),\n            adminOnlyApiKeyCreation: config.adminOnlyApiKeyCreation,\n            timezone: config.timezone,\n            allowUserTimezone: config.allowUserTimezone,\n            customDateTemplates: config.customDateTemplates as { format: string; label: string }[] | null,\n            panelIpWhitelistEnabled: config.panelIpWhitelistEnabled,\n            panelIpWhitelist: config.panelIpWhitelist,\n        }\n\n        // Notify nginx-agent if the IP whitelist changed (fire-and-forget — never fail the request)\n        const whitelistChanged = changes.panelIpWhitelistEnabled !== undefined\n            || changes.panelIpWhitelist !== undefined\n            || isNewConfig\n        if (whitelistChanged) {\n            notifyAgent({\n                event: 'ip_whitelist.updated',\n                enabled: config.panelIpWhitelistEnabled,\n                whitelist: config.panelIpWhitelist,\n            }).catch(err => console.warn('[ip-whitelist] Failed to notify nginx-agent:', err))\n        }\n\n        console.log('System configuration updated successfully')\n        return NextResponse.json({\n            ...responseConfig,\n            ...(whitelistAutoAdded && {\n                _warning: `Your current IP (${adminIp}) was automatically added to the whitelist to prevent lockout.`\n            })\n        })\n\n    } catch (error) {\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {error: 'Invalid configuration data', details: error.errors},\n                {status: 400}\n            )\n        }\n\n        console.error('Error updating system configuration:', error)\n        return NextResponse.json(\n            {error: 'Failed to update system configuration'},\n            {status: 500}\n        )\n    }\n}"
  },
  {
    "path": "app/api/admin/config/system-email/route.ts",
    "content": "// app/api/admin/config/system-email/route.ts\nimport { NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { createAuditLog } from '@/lib/utils/auditLog'; // Add this import\nimport { db } from '@/lib/db';\nimport { z } from 'zod';\nimport { createTransport } from 'nodemailer';\nimport SMTPTransport from 'nodemailer/lib/smtp-transport';\n\n// Validation schema for system email settings\nconst systemEmailSchema = z.object({\n    enablePasswordReset: z.boolean(),\n    smtpHost: z.string().min(1, 'SMTP host is required'),\n    smtpPort: z.coerce.number().int().min(1).max(65535),\n    smtpUser: z.string().optional().nullable(),\n    smtpPassword: z.string().optional().nullable(),\n    smtpSecure: z.boolean().default(true),\n    systemEmail: z.string().email('Invalid email address'),\n});\n\n/**\n * @method GET\n * @description Retrieves the system email configuration\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"enablePasswordReset\": { \"type\": \"boolean\" },\n *     \"smtpHost\": { \"type\": \"string\" },\n *     \"smtpPort\": { \"type\": \"number\" },\n *     \"smtpUser\": { \"type\": \"string\" },\n *     \"smtpSecure\": { \"type\": \"boolean\" },\n *     \"systemEmail\": { \"type\": \"string\" }\n *   }\n * }\n * @error 403 Unauthorized - User does not have 'ADMIN' role\n * @error 500 An unexpected error occurred while fetching system email configuration\n */\nexport async function GET() {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                { error: 'Not authorized' },\n                { status: 403 }\n            );\n        }\n\n        const config = await db.systemConfig.findFirst({\n            where: { id: 1 },\n            select: {\n                enablePasswordReset: true,\n                smtpHost: true,\n                smtpPort: true,\n                smtpUser: true,\n                smtpSecure: true,\n                systemEmail: true\n            }\n        });\n\n        // Create audit log for viewing system email config\n        try {\n            await createAuditLog(\n                'VIEW_SYSTEM_EMAIL_CONFIG',\n                user.id,\n                user.id, // Use admin's ID as target to avoid foreign key issues\n                {\n                    configExists: !!config\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n            // Continue execution even if audit log creation fails\n        }\n\n        if (!config) {\n            // Return default configuration if none exists\n            return NextResponse.json({\n                enablePasswordReset: false,\n                smtpHost: '',\n                smtpPort: 587,\n                smtpUser: '',\n                smtpSecure: true,\n                systemEmail: ''\n            });\n        }\n\n        // Don't return password in the response for security reasons\n        return NextResponse.json(config);\n    } catch (error) {\n        console.error('Error fetching system email configuration:', error);\n        return NextResponse.json(\n            { error: 'Failed to fetch system email configuration' },\n            { status: 500 }\n        );\n    }\n}\n\n/**\n * @method POST\n * @description Updates the system email configuration\n * @body {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"enablePasswordReset\": { \"type\": \"boolean\" },\n *     \"smtpHost\": { \"type\": \"string\" },\n *     \"smtpPort\": { \"type\": \"number\" },\n *     \"smtpUser\": { \"type\": \"string\" },\n *     \"smtpPassword\": { \"type\": \"string\" },\n *     \"smtpSecure\": { \"type\": \"boolean\" },\n *     \"systemEmail\": { \"type\": \"string\" }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\" },\n *     \"message\": { \"type\": \"string\" }\n *   }\n * }\n * @error 400 Invalid input - Validation error\n * @error 403 Unauthorized - User does not have 'ADMIN' role\n * @error 500 An unexpected error occurred while updating system email configuration\n */\nexport async function POST(request: Request) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                { error: 'Not authorized' },\n                { status: 403 }\n            );\n        }\n\n        // Parse and validate request body\n        const body = await request.json();\n        const validatedData = systemEmailSchema.parse(body);\n\n        // Check if config exists\n        const existingConfig = await db.systemConfig.findFirst({\n            where: { id: 1 }\n        });\n\n        // Track what changes are being made\n        const changes: Record<string, { from: unknown; to: unknown }> = {};\n        const isNewConfig = !existingConfig;\n\n        if (existingConfig) {\n            // Compare each field and track changes\n            if (validatedData.enablePasswordReset !== existingConfig.enablePasswordReset) {\n                changes.enablePasswordReset = {\n                    from: existingConfig.enablePasswordReset,\n                    to: validatedData.enablePasswordReset\n                };\n            }\n\n            if (validatedData.smtpHost !== existingConfig.smtpHost) {\n                changes.smtpHost = {\n                    from: existingConfig.smtpHost,\n                    to: validatedData.smtpHost\n                };\n            }\n\n            if (validatedData.smtpPort !== existingConfig.smtpPort) {\n                changes.smtpPort = {\n                    from: existingConfig.smtpPort,\n                    to: validatedData.smtpPort\n                };\n            }\n\n            if (validatedData.smtpUser !== existingConfig.smtpUser) {\n                changes.smtpUser = {\n                    from: existingConfig.smtpUser,\n                    to: validatedData.smtpUser\n                };\n            }\n\n            if (validatedData.smtpPassword) {\n                // Don't log actual password values, just note that it was changed\n                changes.smtpPassword = {\n                    from: '********',\n                    to: '********'\n                };\n            }\n\n            if (validatedData.smtpSecure !== existingConfig.smtpSecure) {\n                changes.smtpSecure = {\n                    from: existingConfig.smtpSecure,\n                    to: validatedData.smtpSecure\n                };\n            }\n\n            if (validatedData.systemEmail !== existingConfig.systemEmail) {\n                changes.systemEmail = {\n                    from: existingConfig.systemEmail,\n                    to: validatedData.systemEmail\n                };\n            }\n        }\n\n        // Prepare data for update or create\n        const data = {\n            ...validatedData,\n            // Handle null/empty values for optional fields\n            smtpUser: validatedData.smtpUser || '',\n            // Handle password update logic:\n            // - If password is provided in the request, use it\n            // - If not and there's an existing password, keep it\n            // - Otherwise use empty string\n            smtpPassword: validatedData.smtpPassword\n                ? validatedData.smtpPassword\n                : (existingConfig?.smtpPassword || '')\n        };\n\n        // Update or create system config\n        const updatedConfig = await db.systemConfig.upsert({\n            where: { id: 1 },\n            update: data,\n            create: {\n                id: 1,\n                ...data,\n                // Set default values for other required fields\n                defaultInvitationExpiry: 7,\n                requireApprovalForChangelogs: true,\n                maxChangelogEntriesPerProject: 100,\n                enableAnalytics: true,\n                enableNotifications: true\n            }\n        });\n\n        // Create appropriate audit log based on operation\n        try {\n            if (isNewConfig) {\n                // This is the initial email configuration\n                await createAuditLog(\n                    'CREATE_SYSTEM_EMAIL_CONFIG',\n                    user.id,\n                    user.id, // Use admin's ID as target to avoid foreign key issues\n                    {\n                        // Don't include actual credentials in audit log\n                        config: {\n                            enablePasswordReset: updatedConfig.enablePasswordReset,\n                            smtpHost: updatedConfig.smtpHost,\n                            smtpPort: updatedConfig.smtpPort,\n                            smtpUser: updatedConfig.smtpUser ? '(configured)' : '(not set)',\n                            smtpSecure: updatedConfig.smtpSecure,\n                            systemEmail: updatedConfig.systemEmail,\n                        }\n                    }\n                );\n            } else if (Object.keys(changes).length > 0) {\n                // This is an update to existing configuration\n                await createAuditLog(\n                    'UPDATE_SYSTEM_EMAIL_CONFIG',\n                    user.id,\n                    user.id, // Use admin's ID as target to avoid foreign key issues\n                    {\n                        changes,\n                        changeCount: Object.keys(changes).length\n                    }\n                );\n            }\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n            // Continue execution even if audit log creation fails\n        }\n\n        return NextResponse.json({\n            success: true,\n            message: 'System email configuration updated successfully'\n        });\n    } catch (error) {\n        console.error('Failed to update system email configuration:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: 'Validation failed', details: error.errors },\n                { status: 400 }\n            );\n        }\n\n        return NextResponse.json(\n            { error: 'Failed to update system email configuration' },\n            { status: 500 }\n        );\n    }\n}\n\n/**\n * @method PATCH\n * @description Tests the system email configuration by sending a test email\n * @path /api/admin/config/system-email/test\n * @body {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"smtpHost\": { \"type\": \"string\" },\n *     \"smtpPort\": { \"type\": \"number\" },\n *     \"smtpUser\": { \"type\": \"string\" },\n *     \"smtpPassword\": { \"type\": \"string\" },\n *     \"smtpSecure\": { \"type\": \"boolean\" },\n *     \"systemEmail\": { \"type\": \"string\" },\n *     \"testEmail\": { \"type\": \"string\" }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\" },\n *     \"message\": { \"type\": \"string\" }\n *   }\n * }\n * @error 400 Invalid input - Validation error\n * @error 403 Unauthorized - User does not have 'ADMIN' role\n * @error 500 An unexpected error occurred while testing system email configuration\n */\nexport async function PATCH(request: Request) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                { error: 'Not authorized' },\n                { status: 403 }\n            );\n        }\n\n        // Parse and validate request body for test email\n        const body = await request.json();\n        const testEmailSchema = systemEmailSchema.extend({\n            testEmail: z.string().email('Invalid test email address')\n        });\n\n        const validatedData = testEmailSchema.parse(body);\n\n        // Create audit log for test attempt before sending (in case of failure)\n        try {\n            await createAuditLog(\n                'TEST_SYSTEM_EMAIL_CONFIG',\n                user.id,\n                user.id, // Use admin's ID as target to avoid foreign key issues\n                {\n                    smtpHost: validatedData.smtpHost,\n                    smtpPort: validatedData.smtpPort,\n                    smtpUser: validatedData.smtpUser ? '(configured)' : '(not set)',\n                    smtpSecure: validatedData.smtpSecure,\n                    systemEmail: validatedData.systemEmail,\n                    testEmail: validatedData.testEmail\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n            // Continue execution even if audit log creation fails\n        }\n\n        // Set up transporter for test\n        const transporterOptions: SMTPTransport.Options = {\n            host: validatedData.smtpHost,\n            port: validatedData.smtpPort,\n            secure: validatedData.smtpSecure,\n            auth: validatedData.smtpUser\n                ? {\n                    user: validatedData.smtpUser,\n                    pass: validatedData.smtpPassword || ''\n                }\n                : undefined,\n            tls: {\n                rejectUnauthorized: validatedData.smtpSecure,\n            }\n        };\n\n        const transporter = createTransport(transporterOptions);\n\n        // Send test email\n        const info = await transporter.sendMail({\n            from: `\"Changerawr System\" <${validatedData.systemEmail}>`,\n            to: validatedData.testEmail,\n            subject: \"Test Email from Changerawr System\",\n            text: \"This is a test email from the Changerawr system email configuration.\",\n            html: `\n                <div style=\"font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;\">\n                    <h1 style=\"color: #333;\">Test Email from Changerawr System</h1>\n                    <p>This is a test email from the Changerawr system email configuration.</p>\n                    <p>If you're receiving this, the SMTP configuration is working correctly.</p>\n                    <hr style=\"border: 1px solid #eee; margin: 20px 0;\" />\n                    <p style=\"color: #666; font-size: 12px;\">This is an automated test email.</p>\n                </div>\n            `\n        });\n\n        // Update the audit log or create a new one for successful test\n        try {\n            await createAuditLog(\n                'SYSTEM_EMAIL_TEST_SUCCESS',\n                user.id,\n                user.id, // Use admin's ID as target to avoid foreign key issues\n                {\n                    messageId: info.messageId,\n                    recipient: validatedData.testEmail\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create success audit log:', auditLogError);\n            // Continue execution even if audit log creation fails\n        }\n\n        return NextResponse.json({\n            success: true,\n            message: 'Test email sent successfully'\n        });\n    } catch (error) {\n        console.error('Failed to test system email configuration:', error);\n\n        // Create audit log for test failure\n        try {\n            const user = await validateAuthAndGetUser();\n            await createAuditLog(\n                'SYSTEM_EMAIL_TEST_FAILURE',\n                user.id,\n                user.id, // Use admin's ID as target to avoid foreign key issues\n                {\n                    error: error instanceof Error ? error.message : 'Unknown error'\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create failure audit log:', auditLogError);\n        }\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: 'Validation failed', details: error.errors },\n                { status: 400 }\n            );\n        }\n\n        const errorMessage = error instanceof Error ? error.message : 'Unknown error';\n        return NextResponse.json(\n            {\n                success: false,\n                message: 'Failed to send test email',\n                error: errorMessage\n            },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/admin/dashboard/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog'\n\n/**\n * @method GET\n * @description Fetches dashboard metrics for the authenticated user\n * @description Validates that the authenticated user has 'ADMIN' role\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"userCount\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"total\": { \"type\": \"number\" },\n *         \"admins\": { \"type\": \"number\" },\n *         \"staff\": { \"type\": \"number\" }\n *       }\n *     },\n *     \"invitations\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"total\": { \"type\": \"number\" },\n *         \"pending\": { \"type\": \"number\" }\n *       }\n *     },\n *     \"changelog\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"totalEntries\": { \"type\": \"number\" },\n *         \"entriesThisMonth\": { \"type\": \"number\" }\n *       }\n *     },\n *     \"systemHealth\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"databaseConnected\": { \"type\": \"boolean\" },\n *         \"lastDataSync\": { \"type\": \"string\", \"format\": \"date-time\" }\n *       }\n *     }\n *   }\n * }\n * @error 403 Unauthorized - User does not have 'ADMIN' role\n * @error 500 An unexpected error occurred while fetching dashboard data\n */\nexport async function GET() {\n    try {\n        // Validate that the user is an admin\n        const user = await validateAuthAndGetUser()\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })\n        }\n\n        // Calculate start of current month\n        const startOfMonth = new Date()\n        startOfMonth.setDate(1)\n        startOfMonth.setHours(0, 0, 0, 0)\n\n        // Fetch dashboard metrics\n        const [\n            userCount,\n            invitations,\n            changelog,\n            systemHealth\n        ] = await Promise.all([\n            // User counts excluding system users\n            db.user.groupBy({\n                by: ['role'],\n                where: {\n                    email: {\n                        not: {\n                            contains: '@changerawr.sys'\n                        }\n                    }\n                },\n                _count: {\n                    id: true\n                }\n            }),\n            // Invitation metrics\n            db.invitationLink.aggregate({\n                _count: {\n                    id: true\n                },\n                where: {\n                    usedAt: null,\n                    expiresAt: { gt: new Date() }\n                }\n            }),\n            // Changelog metrics\n            db.changelogEntry.aggregate({\n                _count: { id: true },\n                where: {\n                    publishedAt: { gte: startOfMonth }\n                }\n            }),\n            // System health (simple database connection check)\n            (async () => ({\n                databaseConnected: true,\n                lastDataSync: new Date().toISOString()\n            }))()\n        ])\n\n        // Aggregate user counts\n        const userCountData = {\n            total: userCount.reduce((sum, group) => sum + group._count.id, 0),\n            admins: userCount.find(group => group.role === 'ADMIN')?._count.id || 0,\n            staff: userCount.find(group => group.role === 'STAFF')?._count.id || 0\n        }\n\n        return NextResponse.json({\n            userCount: userCountData,\n            invitations: {\n                total: await db.invitationLink.count(),\n                pending: invitations._count.id\n            },\n            changelog: {\n                totalEntries: await db.changelogEntry.count(),\n                entriesThisMonth: changelog._count.id\n            },\n            systemHealth\n        })\n    } catch (error) {\n        console.error('Dashboard data fetch error:', error)\n        return NextResponse.json({ error: 'Failed to fetch dashboard data' }, { status: 500 })\n    }\n}"
  },
  {
    "path": "app/api/admin/oauth/providers/[id]/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { db } from '@/lib/db';\nimport { z } from 'zod';\nimport { OAuthProviderUpdateData } from '@/lib/types/oauth';\nimport { createAuditLog } from '@/lib/utils/auditLog';\n\nconst updateProviderSchema = z.object({\n    name: z.string().min(1, 'Name is required').optional(),\n    authorizationUrl: z.string().url('Authorization URL must be valid').optional(),\n    tokenUrl: z.string().url('Token URL must be valid').optional(),\n    userInfoUrl: z.string().url('User Info URL must be valid').optional(),\n    clientId: z.string().min(1, 'Client ID is required').optional(),\n    clientSecret: z.string().min(1, 'Client Secret is required').optional(),\n    scopes: z.array(z.string()).min(1, 'At least one scope is required').optional(),\n    enabled: z.boolean().optional(),\n    isDefault: z.boolean().optional(),\n    allowedEmailDomains: z.array(z.string()).optional(),\n    blockExistingUsers: z.boolean().optional(),\n    requiredClaims: z.record(z.string()).optional().nullable(),\n});\n\n/**\n * @method PATCH\n * @description Updates an existing OAuth provider with support for custom URLs\n * @param {string} id - The ID of the OAuth provider to update\n * @body {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"name\": { \"type\": \"string\" },\n *     \"authorizationUrl\": { \"type\": \"string\", \"format\": \"url\" },\n *     \"tokenUrl\": { \"type\": \"string\", \"format\": \"url\" },\n *     \"userInfoUrl\": { \"type\": \"string\", \"format\": \"url\" },\n *     \"clientId\": { \"type\": \"string\" },\n *     \"clientSecret\": { \"type\": \"string\" },\n *     \"scopes\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n *     \"enabled\": { \"type\": \"boolean\" },\n *     \"isDefault\": { \"type\": \"boolean\" }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"name\": { \"type\": \"string\" },\n *     \"enabled\": { \"type\": \"boolean\" },\n *     \"isDefault\": { \"type\": \"boolean\" },\n *     \"updatedAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *   }\n * }\n * @error 400 Invalid input - Validation error\n * @error 403 Unauthorized - User not authorized to access this endpoint\n * @error 404 Provider not found\n * @error 500 An unexpected error occurred while updating provider\n */\nexport async function PATCH(\n    request: Request,\n    { params }: { params: Promise<{ id: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        // Only admins can update providers\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                { error: 'Unauthorized' },\n                { status: 403 }\n            );\n        }\n\n        const { id } = await params;\n        const body = await request.json();\n        const validatedData = updateProviderSchema.parse(body);\n\n        // Check if provider exists\n        const provider = await db.oAuthProvider.findUnique({\n            where: { id }\n        });\n\n        if (!provider) {\n            return NextResponse.json(\n                { error: 'Provider not found' },\n                { status: 404 }\n            );\n        }\n\n        // Prepare update data\n        const updateData: OAuthProviderUpdateData = {};\n\n        if (validatedData.name !== undefined) {\n            updateData.name = validatedData.name;\n        }\n\n        if (validatedData.clientId !== undefined) {\n            updateData.clientId = validatedData.clientId;\n        }\n\n        if (validatedData.clientSecret !== undefined) {\n            updateData.clientSecret = validatedData.clientSecret;\n        }\n\n        if (validatedData.scopes !== undefined) {\n            updateData.scopes = validatedData.scopes;\n        }\n\n        if (validatedData.enabled !== undefined) {\n            updateData.enabled = validatedData.enabled;\n        }\n\n        if (validatedData.isDefault !== undefined) {\n            updateData.isDefault = validatedData.isDefault;\n\n            // If setting as default, unset any other defaults\n            if (validatedData.isDefault) {\n                await db.oAuthProvider.updateMany({\n                    where: { id: { not: id }, isDefault: true },\n                    data: { isDefault: false }\n                });\n            }\n        }\n\n        // Handle custom URL updates\n        if (validatedData.authorizationUrl !== undefined) {\n            updateData.authorizationUrl = validatedData.authorizationUrl;\n        }\n\n        if (validatedData.tokenUrl !== undefined) {\n            updateData.tokenUrl = validatedData.tokenUrl;\n        }\n\n        if (validatedData.userInfoUrl !== undefined) {\n            updateData.userInfoUrl = validatedData.userInfoUrl;\n        }\n\n        if (validatedData.allowedEmailDomains !== undefined) {\n            updateData.allowedEmailDomains = validatedData.allowedEmailDomains;\n        }\n\n        if (validatedData.blockExistingUsers !== undefined) {\n            updateData.blockExistingUsers = validatedData.blockExistingUsers;\n        }\n\n        if (validatedData.requiredClaims !== undefined) {\n            updateData.requiredClaims = validatedData.requiredClaims || {};\n        }\n\n        // Update callback URL if name changes\n        if (validatedData.name !== undefined) {\n            const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';\n            updateData.callbackUrl = `${appUrl}/api/auth/oauth/callback/${validatedData.name.toLowerCase().replace(/\\s+/g, '-')}`;\n        }\n\n        // Track changes for audit log\n        const changes: Record<string, { from: unknown; to: unknown }> = {};\n\n        if (validatedData.name && validatedData.name !== provider.name) {\n            changes.name = { from: provider.name, to: validatedData.name };\n        }\n\n        if (validatedData.authorizationUrl && validatedData.authorizationUrl !== provider.authorizationUrl) {\n            changes.authorizationUrl = { from: provider.authorizationUrl, to: validatedData.authorizationUrl };\n        }\n\n        if (validatedData.tokenUrl && validatedData.tokenUrl !== provider.tokenUrl) {\n            changes.tokenUrl = { from: provider.tokenUrl, to: validatedData.tokenUrl };\n        }\n\n        if (validatedData.userInfoUrl && validatedData.userInfoUrl !== provider.userInfoUrl) {\n            changes.userInfoUrl = { from: provider.userInfoUrl, to: validatedData.userInfoUrl };\n        }\n\n        if (validatedData.clientId && validatedData.clientId !== provider.clientId) {\n            changes.clientId = { from: provider.clientId, to: validatedData.clientId };\n        }\n\n        if (validatedData.enabled !== undefined && validatedData.enabled !== provider.enabled) {\n            changes.enabled = { from: provider.enabled, to: validatedData.enabled };\n        }\n\n        if (validatedData.isDefault !== undefined && validatedData.isDefault !== provider.isDefault) {\n            changes.isDefault = { from: provider.isDefault, to: validatedData.isDefault };\n        }\n\n        if (validatedData.scopes) {\n            const currentScopes = provider.scopes || [];\n            const newScopes = validatedData.scopes;\n\n            if (JSON.stringify(currentScopes.sort()) !== JSON.stringify(newScopes.sort())) {\n                changes.scopes = { from: currentScopes, to: newScopes };\n            }\n        }\n\n        // Update the provider\n        const updatedProvider = await db.oAuthProvider.update({\n            where: { id },\n            data: updateData\n        });\n\n        // Create audit log for the update\n        try {\n            await createAuditLog(\n                'UPDATE_OAUTH_PROVIDER',\n                user.id,\n                user.id,\n                {\n                    providerId: updatedProvider.id,\n                    providerName: updatedProvider.name,\n                    changes,\n                    changeCount: Object.keys(changes).length,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n        }\n\n        return NextResponse.json({\n            id: updatedProvider.id,\n            name: updatedProvider.name,\n            enabled: updatedProvider.enabled,\n            isDefault: updatedProvider.isDefault,\n            updatedAt: updatedProvider.updatedAt\n        });\n    } catch (error) {\n        console.error('Failed to update OAuth provider:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: 'Validation failed', details: error.errors },\n                { status: 400 }\n            );\n        }\n\n        return NextResponse.json(\n            { error: 'Failed to update OAuth provider' },\n            { status: 500 }\n        );\n    }\n}\n\n/**\n * @method DELETE\n * @description Deletes an OAuth provider\n * @param {string} id - The ID of the OAuth provider to delete\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\" }\n *   }\n * }\n * @error 403 Unauthorized - User not authorized to access this endpoint\n * @error 404 Provider not found\n * @error 500 An unexpected error occurred while deleting provider\n */\nexport async function DELETE(\n    request: Request,\n    { params }: { params: Promise<{ id: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        // Only admins can delete providers\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                { error: 'Unauthorized' },\n                { status: 403 }\n            );\n        }\n\n        const { id } = await params;\n\n        // Check if provider exists\n        const provider = await db.oAuthProvider.findUnique({\n            where: { id }\n        });\n\n        if (!provider) {\n            return NextResponse.json(\n                { error: 'Provider not found' },\n                { status: 404 }\n            );\n        }\n\n        // Store provider details for audit log before deletion\n        const providerDetails = {\n            id: provider.id,\n            name: provider.name,\n            authorizationUrl: provider.authorizationUrl,\n            tokenUrl: provider.tokenUrl,\n            userInfoUrl: provider.userInfoUrl,\n            enabled: provider.enabled,\n            isDefault: provider.isDefault\n        };\n\n        // Delete the provider\n        await db.oAuthProvider.delete({\n            where: { id }\n        });\n\n        // Create audit log for the deletion\n        try {\n            await createAuditLog(\n                'DELETE_OAUTH_PROVIDER',\n                user.id,\n                user.id,\n                {\n                    deletedProvider: providerDetails,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n        }\n\n        return NextResponse.json({ success: true });\n\n    } catch (error) {\n        console.error('Failed to delete OAuth provider:', error);\n        return NextResponse.json(\n            { error: 'Failed to delete OAuth provider' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/admin/oauth/providers/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { createAuditLog } from '@/lib/utils/auditLog';\nimport { db } from '@/lib/db';\nimport { z } from 'zod';\n\n// Enhanced schema to handle both preset and custom URL configurations\nconst providerSchema = z.object({\n    name: z.string().min(1, 'Name is required'),\n    clientId: z.string().min(1, 'Client ID is required'),\n    clientSecret: z.string().min(1, 'Client Secret is required'),\n    // Direct URL fields - these will be provided regardless of preset/custom mode\n    authorizationUrl: z.string().url('Authorization URL must be valid'),\n    tokenUrl: z.string().url('Token URL must be valid'),\n    userInfoUrl: z.string().url('User Info URL must be valid'),\n    scopes: z.array(z.string()).min(1, 'At least one scope is required'),\n    enabled: z.boolean().default(true),\n    isDefault: z.boolean().default(false),\n    allowedEmailDomains: z.array(z.string()).default([]),\n    blockExistingUsers: z.boolean().default(false),\n    requiredClaims: z.record(z.string()).optional().nullable(),\n});\n\n/**\n * @method GET\n * @description Retrieves all OAuth providers\n * @query {\n *   includeAll: boolean, default: false - Whether to include disabled providers\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"providers\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"name\": { \"type\": \"string\" },\n *           \"clientId\": { \"type\": \"string\" },\n *           \"clientSecret\": { \"type\": \"string\" },\n *           \"authorizationUrl\": { \"type\": \"string\" },\n *           \"tokenUrl\": { \"type\": \"string\" },\n *           \"userInfoUrl\": { \"type\": \"string\" },\n *           \"callbackUrl\": { \"type\": \"string\" },\n *           \"scopes\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n *           \"enabled\": { \"type\": \"boolean\" },\n *           \"isDefault\": { \"type\": \"boolean\" },\n *           \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *           \"updatedAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 403 Unauthorized - User not authorized to access this endpoint\n * @error 500 An unexpected error occurred while fetching providers\n */\nexport async function GET(request: Request) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        // Only admins can access this endpoint\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                { error: 'Unauthorized' },\n                { status: 403 }\n            );\n        }\n\n        const { searchParams } = new URL(request.url);\n        const includeAll = searchParams.get('includeAll') === 'true';\n\n        const providers = await db.oAuthProvider.findMany({\n            where: includeAll ? {} : { enabled: true },\n            orderBy: { name: 'asc' }\n        });\n\n        // Log the action of viewing OAuth providers\n        try {\n            await createAuditLog(\n                'VIEW_OAUTH_PROVIDERS',\n                user.id,\n                user.id,\n                {\n                    providerCount: providers.length,\n                    includeAll: includeAll\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n        }\n\n        // Never send secrets to the client — return a masked indicator instead\n        const safeProviders = providers.map(({ clientSecret, ...rest }) => ({\n            ...rest,\n            hasSecret: !!clientSecret,\n        }));\n\n        return NextResponse.json({ providers: safeProviders });\n    } catch (error) {\n        console.error('Failed to fetch OAuth providers:', error);\n        return NextResponse.json(\n            { error: 'Failed to fetch OAuth providers' },\n            { status: 500 }\n        );\n    }\n}\n\n/**\n * @method POST\n * @description Creates a new OAuth provider with support for custom URLs\n * @body {\n *   \"type\": \"object\",\n *   \"required\": [\"name\", \"authorizationUrl\", \"tokenUrl\", \"userInfoUrl\", \"clientId\", \"clientSecret\", \"scopes\"],\n *   \"properties\": {\n *     \"name\": { \"type\": \"string\" },\n *     \"authorizationUrl\": { \"type\": \"string\", \"format\": \"url\" },\n *     \"tokenUrl\": { \"type\": \"string\", \"format\": \"url\" },\n *     \"userInfoUrl\": { \"type\": \"string\", \"format\": \"url\" },\n *     \"clientId\": { \"type\": \"string\" },\n *     \"clientSecret\": { \"type\": \"string\" },\n *     \"scopes\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n *     \"enabled\": { \"type\": \"boolean\" },\n *     \"isDefault\": { \"type\": \"boolean\" }\n *   }\n * }\n * @response 201 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"name\": { \"type\": \"string\" },\n *     \"enabled\": { \"type\": \"boolean\" },\n *     \"isDefault\": { \"type\": \"boolean\" }\n *   }\n * }\n * @error 400 Invalid input - Validation error\n * @error 403 Unauthorized - User not authorized to access this endpoint\n * @error 500 An unexpected error occurred while creating provider\n */\nexport async function POST(request: Request) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        // Only admins can create providers\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                { error: 'Unauthorized' },\n                { status: 403 }\n            );\n        }\n\n        const body = await request.json();\n        const validatedData = providerSchema.parse(body);\n\n        // Get app URL for callback\n        const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';\n\n        // Track if we made another provider lose default status\n        let previousDefaultChanged = false;\n\n        // If this is set as default, unset any existing default\n        if (validatedData.isDefault) {\n            const previousDefault = await db.oAuthProvider.findFirst({\n                where: { isDefault: true },\n                select: { id: true, name: true }\n            });\n\n            if (previousDefault) {\n                previousDefaultChanged = true;\n                await db.oAuthProvider.updateMany({\n                    where: { isDefault: true },\n                    data: { isDefault: false }\n                });\n            }\n        }\n\n        // Create the provider with the provided URLs\n        const provider = await db.oAuthProvider.create({\n            data: {\n                name: validatedData.name,\n                clientId: validatedData.clientId,\n                clientSecret: validatedData.clientSecret,\n                authorizationUrl: validatedData.authorizationUrl,\n                tokenUrl: validatedData.tokenUrl,\n                userInfoUrl: validatedData.userInfoUrl,\n                // Generate callback URL based on provider name\n                callbackUrl: `${appUrl}/api/auth/oauth/callback/${validatedData.name.toLowerCase().replace(/\\s+/g, '-')}`,\n                scopes: validatedData.scopes,\n                enabled: validatedData.enabled,\n                isDefault: validatedData.isDefault,\n                allowedEmailDomains: validatedData.allowedEmailDomains,\n                blockExistingUsers: validatedData.blockExistingUsers,\n                requiredClaims: validatedData.requiredClaims || {},\n            }\n        });\n\n        // Create audit log for provider creation\n        try {\n            await createAuditLog(\n                'CREATE_OAUTH_PROVIDER',\n                user.id,\n                user.id,\n                {\n                    providerId: provider.id,\n                    providerName: provider.name,\n                    authorizationUrl: validatedData.authorizationUrl,\n                    tokenUrl: validatedData.tokenUrl,\n                    userInfoUrl: validatedData.userInfoUrl,\n                    enabled: provider.enabled,\n                    isDefault: provider.isDefault,\n                    scopes: validatedData.scopes,\n                    previousDefaultChanged: previousDefaultChanged\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n        }\n\n        return NextResponse.json(\n            {\n                id: provider.id,\n                name: provider.name,\n                enabled: provider.enabled,\n                isDefault: provider.isDefault\n            },\n            { status: 201 }\n        );\n    } catch (error) {\n        console.error('Failed to create OAuth provider:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: 'Validation failed', details: error.errors },\n                { status: 400 }\n            );\n        }\n\n        return NextResponse.json(\n            { error: 'Failed to create OAuth provider' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/admin/saml/providers/[id]/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { createAuditLog } from '@/lib/utils/auditLog';\nimport { db } from '@/lib/db';\nimport { z } from 'zod';\n\nconst updateSchema = z.object({\n    name: z.string().min(1).optional(),\n    entityId: z.string().min(1).optional(),\n    ssoUrl: z.string().url().optional(),\n    certificate: z.string().min(1).optional(),\n    spEntityId: z.string().optional().nullable(),\n    nameIdFormat: z.string().optional(),\n    emailAttribute: z.string().optional(),\n    nameAttribute: z.string().optional(),\n    enabled: z.boolean().optional(),\n    isDefault: z.boolean().optional(),\n    allowedEmailDomains: z.array(z.string()).optional(),\n    blockExistingUsers: z.boolean().optional(),\n    requiredClaims: z.record(z.string()).optional().nullable(),\n});\n\nexport async function PATCH(\n    request: Request,\n    { params }: { params: Promise<{ id: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });\n        }\n\n        const { id } = await params;\n        const body = await request.json();\n        const validatedData = updateSchema.parse(body);\n\n        const provider = await db.sAMLProvider.findUnique({ where: { id } });\n        if (!provider) {\n            return NextResponse.json({ error: 'Provider not found' }, { status: 404 });\n        }\n\n        // Enforce single default\n        if (validatedData.isDefault) {\n            await db.sAMLProvider.updateMany({\n                where: { id: { not: id }, isDefault: true },\n                data: { isDefault: false },\n            });\n        }\n\n        const updatedProvider = await db.sAMLProvider.update({\n            where: { id },\n            data: {\n                ...(validatedData.name !== undefined && { name: validatedData.name }),\n                ...(validatedData.entityId !== undefined && { entityId: validatedData.entityId }),\n                ...(validatedData.ssoUrl !== undefined && { ssoUrl: validatedData.ssoUrl }),\n                ...(validatedData.certificate !== undefined && { certificate: validatedData.certificate }),\n                ...(validatedData.spEntityId !== undefined && { spEntityId: validatedData.spEntityId }),\n                ...(validatedData.nameIdFormat !== undefined && { nameIdFormat: validatedData.nameIdFormat }),\n                ...(validatedData.emailAttribute !== undefined && { emailAttribute: validatedData.emailAttribute }),\n                ...(validatedData.nameAttribute !== undefined && { nameAttribute: validatedData.nameAttribute }),\n                ...(validatedData.enabled !== undefined && { enabled: validatedData.enabled }),\n                ...(validatedData.isDefault !== undefined && { isDefault: validatedData.isDefault }),\n                ...(validatedData.allowedEmailDomains !== undefined && { allowedEmailDomains: validatedData.allowedEmailDomains }),\n                ...(validatedData.blockExistingUsers !== undefined && { blockExistingUsers: validatedData.blockExistingUsers }),\n                ...(validatedData.requiredClaims !== undefined && { requiredClaims: validatedData.requiredClaims || {} }),\n            },\n        });\n\n        try {\n            await createAuditLog('UPDATE_SAML_PROVIDER', user.id, user.id, {\n                providerId: updatedProvider.id,\n                providerName: updatedProvider.name,\n            });\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n        }\n\n        return NextResponse.json({\n            id: updatedProvider.id,\n            name: updatedProvider.name,\n            enabled: updatedProvider.enabled,\n            isDefault: updatedProvider.isDefault,\n            updatedAt: updatedProvider.updatedAt,\n        });\n    } catch (error) {\n        console.error('Failed to update SAML provider:', error);\n        if (error instanceof z.ZodError) {\n            return NextResponse.json({ error: 'Validation failed', details: error.errors }, { status: 400 });\n        }\n        return NextResponse.json({ error: 'Failed to update SAML provider' }, { status: 500 });\n    }\n}\n\nexport async function DELETE(\n    request: Request,\n    { params }: { params: Promise<{ id: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });\n        }\n\n        const { id } = await params;\n\n        const provider = await db.sAMLProvider.findUnique({ where: { id } });\n        if (!provider) {\n            return NextResponse.json({ error: 'Provider not found' }, { status: 404 });\n        }\n\n        await db.sAMLProvider.delete({ where: { id } });\n\n        try {\n            await createAuditLog('DELETE_SAML_PROVIDER', user.id, user.id, {\n                deletedProvider: { id: provider.id, name: provider.name },\n            });\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n        }\n\n        return NextResponse.json({ success: true });\n    } catch (error) {\n        console.error('Failed to delete SAML provider:', error);\n        return NextResponse.json({ error: 'Failed to delete SAML provider' }, { status: 500 });\n    }\n}\n"
  },
  {
    "path": "app/api/admin/saml/providers/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { createAuditLog } from '@/lib/utils/auditLog';\nimport { db } from '@/lib/db';\nimport { z } from 'zod';\n\nconst providerSchema = z.object({\n    name: z.string().min(1, 'Name is required'),\n    entityId: z.string().min(1, 'IdP Entity ID is required'),\n    ssoUrl: z.string().url('SSO URL must be a valid URL'),\n    certificate: z.string().min(1, 'Certificate is required'),\n    spEntityId: z.string().optional().nullable(),\n    nameIdFormat: z\n        .string()\n        .default('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'),\n    emailAttribute: z.string().default('email'),\n    nameAttribute: z.string().default('name'),\n    enabled: z.boolean().default(true),\n    isDefault: z.boolean().default(false),\n    allowedEmailDomains: z.array(z.string()).default([]),\n    blockExistingUsers: z.boolean().default(false),\n    requiredClaims: z.record(z.string()).optional().nullable(),\n});\n\nexport async function GET(request: Request) {\n    try {\n        const user = await validateAuthAndGetUser();\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });\n        }\n\n        const { searchParams } = new URL(request.url);\n        const includeAll = searchParams.get('includeAll') === 'true';\n\n        const providers = await db.sAMLProvider.findMany({\n            where: includeAll ? {} : { enabled: true },\n            orderBy: { name: 'asc' },\n        });\n\n        try {\n            await createAuditLog('VIEW_SAML_PROVIDERS', user.id, user.id, {\n                providerCount: providers.length,\n                includeAll,\n            });\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n        }\n\n        return NextResponse.json({ providers });\n    } catch (error) {\n        console.error('Failed to fetch SAML providers:', error);\n        return NextResponse.json({ error: 'Failed to fetch SAML providers' }, { status: 500 });\n    }\n}\n\nexport async function POST(request: Request) {\n    try {\n        const user = await validateAuthAndGetUser();\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });\n        }\n\n        const body = await request.json();\n        const validatedData = providerSchema.parse(body);\n\n        // Enforce single default\n        if (validatedData.isDefault) {\n            await db.sAMLProvider.updateMany({\n                where: { isDefault: true },\n                data: { isDefault: false },\n            });\n        }\n\n        const provider = await db.sAMLProvider.create({\n            data: {\n                name: validatedData.name,\n                entityId: validatedData.entityId,\n                ssoUrl: validatedData.ssoUrl,\n                certificate: validatedData.certificate,\n                spEntityId: validatedData.spEntityId || null,\n                nameIdFormat: validatedData.nameIdFormat,\n                emailAttribute: validatedData.emailAttribute,\n                nameAttribute: validatedData.nameAttribute,\n                enabled: validatedData.enabled,\n                isDefault: validatedData.isDefault,\n                allowedEmailDomains: validatedData.allowedEmailDomains,\n                blockExistingUsers: validatedData.blockExistingUsers,\n                requiredClaims: validatedData.requiredClaims || {},\n            },\n        });\n\n        try {\n            await createAuditLog('CREATE_SAML_PROVIDER', user.id, user.id, {\n                providerId: provider.id,\n                providerName: provider.name,\n                enabled: provider.enabled,\n                isDefault: provider.isDefault,\n            });\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n        }\n\n        return NextResponse.json(\n            { id: provider.id, name: provider.name, enabled: provider.enabled, isDefault: provider.isDefault },\n            { status: 201 }\n        );\n    } catch (error) {\n        console.error('Failed to create SAML provider:', error);\n        if (error instanceof z.ZodError) {\n            return NextResponse.json({ error: 'Validation failed', details: error.errors }, { status: 400 });\n        }\n        return NextResponse.json({ error: 'Failed to create SAML provider' }, { status: 500 });\n    }\n}\n"
  },
  {
    "path": "app/api/admin/sponsor/route.ts",
    "content": "import {NextResponse} from 'next/server'\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog'\nimport {createAuditLog} from '@/lib/utils/auditLog'\nimport {SponsorService} from '@/lib/services/sponsor/service'\n\nexport async function GET() {\n    try {\n        const user = await validateAuthAndGetUser()\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json({error: 'Not authorized'}, {status: 403})\n        }\n\n        // Always re-verify against the server when admin loads status\n        const licenseKey = await SponsorService.getStoredLicenseKey()\n        const instanceId = await SponsorService.getInstanceId()\n\n        if (licenseKey && instanceId) {\n            try {\n                const fresh = await SponsorService.verifyLicense(licenseKey, instanceId)\n                await SponsorService.storeLicenseActivation(\n                    licenseKey, fresh.valid, fresh.proof, fresh.payload\n                )\n                return NextResponse.json({\n                    active: fresh.valid,\n                    features: fresh.features,\n                })\n            } catch {\n                // Network error — deactivate and signal connection failure\n                await SponsorService.clearLicenseState()\n                return NextResponse.json({\n                    active: false,\n                    features: [],\n                    connectionFailed: true,\n                })\n            }\n        }\n\n        const status = await SponsorService.getLicenseStatus()\n        return NextResponse.json({\n            active: status.active,\n            features: status.features,\n        })\n    } catch (error) {\n        console.error('Error fetching status:', error)\n        return NextResponse.json({error: 'Failed to fetch status'}, {status: 500})\n    }\n}\n\nexport async function POST(request: Request) {\n    try {\n        const user = await validateAuthAndGetUser()\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json({error: 'Not authorized'}, {status: 403})\n        }\n\n        const body = await request.json()\n        const {licenseKey, instanceName, mode, challengeId, responseCode} = body\n\n        const instanceId = await SponsorService.getInstanceId()\n        if (!instanceId) {\n            return NextResponse.json(\n                {error: 'Telemetry must be enabled to activate a license. An instance ID is required.'},\n                {status: 400}\n            )\n        }\n\n        // Challenge-response mode: step 1 — request challenge\n        if (mode === 'challenge') {\n            if (!licenseKey || typeof licenseKey !== 'string' || !licenseKey.startsWith('chr_sp_')) {\n                return NextResponse.json({error: 'Invalid license key'}, {status: 400})\n            }\n            try {\n                const result = await SponsorService.requestChallenge(licenseKey, instanceId)\n                return NextResponse.json(result)\n            } catch (error) {\n                return NextResponse.json(\n                    {error: error instanceof Error ? error.message : 'Challenge request failed'},\n                    {status: 400}\n                )\n            }\n        }\n\n        // Challenge-response mode: step 3 — confirm with response code\n        if (mode === 'confirm') {\n            if (!challengeId || !responseCode) {\n                return NextResponse.json({error: 'Missing challenge_id or response_code'}, {status: 400})\n            }\n            const confirmResult = await SponsorService.confirmChallenge(challengeId, responseCode, instanceName)\n            if (!confirmResult.success) {\n                return NextResponse.json(\n                    {error: confirmResult.message || 'Confirmation failed'},\n                    {status: 400}\n                )\n            }\n\n            // Store the activation using the license key from the challenge\n            if (licenseKey) {\n                await SponsorService.storeLicenseActivation(\n                    licenseKey, true, confirmResult.proof, confirmResult.payload\n                )\n            }\n\n            await createAuditLog('LICENSE_ACTIVATED', user.id, user.id, {\n                mode: 'challenge'\n            }).catch(() => {})\n\n            return NextResponse.json({\n                success: true,\n                active: true,\n                features: confirmResult.features || [],\n            })\n        }\n\n        // Default: direct activation (backward compat)\n        if (!licenseKey || typeof licenseKey !== 'string' || !licenseKey.startsWith('chr_sp_')) {\n            return NextResponse.json({error: 'Invalid license key'}, {status: 400})\n        }\n\n        const activateResult = await SponsorService.activateLicense(licenseKey, instanceId, instanceName)\n        if (!activateResult.success) {\n            return NextResponse.json(\n                {error: activateResult.message || 'Activation failed'},\n                {status: 400}\n            )\n        }\n\n        try {\n            const verifyResult = await SponsorService.verifyLicense(licenseKey, instanceId)\n            await SponsorService.storeLicenseActivation(\n                licenseKey, verifyResult.valid, verifyResult.proof, verifyResult.payload\n            )\n\n            await createAuditLog('LICENSE_ACTIVATED', user.id, user.id, {\n                valid: verifyResult.valid\n            }).catch(() => {})\n\n            return NextResponse.json({\n                success: true,\n                active: verifyResult.valid,\n                features: verifyResult.features,\n            })\n        } catch (error) {\n            if (error instanceof Error && error.message === 'NETWORK_ERROR') {\n                return NextResponse.json({\n                    success: false,\n                    error: 'Could not verify. Please try again.',\n                }, {status: 502})\n            }\n            throw error\n        }\n    } catch (error) {\n        console.error('Error activating:', error)\n        return NextResponse.json({error: 'Failed to activate'}, {status: 500})\n    }\n}\n\nexport async function DELETE() {\n    try {\n        const user = await validateAuthAndGetUser()\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json({error: 'Not authorized'}, {status: 403})\n        }\n\n        const licenseKey = await SponsorService.getStoredLicenseKey()\n        const instanceId = await SponsorService.getInstanceId()\n        if (licenseKey && instanceId) {\n            await SponsorService.deactivateLicense(licenseKey, instanceId)\n        }\n        await SponsorService.clearLicenseState()\n\n        await createAuditLog('LICENSE_DEACTIVATED', user.id, user.id, {}).catch(() => {})\n        return NextResponse.json({success: true})\n    } catch (error) {\n        console.error('Error deactivating:', error)\n        return NextResponse.json({error: 'Failed to deactivate'}, {status: 500})\n    }\n}\n"
  },
  {
    "path": "app/api/admin/system/slack/route.ts",
    "content": "import {NextResponse} from 'next/server'\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog'\nimport {db} from '@/lib/db'\n\n/**\n * GET /api/admin/system/slack\n * Fetch Slack OAuth configuration\n */\nexport async function GET() {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        // Check admin role\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                {error: 'Not authorized'},\n                {status: 403}\n            );\n        }\n\n        const config = await db.systemConfig.findUnique({\n            where: {id: 1},\n            select: {\n                slackOAuthEnabled: true,\n                slackOAuthClientId: true,\n                slackOAuthClientSecret: true,\n                slackSigningSecret: true,\n            },\n        });\n\n        if (!config) {\n            return NextResponse.json(\n                {error: 'Configuration not found'},\n                {status: 404}\n            );\n        }\n\n        // Never return raw secrets to the client — mask with boolean indicators\n        return NextResponse.json({\n            slackOAuthEnabled: config.slackOAuthEnabled,\n            slackOAuthClientId: config.slackOAuthClientId,\n            slackOAuthClientSecret: config.slackOAuthClientSecret ? '***' : null,\n            slackSigningSecret: config.slackSigningSecret ? '***' : null,\n            hasClientSecret: !!config.slackOAuthClientSecret,\n            hasSigningSecret: !!config.slackSigningSecret,\n        });\n    } catch (error) {\n        console.error('Error fetching Slack config:', error);\n        return NextResponse.json(\n            {error: 'Internal server error'},\n            {status: 500}\n        );\n    }\n}\n\n/**\n * PUT /api/admin/system/slack\n * Update Slack OAuth configuration\n */\nexport async function PUT(req: Request) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        // Check admin role\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                {error: 'Not authorized'},\n                {status: 403}\n            );\n        }\n\n        const body = await req.json();\n        const {slackOAuthEnabled, slackOAuthClientId, slackOAuthClientSecret, slackSigningSecret} = body;\n\n        // Validate required fields\n        if (slackOAuthEnabled && (!slackOAuthClientId || !slackOAuthClientSecret || !slackSigningSecret)) {\n            return NextResponse.json(\n                {error: 'Client ID, Secret, and Signing Secret are required when enabling Slack integration'},\n                {status: 400}\n            );\n        }\n\n        const config = await db.systemConfig.update({\n            where: {id: 1},\n            data: {\n                slackOAuthEnabled,\n                slackOAuthClientId: slackOAuthClientId || null,\n                slackOAuthClientSecret: slackOAuthClientSecret || null,\n                slackSigningSecret: slackSigningSecret || null,\n            },\n        });\n\n        return NextResponse.json({\n            slackOAuthEnabled: config.slackOAuthEnabled,\n            slackOAuthClientId: config.slackOAuthClientId ? '***' : null,\n            slackOAuthClientSecret: config.slackOAuthClientSecret ? '***' : null,\n            slackSigningSecret: config.slackSigningSecret ? '***' : null,\n        });\n    } catch (error) {\n        console.error('Error updating Slack config:', error);\n        return NextResponse.json(\n            {error: 'Internal server error'},\n            {status: 500}\n        );\n    }\n}\n"
  },
  {
    "path": "app/api/admin/users/[userId]/role/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { db } from '@/lib/db';\nimport { z } from 'zod';\n\n// Validation schema for role update\nconst updateRoleSchema = z.object({\n    role: z.enum(['ADMIN', 'STAFF'] as const),\n});\n\n/**\n * Update a user's role\n * @method PATCH\n * @description Updates a user's role to either 'ADMIN' or 'STAFF'. Only admins can perform this action. Requires user authentication.\n * @queryParams None\n * @requestBody {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"role\": { \"type\": \"enum\", \"enum\": [\"ADMIN\", \"STAFF\"] },\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"message\": { \"type\": \"string\" },\n *     \"user\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"email\": { \"type\": \"string\" },\n *         \"name\": { \"type\": \"string\" },\n *         \"role\": { \"type\": \"string\" },\n *         \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *         \"lastLoginAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *       }\n *     }\n *   }\n * }\n * @error 403 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n * @error 400 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" },\n *     \"details\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"message\": { \"type\": \"string\" },\n *           \"path\": { \"type\": \"string\" }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 500 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n */\nexport async function PATCH(\n    request: NextRequest,\n    context: { params: Promise<{ userId: string }> }\n) {\n    try {\n        const currentUser = await validateAuthAndGetUser();\n\n        // Only admins can update roles\n        if (currentUser.role !== 'ADMIN') {\n            return NextResponse.json(\n                { error: 'Unauthorized - Only admins can update roles' },\n                { status: 403 }\n            );\n        }\n\n        // Validate user ID\n        const { userId } = await context.params;\n        if (!userId) {\n            return NextResponse.json(\n                { error: 'User ID is required' },\n                { status: 400 }\n            );\n        }\n\n        // Check if user exists\n        const targetUser = await db.user.findUnique({\n            where: { id: userId },\n            select: { id: true, email: true, role: true },\n        });\n\n        if (!targetUser) {\n            return NextResponse.json(\n                { error: 'User not found' },\n                { status: 404 }\n            );\n        }\n\n        // Prevent self-role modification\n        if (targetUser.id === currentUser.id) {\n            return NextResponse.json(\n                { error: 'Cannot modify your own role' },\n                { status: 400 }\n            );\n        }\n\n        // Parse and validate request body\n        const body = await request.json();\n        const { role } = updateRoleSchema.parse(body);\n\n        // Update user role\n        const updatedUser = await db.user.update({\n            where: { id: userId },\n            data: { role },\n            select: {\n                id: true,\n                email: true,\n                name: true,\n                role: true,\n                createdAt: true,\n                lastLoginAt: true,\n            },\n        });\n\n        // Log the role change for audit purposes\n        await db.auditLog.create({\n            data: {\n                action: 'UPDATE_ROLE',\n                userId: currentUser.id,\n                targetUserId: userId,\n                details: JSON.stringify({\n                    previousRole: targetUser.role,\n                    newRole: role,\n                }),\n            },\n        });\n\n        return NextResponse.json({\n            message: 'Role updated successfully',\n            user: updatedUser,\n        });\n    } catch (error) {\n        console.error('Role update error:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {\n                    error: 'Invalid role value',\n                    details: error.errors,\n                },\n                { status: 400 }\n            );\n        }\n\n        return NextResponse.json(\n            { error: 'Failed to update user role' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/admin/users/[userId]/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server';\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog';\nimport {createAuditLog} from '@/lib/utils/auditLog';\nimport {db} from '@/lib/db';\nimport {z} from 'zod';\nimport {Role} from '@prisma/client';\n\n// Type definitions\ninterface PreservedUserData {\n    [key: string]: string | null; // Index signature for Prisma JSON compatibility\n    id: string;\n    email: string;\n    name: string | null;\n    role: string; // Use string instead of Role enum for JSON storage\n    deletedAt: string;\n    deletedBy: string;\n}\n\ninterface AuditLogDetails {\n    [key: string]: unknown;\n    _preservedUser?: PreservedUserData;\n    _preservedTargetUser?: PreservedUserData;\n}\n\n// Validation schemas\nconst updateUserSchema = z.object({\n    name: z.string().optional(),\n    role: z.enum(['ADMIN', 'STAFF'] as const).optional(),\n});\n\n/**\n * Update a user's details\n * @method PATCH\n * @description Updates a user's name and/or role. Only admins can perform this action.\n * @requestBody {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"name\": { \"type\": \"string\", \"description\": \"User's display name\" },\n *     \"role\": { \"type\": \"string\", \"enum\": [\"ADMIN\", \"STAFF\"], \"description\": \"User's role\" }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"message\": { \"type\": \"string\" },\n *     \"user\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"email\": { \"type\": \"string\" },\n *         \"name\": { \"type\": \"string\" },\n *         \"role\": { \"type\": \"string\" },\n *         \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *         \"lastLoginAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *       }\n *     }\n *   }\n * }\n * @error 403 Unauthorized - Only admins can update users\n * @error 400 Invalid request data or cannot modify own role\n * @error 404 User not found\n * @error 500 Failed to update user\n */\nexport async function PATCH(\n    request: NextRequest,\n    context: { params: Promise<{ userId: string }> }\n) {\n    try {\n        const currentUser = await validateAuthAndGetUser();\n\n        // Only admins can update users\n        if (currentUser.role !== 'ADMIN') {\n            try {\n                await createAuditLog(\n                    'UNAUTHORIZED_USER_UPDATE_ATTEMPT',\n                    currentUser.id,\n                    currentUser.id,\n                    {\n                        userRole: currentUser.role,\n                        targetUserId: (await context.params).userId,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                {error: 'Unauthorized - Only admins can update users'},\n                {status: 403}\n            );\n        }\n\n        const {userId} = await context.params;\n\n        if (!userId) {\n            return NextResponse.json(\n                {error: 'User ID is required'},\n                {status: 400}\n            );\n        }\n\n        // Check if user exists\n        const targetUser = await db.user.findUnique({\n            where: {id: userId},\n            select: {id: true, email: true, role: true, name: true},\n        });\n\n        if (!targetUser) {\n            try {\n                await createAuditLog(\n                    'USER_UPDATE_NOT_FOUND',\n                    currentUser.id,\n                    currentUser.id,\n                    {\n                        action: 'UPDATE_USER',\n                        requestedUserId: userId,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                {error: 'User not found'},\n                {status: 404}\n            );\n        }\n\n        // Parse and validate request body\n        const body = await request.json();\n        const validatedData = updateUserSchema.parse(body);\n\n        // Prevent self-role modification\n        if (validatedData.role && targetUser.id === currentUser.id) {\n            try {\n                await createAuditLog(\n                    'SELF_ROLE_MODIFICATION_ATTEMPT',\n                    currentUser.id,\n                    currentUser.id,\n                    {\n                        currentRole: currentUser.role,\n                        requestedRole: validatedData.role,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                {error: 'Cannot modify your own role'},\n                {status: 400}\n            );\n        }\n\n        // Build update data with proper typing\n        const updateData: { name?: string; role?: Role } = {};\n        if (validatedData.name !== undefined) updateData.name = validatedData.name;\n        if (validatedData.role !== undefined) updateData.role = validatedData.role as Role;\n\n        // Check if there are actually changes to make\n        if (Object.keys(updateData).length === 0) {\n            return NextResponse.json({\n                message: 'No changes specified',\n                user: targetUser,\n            });\n        }\n\n        // Log update attempt\n        try {\n            await createAuditLog(\n                'USER_UPDATE_ATTEMPT',\n                currentUser.id,\n                targetUser.id,\n                {\n                    targetUserEmail: targetUser.email,\n                    targetUserName: targetUser.name,\n                    currentRole: targetUser.role,\n                    updates: updateData,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n        }\n\n        // Update user\n        const updatedUser = await db.user.update({\n            where: {id: userId},\n            data: updateData,\n            select: {\n                id: true,\n                email: true,\n                name: true,\n                role: true,\n                createdAt: true,\n                lastLoginAt: true,\n            },\n        });\n\n        // Log successful update\n        try {\n            await createAuditLog(\n                'UPDATE_USER',\n                currentUser.id,\n                userId,\n                {\n                    previousData: {\n                        name: targetUser.name,\n                        role: targetUser.role\n                    },\n                    newData: {\n                        name: updatedUser.name,\n                        role: updatedUser.role\n                    },\n                    adminEmail: currentUser.email,\n                    targetUserEmail: targetUser.email,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n        }\n\n        return NextResponse.json({\n            message: 'User updated successfully',\n            user: updatedUser,\n        });\n\n    } catch (error) {\n        console.error('User update error:', error);\n\n        if (error instanceof z.ZodError) {\n            try {\n                const currentUser = await validateAuthAndGetUser();\n                await createAuditLog(\n                    'USER_UPDATE_VALIDATION_ERROR',\n                    currentUser.id,\n                    currentUser.id,\n                    {\n                        targetUserId: (await context.params).userId,\n                        validationErrors: error.errors,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                {\n                    error: 'Invalid request data',\n                    details: error.errors,\n                },\n                {status: 400}\n            );\n        }\n\n        try {\n            const currentUser = await validateAuthAndGetUser();\n            await createAuditLog(\n                'USER_UPDATE_ERROR',\n                currentUser.id,\n                currentUser.id,\n                {\n                    targetUserId: (await context.params).userId,\n                    error: error instanceof Error ? error.message : 'Unknown error',\n                    stack: error instanceof Error ? error.stack : undefined,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n        }\n\n        return NextResponse.json(\n            {error: 'Failed to update user'},\n            {status: 500}\n        );\n    }\n}\n\n/**\n * Delete a user account while preserving their data\n * @method DELETE\n * @description Safely deletes a user account by setting their references to NULL in related data.\n * This preserves data integrity while removing the user from the system.\n * Only admins can perform this action.\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"message\": { \"type\": \"string\" },\n *     \"preservedData\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"auditLogs\": { \"type\": \"number\" },\n *         \"changelogRequests\": { \"type\": \"number\" },\n *         \"apiKeys\": { \"type\": \"number\" },\n *         \"invitations\": { \"type\": \"number\" }\n *       }\n *     }\n *   }\n * }\n * @error 403 Unauthorized - Only admins can delete users\n * @error 404 User not found\n * @error 400 Cannot delete own account or last admin\n * @error 500 Failed to delete user\n */\nexport async function DELETE(\n    request: NextRequest,\n    context: { params: Promise<{ userId: string }> }\n) {\n    try {\n        const currentUser = await validateAuthAndGetUser();\n\n        // Only admins can delete users\n        if (currentUser.role !== 'ADMIN') {\n            try {\n                await createAuditLog(\n                    'UNAUTHORIZED_USER_DELETE_ATTEMPT',\n                    currentUser.id,\n                    currentUser.id,\n                    {\n                        userRole: currentUser.role,\n                        targetUserId: (await context.params).userId,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                {error: 'Unauthorized - Only admins can delete users'},\n                {status: 403}\n            );\n        }\n\n        const {userId} = await context.params;\n\n        if (!userId) {\n            return NextResponse.json(\n                {error: 'User ID is required'},\n                {status: 400}\n            );\n        }\n\n        // Check if user exists\n        const targetUser = await db.user.findUnique({\n            where: {id: userId},\n            select: {\n                id: true,\n                email: true,\n                role: true,\n                name: true,\n                createdAt: true\n            },\n        });\n\n        if (!targetUser) {\n            try {\n                await createAuditLog(\n                    'USER_DELETE_NOT_FOUND',\n                    currentUser.id,\n                    currentUser.id,\n                    {\n                        action: 'DELETE_USER',\n                        requestedUserId: userId,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                {error: 'User not found'},\n                {status: 404}\n            );\n        }\n\n        // Prevent self-deletion\n        if (targetUser.id === currentUser.id) {\n            try {\n                await createAuditLog(\n                    'SELF_DELETE_ATTEMPT',\n                    currentUser.id,\n                    currentUser.id,\n                    {\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                {error: 'Cannot delete your own account'},\n                {status: 400}\n            );\n        }\n\n        // Check if this is the last admin\n        if (targetUser.role === 'ADMIN') {\n            const adminCount = await db.user.count({\n                where: {role: 'ADMIN'}\n            });\n\n            if (adminCount <= 1) {\n                try {\n                    await createAuditLog(\n                        'LAST_ADMIN_DELETE_ATTEMPT',\n                        currentUser.id,\n                        targetUser.id,\n                        {\n                            targetUserEmail: targetUser.email,\n                            adminCount,\n                            timestamp: new Date().toISOString()\n                        }\n                    );\n                } catch (auditLogError) {\n                    console.error('Failed to create audit log:', auditLogError);\n                }\n\n                return NextResponse.json(\n                    {error: 'Cannot delete the last admin user'},\n                    {status: 400}\n                );\n            }\n        }\n\n        // Log deletion attempt\n        try {\n            await createAuditLog(\n                'USER_DELETE_ATTEMPT',\n                currentUser.id,\n                targetUser.id,\n                {\n                    targetUserEmail: targetUser.email,\n                    targetUserName: targetUser.name,\n                    targetUserRole: targetUser.role,\n                    targetUserCreatedAt: targetUser.createdAt.toISOString(),\n                    adminEmail: currentUser.email,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n        }\n\n        // Create preserved user data object\n        const preservedUserData: PreservedUserData = {\n            id: targetUser.id,\n            email: targetUser.email,\n            name: targetUser.name,\n            role: targetUser.role, // This will be automatically converted to string\n            deletedAt: new Date().toISOString(),\n            deletedBy: currentUser.id\n        };\n\n        // Perform the deletion in a transaction to ensure data consistency\n        const result = await db.$transaction(async (tx) => {\n            // Count existing data for reporting\n            const [\n                auditLogCount,\n                changelogRequestCount,\n                apiKeyCount,\n                invitationCount\n            ] = await Promise.all([\n                tx.auditLog.count({where: {OR: [{userId}, {targetUserId: userId}]}}),\n                tx.changelogRequest.count({where: {OR: [{staffId: userId}, {adminId: userId}]}}),\n                tx.apiKey.count({where: {userId}}),\n                tx.invitationLink.count({where: {createdBy: userId}})\n            ]);\n\n            // Step 1: Preserve user info in audit logs before deletion\n            if (auditLogCount > 0) {\n                // Get audit logs where user is the performer\n                const userAuditLogs = await tx.auditLog.findMany({\n                    where: {userId},\n                    select: {id: true, details: true}\n                });\n\n                // Update each audit log to preserve user info in details\n                for (const log of userAuditLogs) {\n                    const currentDetails = (log.details as AuditLogDetails) || {};\n                    await tx.auditLog.update({\n                        where: {id: log.id},\n                        data: {\n                            details: {\n                                ...currentDetails,\n                                _preservedUser: preservedUserData\n                            }\n                        }\n                    });\n                }\n\n                // Get audit logs where user is the target\n                const targetAuditLogs = await tx.auditLog.findMany({\n                    where: {targetUserId: userId},\n                    select: {id: true, details: true}\n                });\n\n                // Update each audit log to preserve target user info in details\n                for (const log of targetAuditLogs) {\n                    const currentDetails = (log.details as AuditLogDetails) || {};\n                    await tx.auditLog.update({\n                        where: {id: log.id},\n                        data: {\n                            details: {\n                                ...currentDetails,\n                                _preservedTargetUser: preservedUserData\n                            }\n                        }\n                    });\n                }\n            }\n\n            // Step 2: Revoke all API keys (mark as revoked to preserve audit trail)\n            if (apiKeyCount > 0) {\n                await tx.apiKey.updateMany({\n                    where: {userId},\n                    data: {isRevoked: true}\n                });\n            }\n\n            // Step 3: Mark unused invitations as used/expired (preserve for audit trail)\n            if (invitationCount > 0) {\n                await tx.invitationLink.updateMany({\n                    where: {\n                        createdBy: userId,\n                        usedAt: null\n                    },\n                    data: {usedAt: new Date()}\n                });\n            }\n\n            // Step 4: Delete the user account\n            // Foreign key constraints will automatically SET NULL on related records\n            // But we've already preserved the user info in the audit log details\n            await tx.user.delete({\n                where: {id: userId}\n            });\n\n            return {\n                preservedData: {\n                    auditLogs: auditLogCount,\n                    changelogRequests: changelogRequestCount,\n                    apiKeys: apiKeyCount,\n                    invitations: invitationCount\n                }\n            };\n        });\n\n        // Log successful deletion\n        try {\n            await createAuditLog(\n                'DELETE_USER',\n                currentUser.id,\n                currentUser.id, // Use admin's ID since target user no longer exists\n                {\n                    deletedUserId: targetUser.id,\n                    deletedUserEmail: targetUser.email,\n                    deletedUserName: targetUser.name,\n                    deletedUserRole: targetUser.role,\n                    deletedAt: new Date().toISOString(),\n                    adminEmail: currentUser.email,\n                    preservedDataCounts: result.preservedData\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n        }\n\n        return NextResponse.json({\n            message: 'User deleted successfully',\n            preservedData: result.preservedData\n        });\n\n    } catch (error) {\n        console.error('User deletion error:', error);\n\n        // Log deletion error\n        try {\n            const currentUser = await validateAuthAndGetUser();\n            await createAuditLog(\n                'USER_DELETE_ERROR',\n                currentUser.id,\n                currentUser.id,\n                {\n                    targetUserId: (await context.params).userId,\n                    error: error instanceof Error ? error.message : 'Unknown error',\n                    stack: error instanceof Error ? error.stack : undefined,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n        }\n\n        return NextResponse.json(\n            {error: 'Failed to delete user'},\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/admin/users/invitations/[id]/route.ts",
    "content": "import { validateAuthAndGetUser } from \"@/lib/utils/changelog\";\nimport { NextResponse } from \"next/server\";\nimport { createAuditLog } from \"@/lib/utils/auditLog\";\nimport { db } from \"@/lib/db\";\n\n/**\n * Revoke an invitation\n * @method DELETE\n * @description Revokes an invitation by marking it as used. Only admins have the permission to revoke invitations.\n * @queryParams { id: string } - The invitation ID to be revoked.\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"message\": { \"type\": \"string\" },\n *     \"invitation\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"email\": { \"type\": \"string\" },\n *         \"role\": { \"type\": \"string\" },\n *         \"expiresAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *         \"usedAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *       }\n *     }\n *   }\n * }\n * @error 403 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n * @error 404 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n * @error 500 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n */\nexport async function DELETE(\n    request: Request,\n    { params }: { params: Promise<{ id: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        if (user.role !== 'ADMIN') {\n            // Log unauthorized attempt to revoke invitation\n            try {\n                await createAuditLog(\n                    'UNAUTHORIZED_ACCESS_ATTEMPT',\n                    user.id,\n                    user.id, // Use the user's own ID as target to avoid foreign key issues\n                    {\n                        action: 'REVOKE_INVITATION',\n                        targetInvitationId: (await params).id,\n                        userRole: user.role,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create unauthorized access audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                { error: 'Unauthorized' },\n                { status: 403 }\n            );\n        }\n\n        // Await params to satisfy Next.js's requirement\n        const awaitedParams = await Promise.resolve(params);\n\n        // Create audit log for attempting to revoke an invitation\n        try {\n            await createAuditLog(\n                'INVITATION_REVOCATION_ATTEMPT',\n                user.id,\n                user.id, // Use the admin's own ID as target to avoid foreign key issues\n                {\n                    invitationId: awaitedParams.id,\n                    timestamp: new Date().toISOString(),\n                    ipAddress: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create attempt audit log:', auditLogError);\n        }\n\n        // Fetch the invitation details before revoking\n        const existingInvitation = await db.invitationLink.findUnique({\n            where: { id: awaitedParams.id }\n        });\n\n        if (!existingInvitation) {\n            // Log attempt to revoke non-existent invitation\n            try {\n                await createAuditLog(\n                    'INVITATION_NOT_FOUND',\n                    user.id,\n                    user.id, // Use the admin's own ID as target to avoid foreign key issues\n                    {\n                        invitationId: awaitedParams.id,\n                        timestamp: new Date().toISOString(),\n                        action: 'REVOKE_INVITATION'\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create not found audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                { error: 'Invitation link not found' },\n                { status: 404 }\n            );\n        }\n\n        // Check if invitation is already used/revoked\n        if (existingInvitation.usedAt) {\n            // Log attempt to revoke already revoked invitation\n            try {\n                await createAuditLog(\n                    'INVITATION_ALREADY_REVOKED',\n                    user.id,\n                    user.id, // Use the admin's own ID as target to avoid foreign key issues\n                    {\n                        invitationId: existingInvitation.id,\n                        invitationEmail: existingInvitation.email,\n                        originalUsedAt: existingInvitation.usedAt.toISOString(),\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create already revoked audit log:', auditLogError);\n            }\n\n            return NextResponse.json({\n                message: 'Invitation has already been revoked',\n                invitation: existingInvitation\n            });\n        }\n\n        // Check if invitation is expired\n        const isExpired = existingInvitation.expiresAt < new Date();\n\n        // Create audit log BEFORE performing the update\n        try {\n            await createAuditLog(\n                'REVOKE_INVITATION',\n                user.id,\n                user.id, // Use the admin's own ID as target to avoid foreign key issues\n                {\n                    invitationId: existingInvitation.id,\n                    invitationEmail: existingInvitation.email,\n                    invitationRole: existingInvitation.role,\n                    originalExpiresAt: existingInvitation.expiresAt.toISOString(),\n                    wasAlreadyExpired: isExpired,\n                    revokedBy: user.email || user.id,\n                    revokedAt: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create revocation audit log:', auditLogError);\n            // Continue execution even if audit log creation fails\n        }\n\n        // Instead of deleting, mark the invitation as used\n        const updatedInvitation = await db.invitationLink.update({\n            where: { id: awaitedParams.id },\n            data: {\n                usedAt: new Date(), // Mark as used to effectively revoke it\n            }\n        });\n\n        // Create success audit log after update\n        try {\n            await createAuditLog(\n                'INVITATION_REVOCATION_SUCCESS',\n                user.id,\n                user.id, // Use the admin's own ID as target to avoid foreign key issues\n                {\n                    invitationId: updatedInvitation.id,\n                    invitationEmail: updatedInvitation.email,\n                    invitationRole: updatedInvitation.role,\n                    usedAt: updatedInvitation.usedAt?.toISOString(),\n                    completedAt: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create success audit log:', auditLogError);\n        }\n\n        return NextResponse.json({\n            message: 'Invitation link revoked successfully',\n            invitation: updatedInvitation\n        });\n    } catch (error) {\n        console.error('Failed to revoke invitation link:', error);\n\n        // Log error during invitation revocation\n        try {\n            const user = await validateAuthAndGetUser();\n            await createAuditLog(\n                'INVITATION_REVOCATION_ERROR',\n                user.id,\n                user.id, // Use the user's own ID as target to avoid foreign key issues\n                {\n                    invitationId: (await params).id,\n                    error: error instanceof Error ? error.message : 'Unknown error',\n                    stack: error instanceof Error ? error.stack : undefined,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create error audit log:', auditLogError);\n        }\n\n        return NextResponse.json(\n            { error: 'Failed to revoke invitation link' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/admin/users/invitations/route.ts",
    "content": "import { validateAuthAndGetUser } from \"@/lib/utils/changelog\";\nimport { NextResponse } from \"next/server\";\nimport {db} from \"@/lib/db\";\n\n/**\n * Retrieves a list of all invitation links\n * @method GET\n * @description Retrieves a list of all invitation links in descending order by creation date. Requires admin permissions.\n * @body None\n * @response 200 {\n *   \"type\": \"array\",\n *   \"items\": {\n *     \"type\": \"object\",\n *     \"properties\": {\n *       \"id\": { \"type\": \"string\" },\n *       \"token\": { \"type\": \"string\" },\n *       \"email\": { \"type\": \"string\" },\n *       \"role\": { \"type\": \"string\" },\n *       \"expiresAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *       \"usedAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *       \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *       \"updatedAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *     }\n *   }\n * }\n * @error 401 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n */\nexport async function GET() {\n    try {\n        const [user] = await Promise.all([validateAuthAndGetUser()]);\n\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                { error: 'Unauthorized' },\n                { status: 403 }\n            );\n        }\n\n        const invitations = await db.invitationLink.findMany({\n            orderBy: {\n                createdAt: 'desc'\n            }\n        });\n\n        return NextResponse.json(invitations);\n    } catch (error) {\n        console.error('Failed to fetch invitation links:', error);\n        return NextResponse.json(\n            { error: 'Failed to fetch invitation links' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/admin/users/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { createAuditLog } from '@/lib/utils/auditLog';\nimport { db } from '@/lib/db';\nimport { z } from 'zod';\nimport { nanoid } from 'nanoid';\n\n// Validation schemas\nconst createInvitationSchema = z.object({\n    email: z.string().toLowerCase().email(),\n    role: z.enum(['ADMIN', 'STAFF']),\n    expiresAt: z.string().datetime().optional()\n});\n\n/**\n * @method GET\n * @description Retrieves a list of users with their email, name, role, creation date, and last login date.\n * Only admins have access to this endpoint.\n * @path /api/users\n * @response 200 {\n *   \"type\": \"array\",\n *   \"items\": {\n *     \"type\": \"object\",\n *     \"properties\": {\n *       \"id\": { \"type\": \"string\" },\n *       \"email\": { \"type\": \"string\" },\n *       \"name\": { \"type\": \"string\" },\n *       \"role\": { \"type\": \"string\" },\n *       \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *       \"lastLoginAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *     }\n *   }\n * }\n * @error 401 Unauthorized - User not authorized to access this endpoint\n * @error 500 An unexpected error occurred while fetching users\n */\nexport async function GET() {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        // Only admins can list users\n        if (user.role !== 'ADMIN') {\n            return new NextResponse(\n                JSON.stringify({ error: 'Unauthorized' }),\n                { status: 403 }\n            );\n        }\n\n        const users = await db.user.findMany({\n            select: {\n                id: true,\n                email: true,\n                name: true,\n                role: true,\n                createdAt: true,\n                lastLoginAt: true\n            },\n            orderBy: {\n                createdAt: 'desc'\n            }\n        });\n\n        // Get metrics for the audit log\n        const userMetrics = {\n            userCount: users.length || 0,\n            adminCount: users.filter(u => u.role === 'ADMIN').length,\n            staffCount: users.filter(u => u.role === 'STAFF').length,\n            newUsersLast30Days: users.filter(u => {\n                const thirtyDaysAgo = new Date();\n                thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);\n                return u.createdAt > thirtyDaysAgo;\n            }).length\n        };\n\n        // Log the action of viewing users\n        try {\n            await createAuditLog(\n                'VIEW_USERS_LIST',\n                user.id,\n                user.id, // Using admin's own ID to avoid foreign key issues\n                userMetrics\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n            // Continue execution even if audit log creation fails\n        }\n\n        return new NextResponse(JSON.stringify(users), { status: 200 });\n    } catch (error) {\n        console.error('Failed to fetch users:', error);\n        return new NextResponse(\n            JSON.stringify({ error: 'Failed to fetch users' }),\n            { status: 500 }\n        );\n    }\n}\n\n/**\n * @method POST\n * @description Creates an invitation link for user registration.\n * Only admins have access to this endpoint.\n * @path /api/invitations\n * @request {json}\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"message\": { \"type\": \"string\" },\n *     \"invitation\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"email\": { \"type\": \"string\" },\n *         \"role\": { \"type\": \"string\" },\n *         \"expiresAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *         \"url\": { \"type\": \"string\" }\n *       }\n *     }\n *   }\n * }\n * @error 400 Invalid input - Email or role is missing or invalid\n * @error 400 An active invitation already exists for this email\n * @error 500 An unexpected error occurred while creating the invitation link\n */\nexport async function POST(request: Request) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        // Only admins can create invitation links\n        if (user.role !== 'ADMIN') {\n            return new NextResponse(\n                JSON.stringify({ error: 'Unauthorized' }),\n                { status: 403 }\n            );\n        }\n\n        const body = await request.json();\n        const validatedData = createInvitationSchema.parse(body);\n\n        // Prevent inviting system emails\n        if (validatedData.email.endsWith('@changerawr.sys')) {\n            // Log attempted invitation of system email\n            try {\n                await createAuditLog(\n                    'INVALID_INVITATION_ATTEMPT',\n                    user.id,\n                    user.id,\n                    {\n                        reason: 'System email address',\n                        attemptedEmail: validatedData.email\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create audit log:', auditLogError);\n            }\n\n            return new NextResponse(\n                JSON.stringify({ error: 'Cannot invite system email addresses' }),\n                { status: 400 }\n            );\n        }\n\n        // Check if user already exists\n        const existingUser = await db.user.findUnique({\n            where: { email: validatedData.email }\n        });\n\n        if (existingUser) {\n            // Log attempted invitation of existing user\n            try {\n                await createAuditLog(\n                    'INVALID_INVITATION_ATTEMPT',\n                    user.id,\n                    existingUser.id, // This is a valid user ID so we can use it\n                    {\n                        reason: 'User already exists',\n                        attemptedEmail: validatedData.email\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create audit log:', auditLogError);\n            }\n\n            return new NextResponse(\n                JSON.stringify({ error: 'User with this email already exists' }),\n                { status: 400 }\n            );\n        }\n\n        // Check for existing active invitation\n        const existingInvitation = await db.invitationLink.findFirst({\n            where: {\n                email: validatedData.email,\n                usedAt: null,\n                expiresAt: {\n                    gt: new Date()\n                }\n            }\n        });\n\n        if (existingInvitation) {\n            // Log attempted duplicate invitation\n            try {\n                await createAuditLog(\n                    'INVALID_INVITATION_ATTEMPT',\n                    user.id,\n                    user.id, // Using admin's own ID to avoid foreign key issues\n                    {\n                        reason: 'Active invitation already exists',\n                        attemptedEmail: validatedData.email,\n                        existingInvitationId: existingInvitation.id,\n                        existingExpiresAt: existingInvitation.expiresAt.toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create audit log:', auditLogError);\n            }\n\n            return new NextResponse(\n                JSON.stringify({ error: 'An active invitation already exists for this email' }),\n                { status: 400 }\n            );\n        }\n\n        // Generate invitation token and set expiration\n        const token = nanoid(32);\n        const expiresAt = validatedData.expiresAt\n            ? new Date(validatedData.expiresAt)\n            : new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days default\n\n        // Create invitation link\n        const invitationLink = await db.invitationLink.create({\n            data: {\n                email: validatedData.email,\n                token,\n                role: validatedData.role,\n                expiresAt,\n                createdBy: user.id\n            }\n        });\n\n        // Create the full invitation URL\n        const invitationUrl = `${process.env.NEXT_PUBLIC_APP_URL}/register/${token}`;\n\n        // Create audit log for invitation creation\n        try {\n            await createAuditLog(\n                'CREATE_INVITATION',\n                user.id,\n                user.id, // Using admin's own ID to avoid foreign key issues\n                {\n                    invitationId: invitationLink.id,\n                    invitationEmail: invitationLink.email,\n                    invitationRole: invitationLink.role,\n                    expiresAt: invitationLink.expiresAt.toISOString(),\n                    tokenLength: token.length\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n            // Continue execution even if audit log creation fails\n        }\n\n        return new NextResponse(\n            JSON.stringify({\n                message: 'Invitation link created successfully',\n                invitation: {\n                    id: invitationLink.id,\n                    email: invitationLink.email,\n                    role: invitationLink.role,\n                    expiresAt: invitationLink.expiresAt,\n                    url: invitationUrl\n                }\n            }),\n            { status: 200 }\n        );\n    } catch (error) {\n        if (error instanceof z.ZodError) {\n            // Log validation error\n            try {\n                const user = await validateAuthAndGetUser();\n                await createAuditLog(\n                    'INVITATION_VALIDATION_ERROR',\n                    user.id,\n                    user.id, // Using admin's own ID to avoid foreign key issues\n                    {\n                        errors: error.errors\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create audit log:', auditLogError);\n            }\n\n            return new NextResponse(\n                JSON.stringify({ error: 'Invalid request data', details: error.errors }),\n                { status: 400 }\n            );\n        }\n\n        console.error('Failed to create invitation link:', error);\n        return new NextResponse(\n            JSON.stringify({ error: 'Failed to create invitation link' }),\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/ai/decrypt/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server'\nimport {validateAuthAndGetUser} from \"@/lib/utils/changelog\"\nimport {decryptToken} from \"@/lib/utils/encryption\"\n\ninterface DecryptRequest {\n    encryptedToken: string\n}\n\ninterface DecryptResponse {\n    decryptedKey: string\n}\n\ninterface DecryptErrorResponse {\n    error: string\n}\n\n/**\n * @method POST\n * @description Decrypt an encrypted API key on the server side\n * @body {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"encryptedToken\": { \"type\": \"string\" }\n *   },\n *   \"required\": [\"encryptedToken\"]\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"decryptedKey\": { \"type\": \"string\" }\n *   }\n * }\n */\nexport async function POST(request: NextRequest): Promise<NextResponse<DecryptResponse | DecryptErrorResponse>> {\n    try {\n        await validateAuthAndGetUser()\n\n        const body: DecryptRequest = await request.json()\n\n        if (!body.encryptedToken) {\n            return new NextResponse(\n                JSON.stringify({error: 'Missing encrypted token'}),\n                {status: 400}\n            )\n        }\n\n        const decryptedKey = decryptToken(body.encryptedToken)\n\n        return NextResponse.json({decryptedKey})\n    } catch (error) {\n        console.error('Error decrypting API key:', error)\n\n        return new NextResponse(\n            JSON.stringify({error: 'Failed to decrypt API key'}),\n            {status: 500}\n        )\n    }\n}"
  },
  {
    "path": "app/api/ai/settings/route.ts",
    "content": "import {NextResponse} from 'next/server'\nimport {PrismaClient} from '@prisma/client'\nimport {validateAuthAndGetUser} from \"@/lib/utils/changelog\"\nimport {encryptToken} from \"@/lib/utils/encryption\"\n\nconst prisma = new PrismaClient()\n\ninterface SystemConfig {\n    enableAIAssistant: boolean\n    aiApiKey: string | null\n    aiDefaultModel: string | null\n}\n\ninterface AISettingsResponse {\n    enableAIAssistant: boolean\n    aiApiKey: string | null\n    aiDefaultModel: string | null\n}\n\ninterface AISettingsErrorResponse {\n    error: string\n    enableAIAssistant: boolean\n    aiApiKey: null\n}\n\n/**\n * @method GET\n * @description Retrieve the system AI settings with encrypted API key for the editor\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"enableAIAssistant\": { \"type\": \"boolean\" },\n *     \"aiApiKey\": { \"type\": \"string\", \"nullable\": true },\n *     \"aiDefaultModel\": { \"type\": \"string\", \"nullable\": true }\n *   }\n * }\n */\nexport async function GET(): Promise<NextResponse<AISettingsResponse | AISettingsErrorResponse>> {\n    try {\n        await validateAuthAndGetUser()\n\n        // Get the system configuration\n        const config = await prisma.systemConfig.findFirst({\n            where: {id: 1},\n            select: {\n                enableAIAssistant: true,\n                aiApiKey: true,\n                aiDefaultModel: true\n            }\n        }) as SystemConfig | null\n\n        // Encrypt the API key before sending to client\n        let encryptedApiKey: string | null = null\n        if (config?.aiApiKey) {\n            encryptedApiKey = encryptToken(config.aiApiKey)\n        }\n\n        const response: AISettingsResponse = {\n            enableAIAssistant: config?.enableAIAssistant || false,\n            aiApiKey: encryptedApiKey,\n            aiDefaultModel: config?.aiDefaultModel || null,\n        }\n\n        return NextResponse.json(response)\n    } catch (error) {\n        console.error('Error fetching AI system settings:', error)\n\n        const errorResponse: AISettingsErrorResponse = {\n            error: 'Failed to fetch AI settings',\n            enableAIAssistant: false,\n            aiApiKey: null,\n        }\n\n        return new NextResponse(\n            JSON.stringify(errorResponse),\n            {status: 500}\n        )\n    }\n}"
  },
  {
    "path": "app/api/analytics/track/route.ts",
    "content": "// app/api/analytics/track/route.ts\nimport {NextResponse} from 'next/server';\nimport {trackChangelogView} from '@/lib/middleware/analytics';\nimport {z} from 'zod';\n\nconst trackingSchema = z.object({\n    projectId: z.string(),\n    changelogEntryId: z.string().optional(),\n});\n\n/**\n * @method POST\n * @description Track a changelog view (cookieless, GDPR compliant)\n * @body {\n *   \"type\": \"object\",\n *   \"required\": [\"projectId\"],\n *   \"properties\": {\n *     \"projectId\": {\n *       \"type\": \"string\",\n *       \"description\": \"ID of the project being viewed\"\n *     },\n *     \"changelogEntryId\": {\n *       \"type\": \"string\",\n *       \"description\": \"ID of the specific changelog entry being viewed (optional)\"\n *     }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\" }\n *   }\n * }\n * @error 400 Invalid request body\n * @error 500 Failed to track view\n */\nexport async function POST(request: Request) {\n    try {\n        const body = await request.json();\n        const validatedData = trackingSchema.parse(body);\n\n        // Track the view using our cookieless system\n        await trackChangelogView(request, {\n            projectId: validatedData.projectId,\n            changelogEntryId: validatedData.changelogEntryId,\n        });\n\n        return NextResponse.json({success: true});\n    } catch (error) {\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {\n                    success: false,\n                    error: 'Invalid request body',\n                    details: error.errors\n                },\n                {status: 400}\n            );\n        }\n\n        console.error('Failed to track changelog view:', error);\n        return NextResponse.json(\n            {\n                success: false,\n                error: 'Failed to track view'\n            },\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/auth/change-password/route.ts",
    "content": "// app/api/auth/change-password/route.ts\nimport { NextResponse } from 'next/server';\nimport { db } from '@/lib/db';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { verifyPassword, hashPassword } from '@/lib/auth/password';\nimport { z } from 'zod';\n\nconst changePasswordSchema = z.object({\n    currentPassword: z.string().min(1, 'Current password is required'),\n    newPassword: z.string().min(8, 'New password must be at least 8 characters'),\n});\n\n/**\n * @method POST\n * @description Changes the password for a logged-in user\n * @body {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"currentPassword\": {\n *       \"type\": \"string\",\n *       \"description\": \"User's current password\"\n *     },\n *     \"newPassword\": {\n *       \"type\": \"string\",\n *       \"minLength\": 8,\n *       \"description\": \"User's new password\"\n *     }\n *   },\n *   \"required\": [\"currentPassword\", \"newPassword\"]\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\" }\n *   }\n * }\n * @error 400 Invalid input - Validation error\n * @error 401 Unauthorized - Invalid current password\n * @error 500 An unexpected error occurred while changing the password\n */\nexport async function POST(request: Request) {\n    try {\n        const user = await validateAuthAndGetUser();\n        const body = await request.json();\n        const { currentPassword, newPassword } = changePasswordSchema.parse(body);\n\n        // Get the user's current password\n        const dbUser = await db.user.findUnique({\n            where: { id: user.id },\n            select: { password: true }\n        });\n\n        if (!dbUser) {\n            return NextResponse.json(\n                { error: 'User not found' },\n                { status: 404 }\n            );\n        }\n\n        // Verify current password\n        const isValidPassword = await verifyPassword(currentPassword, dbUser.password);\n        if (!isValidPassword) {\n            return NextResponse.json(\n                { error: 'Current password is incorrect' },\n                { status: 401 }\n            );\n        }\n\n        // Hash the new password\n        const hashedPassword = await hashPassword(newPassword);\n\n        // Update the user's password\n        await db.user.update({\n            where: { id: user.id },\n            data: { password: hashedPassword }\n        });\n\n        // Invalidate all refresh tokens\n        await db.refreshToken.updateMany({\n            where: { userId: user.id },\n            data: { invalidated: true }\n        });\n\n        return NextResponse.json({ success: true });\n    } catch (error) {\n        console.error('Password change error:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: 'Validation failed', details: error.errors },\n                { status: 400 }\n            );\n        }\n\n        return NextResponse.json(\n            { error: 'Failed to change password' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/auth/cli/generate/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server';\nimport {z} from 'zod';\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog';\nimport {generateCLIAuthCode} from '@/lib/auth/cli-auth';\n\nconst generateCodeSchema = z.object({\n    callbackUrl: z.string().url('Valid callback URL is required'),\n});\n\n/**\n * @method POST\n * @description Generate a temporary authorization code for CLI authentication\n * @body {\n *   \"type\": \"object\",\n *   \"required\": [\"callbackUrl\"],\n *   \"properties\": {\n *     \"callbackUrl\": {\n *       \"type\": \"string\",\n *       \"format\": \"uri\",\n *       \"description\": \"CLI callback URL for the authorization code\"\n *     }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"code\": {\n *       \"type\": \"string\",\n *       \"description\": \"Temporary authorization code\"\n *     },\n *     \"expires\": {\n *       \"type\": \"number\",\n *       \"description\": \"Expiration timestamp in milliseconds\"\n *     },\n *     \"expiresAt\": {\n *       \"type\": \"string\",\n *       \"format\": \"date-time\",\n *       \"description\": \"Expiration date in ISO format\"\n *     }\n *   }\n * }\n * @error 400 Invalid callback URL\n * @error 401 User not authenticated\n * @error 500 Internal server error\n * @secure cookieAuth\n */\nexport async function POST(request: NextRequest) {\n    try {\n        // Validate user authentication (cookie-based)\n        const user = await validateAuthAndGetUser();\n\n        // Parse and validate request body\n        const body = await request.json();\n        const {callbackUrl} = generateCodeSchema.parse(body);\n\n        // Validate that the callback URL is for localhost (security measure)\n        const url = new URL(callbackUrl);\n\n        // Skip hostname validation for wwc:// protocol\n        if (url.protocol !== 'wwc:') {\n            // For other protocols, enforce localhost restriction\n            if (url.hostname !== 'localhost' && url.hostname !== '127.0.0.1') {\n                return NextResponse.json(\n                    {error: 'Callback URL must be localhost for security'},\n                    {status: 400}\n                );\n            }\n        }\n        // wwc:// protocol is allowed through without hostname validation\n\n        // Generate the authorization code\n        const authCode = await generateCLIAuthCode(user.id, callbackUrl);\n\n        return NextResponse.json({\n            code: authCode.code,\n            expires: authCode.expires,\n            expiresAt: authCode.expiresAt.toISOString(),\n        });\n\n    } catch (error) {\n        console.error('CLI auth code generation error:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {error: 'Invalid request format', details: error.errors},\n                {status: 400}\n            );\n        }\n\n        if (error instanceof Error && error.message.includes('token')) {\n            return NextResponse.json(\n                {error: 'Authentication required'},\n                {status: 401}\n            );\n        }\n\n        return NextResponse.json(\n            {error: 'Internal server error'},\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/auth/cli/refresh/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server';\nimport { z } from 'zod';\nimport { refreshCLIAccessToken } from '@/lib/auth/tokens';\n\nconst refreshSchema = z.object({\n    refreshToken: z.string().min(1, 'Refresh token is required'),\n});\n\n/**\n * @method POST\n * @description Refresh CLI access token using refresh token\n * @body {\n *   \"type\": \"object\",\n *   \"required\": [\"refreshToken\"],\n *   \"properties\": {\n *     \"refreshToken\": {\n *       \"type\": \"string\",\n *       \"description\": \"Refresh token from previous authentication\"\n *     }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"access_token\": {\n *       \"type\": \"string\",\n *       \"description\": \"New JWT access token\"\n *     },\n *     \"refresh_token\": {\n *       \"type\": \"string\",\n *       \"description\": \"New JWT refresh token\"\n *     },\n *     \"expires_in\": {\n *       \"type\": \"number\",\n *       \"description\": \"Access token expiration time in seconds\"\n *     },\n *     \"token_type\": {\n *       \"type\": \"string\",\n *       \"example\": \"Bearer\"\n *     },\n *     \"user\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"email\": { \"type\": \"string\" },\n *         \"name\": { \"type\": \"string\" },\n *         \"role\": { \"type\": \"string\" }\n *       }\n *     }\n *   }\n * }\n * @response 400 - Invalid refresh token\n * @response 401 - Expired or revoked refresh token\n * @response 500 - Internal server error\n */\nexport async function POST(request: NextRequest) {\n    try {\n        const body = await request.json();\n        const { refreshToken } = refreshSchema.parse(body);\n\n        // Attempt to refresh the access token\n        const result = await refreshCLIAccessToken(refreshToken);\n\n        if (!result) {\n            return NextResponse.json(\n                { error: 'Invalid or expired refresh token' },\n                { status: 401 }\n            );\n        }\n\n        // Return new tokens in OAuth2-compatible format\n        // 30 days = 30 * 24 * 60 * 60 = 2,592,000 seconds\n        return NextResponse.json({\n            access_token: result.accessToken,\n            refresh_token: result.refreshToken,\n            expires_in: 30 * 24 * 60 * 60, // 30 days in seconds\n            token_type: 'Bearer',\n            user: {\n                id: result.user.id,\n                email: result.user.email,\n                name: result.user.name,\n                role: result.user.role,\n            },\n        });\n\n    } catch (error) {\n        console.error('CLI token refresh error:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: 'Invalid request format', details: error.errors },\n                { status: 400 }\n            );\n        }\n\n        return NextResponse.json(\n            { error: 'Internal server error' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/auth/cli/token/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server';\nimport {z} from 'zod';\nimport {db} from '@/lib/db';\nimport {generateCLITokens} from '@/lib/auth/tokens';\n\nconst tokenExchangeSchema = z.object({\n    code: z.string().min(1, 'Authorization code is required'),\n    expires: z.string().min(1, 'Expiration timestamp is required'),\n});\n\n/**\n * @method POST\n * @description Exchange CLI authorization code for JWT tokens\n * @body {\n *   \"type\": \"object\",\n *   \"required\": [\"code\", \"expires\"],\n *   \"properties\": {\n *     \"code\": {\n *       \"type\": \"string\",\n *       \"description\": \"Temporary authorization code from CLI auth flow\"\n *     },\n *     \"expires\": {\n *       \"type\": \"string\",\n *       \"description\": \"Expiration timestamp of the authorization code\"\n *     }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"access_token\": {\n *       \"type\": \"string\",\n *       \"description\": \"JWT access token\"\n *     },\n *     \"refresh_token\": {\n *       \"type\": \"string\",\n *       \"description\": \"JWT refresh token\"\n *     },\n *     \"expires_in\": {\n *       \"type\": \"number\",\n *       \"description\": \"Access token expiration time in seconds\"\n *     },\n *     \"token_type\": {\n *       \"type\": \"string\",\n *       \"example\": \"Bearer\"\n *     },\n *     \"user\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"email\": { \"type\": \"string\" },\n *         \"name\": { \"type\": \"string\" },\n *         \"role\": { \"type\": \"string\" }\n *       }\n *     }\n *   }\n * }\n * @response 400 - Invalid request or expired code\n * @response 500 - Internal server error\n */\nexport async function POST(request: NextRequest) {\n    try {\n        const body = await request.json();\n        const {code, expires} = tokenExchangeSchema.parse(body);\n\n        // Validate the authorization code\n        const authCode = await db.cliAuthCode.findUnique({\n            where: {code},\n            include: {\n                user: {\n                    select: {\n                        id: true,\n                        email: true,\n                        name: true,\n                        role: true,\n                    },\n                },\n            },\n        });\n\n        if (!authCode) {\n            return NextResponse.json(\n                {error: 'Invalid authorization code'},\n                {status: 400}\n            );\n        }\n\n        // Check if code has expired\n        if (authCode.expiresAt < new Date()) {\n            return NextResponse.json(\n                {error: 'Authorization code has expired'},\n                {status: 400}\n            );\n        }\n\n        // Check if code has already been used\n        if (authCode.usedAt) {\n            return NextResponse.json(\n                {error: 'Authorization code has already been used'},\n                {status: 400}\n            );\n        }\n\n        // Verify the expires parameter matches\n        const providedExpires = new Date(expires);\n        if (Math.abs(authCode.expiresAt.getTime() - providedExpires.getTime()) > 1000) {\n            return NextResponse.json(\n                {error: 'Invalid expiration timestamp'},\n                {status: 400}\n            );\n        }\n\n        // Generate CLI-specific JWT tokens (30 day access, 90 day refresh)\n        const tokens = await generateCLITokens(authCode.user.id);\n\n        // Mark the authorization code as used\n        await db.cliAuthCode.update({\n            where: {code},\n            data: {usedAt: new Date()},\n        });\n\n        // Update user's last login time\n        await db.user.update({\n            where: {id: authCode.user.id},\n            data: {lastLoginAt: new Date()},\n        });\n\n        // Return tokens in OAuth2-compatible format\n        // 30 days = 30 * 24 * 60 * 60 = 2,592,000 seconds\n        return NextResponse.json({\n            access_token: tokens.accessToken,\n            refresh_token: tokens.refreshToken,\n            expires_in: 30 * 24 * 60 * 60, // 30 days in seconds\n            token_type: 'Bearer',\n            user: authCode.user,\n        });\n\n    } catch (error) {\n        console.error('CLI token exchange error:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {error: 'Invalid request format', details: error.errors},\n                {status: 400}\n            );\n        }\n\n        return NextResponse.json(\n            {error: 'Internal server error'},\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/auth/connections/route.ts",
    "content": "import {NextResponse} from 'next/server';\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog';\nimport {db} from '@/lib/db';\n\ninterface ConnectionResponse {\n    id: string;\n    providerId: string;\n    provider: {\n        id: string;\n        name: string;\n        enabled: boolean;\n        isDefault: boolean;\n    };\n    providerUserId: string;\n    expiresAt: string | null;\n    createdAt: string;\n    updatedAt: string;\n}\n\ninterface SAMLConnectionResponse {\n    id: string;\n    providerId: string;\n    provider: {\n        id: string;\n        name: string;\n        enabled: boolean;\n        isDefault: boolean;\n    };\n    nameId: string;\n    createdAt: string;\n    updatedAt: string;\n}\n\ninterface UserConnectionsResponse {\n    connections: ConnectionResponse[];\n    allProviders: {\n        id: string;\n        name: string;\n        enabled: boolean;\n        isDefault: boolean;\n    }[];\n    samlConnections: SAMLConnectionResponse[];\n    allSAMLProviders: {\n        id: string;\n        name: string;\n        enabled: boolean;\n        isDefault: boolean;\n    }[];\n}\n\n/**\n * @method GET\n * @description Retrieves user's OAuth connections and available providers\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"connections\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"providerId\": { \"type\": \"string\" },\n *           \"provider\": {\n *             \"type\": \"object\",\n *             \"properties\": {\n *               \"id\": { \"type\": \"string\" },\n *               \"name\": { \"type\": \"string\" },\n *               \"enabled\": { \"type\": \"boolean\" },\n *               \"isDefault\": { \"type\": \"boolean\" }\n *             }\n *           },\n *           \"providerUserId\": { \"type\": \"string\" },\n *           \"expiresAt\": { \"type\": \"string\", \"nullable\": true },\n *           \"createdAt\": { \"type\": \"string\" },\n *           \"updatedAt\": { \"type\": \"string\" }\n *         }\n *       }\n *     },\n *     \"allProviders\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"name\": { \"type\": \"string\" },\n *           \"enabled\": { \"type\": \"boolean\" },\n *           \"isDefault\": { \"type\": \"boolean\" }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 401 Unauthorized - User not authenticated\n * @error 500 Internal server error - Database query failed\n * @secure\n */\nexport async function GET() {\n    try {\n        // Validate authentication\n        const user = await validateAuthAndGetUser();\n\n        // Fetch user's OAuth connections with provider details\n        const connections = await db.oAuthConnection.findMany({\n            where: {\n                userId: user.id\n            },\n            include: {\n                provider: {\n                    select: {\n                        id: true,\n                        name: true,\n                        enabled: true,\n                        isDefault: true\n                    }\n                }\n            },\n            orderBy: {\n                createdAt: 'desc'\n            }\n        });\n\n        // Fetch all available providers for comparison\n        const allProviders = await db.oAuthProvider.findMany({\n            select: {\n                id: true,\n                name: true,\n                enabled: true,\n                isDefault: true\n            },\n            orderBy: {\n                name: 'asc'\n            }\n        });\n\n        // Fetch SAML connections\n        const samlConnectionsRaw = await db.sAMLConnection.findMany({\n            where: { userId: user.id },\n            include: {\n                provider: {\n                    select: { id: true, name: true, enabled: true, isDefault: true }\n                }\n            },\n            orderBy: { createdAt: 'desc' }\n        });\n\n        // Fetch all SAML providers\n        const allSAMLProviders = await db.sAMLProvider.findMany({\n            select: { id: true, name: true, enabled: true, isDefault: true },\n            orderBy: { name: 'asc' }\n        });\n\n        // Transform connections to match the expected format\n        const transformedConnections: ConnectionResponse[] = connections.map(connection => ({\n            id: connection.id,\n            providerId: connection.providerId,\n            provider: connection.provider,\n            providerUserId: connection.providerUserId,\n            expiresAt: connection.expiresAt ? connection.expiresAt.toISOString() : null,\n            createdAt: connection.createdAt.toISOString(),\n            updatedAt: connection.updatedAt.toISOString()\n        }));\n\n        const samlConnections: SAMLConnectionResponse[] = samlConnectionsRaw.map(c => ({\n            id: c.id,\n            providerId: c.providerId,\n            provider: c.provider,\n            nameId: c.nameId,\n            createdAt: c.createdAt.toISOString(),\n            updatedAt: c.updatedAt.toISOString(),\n        }));\n\n        const response: UserConnectionsResponse = {\n            connections: transformedConnections,\n            allProviders,\n            samlConnections,\n            allSAMLProviders,\n        };\n\n        return NextResponse.json(response);\n    } catch (error) {\n        console.error('Failed to fetch user OAuth connections:', error);\n\n        if (error instanceof Error && error.message === 'Unauthorized') {\n            return NextResponse.json(\n                {error: 'Unauthorized'},\n                {status: 401}\n            );\n        }\n\n        return NextResponse.json(\n            {error: 'Failed to fetch OAuth connections'},\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/auth/forgot-password/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server';\nimport {z} from 'zod';\nimport {createPasswordResetAndSendEmail} from '@/lib/services/auth/password-reset';\nimport {checkRateLimit} from '@/lib/utils/rate-limit';\n\n// Validation schema for forgot password request\nconst forgotPasswordSchema = z.object({\n    email: z.string().email('Please enter a valid email address'),\n});\n\n/**\n * @method POST\n * @description Initiates the password reset process by sending a reset email\n * @body {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"email\": {\n *       \"type\": \"string\",\n *       \"format\": \"email\",\n *       \"description\": \"User's email address\"\n *     }\n *   },\n *   \"required\": [\"email\"]\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": {\n *       \"type\": \"boolean\"\n *     },\n *     \"message\": {\n *       \"type\": \"string\"\n *     }\n *   }\n * }\n * @error 400 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": {\n *       \"type\": \"string\",\n *       \"example\": \"Invalid email format\"\n *     }\n *   }\n * }\n * @error 500 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": {\n *       \"type\": \"string\",\n *       \"example\": \"Failed to initiate password reset\"\n *     }\n *   }\n * }\n */\nexport async function POST(request: NextRequest) {\n    try {\n        // Rate limit: 5 requests per hour per IP to prevent email spam\n        const ip = request.headers.get('x-forwarded-for')?.split(',')[0].trim() || request.headers.get('x-real-ip') || 'unknown'\n        const rateLimit = checkRateLimit(`forgot-password:${ip}`, 5, 60 * 60 * 1000)\n        if (!rateLimit.allowed) {\n            // Return 200 to avoid leaking whether the IP is rate-limited\n            return NextResponse.json({ message: 'If an account exists with that email, a reset link has been sent.' })\n        }\n\n        const body = await request.json();\n        const {email} = forgotPasswordSchema.parse(body);\n\n        // Skip system emails\n        if (email.toLowerCase().endsWith('@changerawr.sys')) {\n            return NextResponse.json({\n                success: true,\n                message: \"If an account with this email exists, a password reset link has been sent.\"\n            });\n        }\n\n        await createPasswordResetAndSendEmail({email});\n\n        // Always return success even if user doesn't exist (for security)\n        return NextResponse.json({\n            success: true,\n            message: \"If an account with this email exists, a password reset link has been sent.\"\n        });\n    } catch (error) {\n        console.error('Password reset error:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {error: 'Invalid email format'},\n                {status: 400}\n            );\n        }\n\n        return NextResponse.json(\n            {error: 'Failed to initiate password reset'},\n            {status: 500}\n        );\n    }\n}\n"
  },
  {
    "path": "app/api/auth/invitation/[token]/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\n\n/**\n * Handles validation of invitation tokens and returns user data\n * @method GET\n * @description Validates an invitation token and returns the associated user email, role, and expiration date.\n * @body None\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"email\": { \"type\": \"string\" },\n *     \"role\": { \"type\": \"string\" },\n *     \"expiresAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *   }\n * }\n * @error 400 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"message\": { \"type\": \"string\" }\n *   }\n * }\n * @error 404 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"message\": { \"type\": \"string\" }\n *   }\n * }\n * @error 500 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"message\": { \"type\": \"string\" }\n *   }\n * }\n */\nexport async function GET(\n    request: Request,\n    { params }: { params: Promise<{ token: string }> }\n) {\n    if (!(await params).token) {\n        return NextResponse.json(\n            { message: 'Token is required' },\n            { status: 400 }\n        )\n    }\n\n    try {\n        console.log(`Checking invitation token: ${(await params).token}`)\n\n        const invitation = await db.invitationLink.findUnique({\n            where: {\n                token: (await params).token\n            }\n        })\n\n        console.log('Database result:', invitation)\n\n        if (!invitation) {\n            return NextResponse.json(\n                { message: 'Invitation not found' },\n                { status: 404 }\n            )\n        }\n\n        if (invitation.usedAt) {\n            return NextResponse.json(\n                { message: 'Invitation has already been used' },\n                { status: 400 }\n            )\n        }\n\n        if (invitation.expiresAt < new Date()) {\n            return NextResponse.json(\n                { message: 'Invitation has expired' },\n                { status: 400 }\n            )\n        }\n\n        return NextResponse.json({\n            email: invitation.email,\n            role: invitation.role,\n            expiresAt: invitation.expiresAt.toISOString()\n        })\n\n    } catch (error) {\n        console.error('Server error validating invitation:', error)\n        return NextResponse.json(\n            { message: 'Error validating invitation' },\n            { status: 500 }\n        )\n    }\n}"
  },
  {
    "path": "app/api/auth/login/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server'\nimport bcrypt from 'bcryptjs'\nimport {z} from 'zod'\nimport {generateTokens} from '@/lib/auth/tokens'\nimport {db} from '@/lib/db'\nimport {createAuditLog} from '@/lib/utils/auditLog'\nimport {checkPasswordBreach} from '@/lib/services/auth/password-breach'\nimport {shouldUseSecureCookies} from '@/lib/utils/cookies'\nimport {checkRateLimit} from '@/lib/utils/rate-limit'\n\nconst loginSchema = z.object({\n    email: z.string().email(),\n    password: z.string().min(1),\n    bypassBreachWarning: z.boolean().optional() // For when user chooses to continue despite breach\n})\n\ntype RequestBody = z.infer<typeof loginSchema>\n\n/**\n * @method POST\n * @description Authenticates user with email and password, checks for password breach\n * @body {\n *   \"type\": \"object\",\n *   \"required\": [\"email\", \"password\"],\n *   \"properties\": {\n *     \"email\": {\n *       \"type\": \"string\",\n *       \"format\": \"email\",\n *       \"description\": \"User's email address\"\n *     },\n *     \"password\": {\n *       \"type\": \"string\",\n *       \"description\": \"User's password\"\n *     },\n *     \"bypassBreachWarning\": {\n *       \"type\": \"boolean\",\n *       \"description\": \"Whether to bypass password breach warning\"\n *     }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"user\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"email\": { \"type\": \"string\" },\n *         \"name\": { \"type\": \"string\" },\n *         \"role\": { \"type\": \"string\" },\n *         \"lastLoginAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *       }\n *     },\n *     \"accessToken\": { \"type\": \"string\" },\n *     \"refreshToken\": { \"type\": \"string\" }\n *   }\n * }\n * @response 422 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\", \"example\": \"password_breached\" },\n *     \"message\": { \"type\": \"string\" },\n *     \"breachCount\": { \"type\": \"number\" },\n *     \"resetUrl\": { \"type\": \"string\" }\n *   }\n * }\n * @error 400 Validation failed - Invalid input format\n * @error 401 Unauthorized - Invalid credentials\n * @error 500 Internal server error\n * @secure cookieAuth\n */\nexport async function POST(request: NextRequest) {\n    const ipAddress = request.headers.get('x-forwarded-for')?.split(',')[0].trim() || request.headers.get('x-real-ip') || 'unknown'\n    const userAgent = request.headers.get('user-agent') || 'unknown'\n\n    // Rate limit: 10 attempts per 15 minutes per IP\n    const rateLimit = checkRateLimit(`login:${ipAddress}`, 10, 15 * 60 * 1000)\n    if (!rateLimit.allowed) {\n        return NextResponse.json(\n            { error: 'Too many login attempts. Please try again later.' },\n            {\n                status: 429,\n                headers: { 'Retry-After': String(Math.ceil((rateLimit.resetAt - Date.now()) / 1000)) }\n            }\n        )\n    }\n\n    let attemptLogId: string | undefined\n    let userId: string | undefined\n\n    try {\n        const body: RequestBody = await request.json()\n\n        // Validate input\n        const {email, password, bypassBreachWarning} = loginSchema.parse(body)\n\n        // Block login for system accounts\n        if (email.endsWith('@changerawr.sys')) {\n            try {\n                await createAuditLog(\n                    'LOGIN_FAILURE',\n                    null,\n                    null,\n                    {\n                        reason: 'SYSTEM_ACCOUNT_LOGIN_BLOCKED',\n                        email,\n                        ipAddress,\n                        userAgent,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create system account block audit log:', auditLogError);\n            }\n\n            await bcrypt.compare(password, '$2a$12$dummy.hash.to.prevent.timing.attacks.with.enough.length.to.be.realistic')\n\n            return NextResponse.json(\n                {error: 'Invalid credentials'},\n                {status: 401}\n            )\n        }\n\n        // Create login attempt log\n        try {\n            const attemptLog = await createAuditLog(\n                'LOGIN_ATTEMPT',\n                null, // No user ID available yet\n                null, // No target user ID\n                {\n                    email: body.email,\n                    ipAddress,\n                    userAgent,\n                    timestamp: new Date().toISOString(),\n                    bypassBreachWarning: bypassBreachWarning || false\n                }\n            );\n            attemptLogId = attemptLog?.id;\n        } catch (auditLogError) {\n            console.error('Failed to create login attempt audit log:', auditLogError);\n            // Continue with the login process even if audit logging fails\n        }\n\n        // Find user and include the necessary fields\n        const user = await db.user.findUnique({\n            where: {email},\n            select: {\n                id: true,\n                email: true,\n                password: true,\n                name: true,\n                role: true,\n                lastLoginAt: true,\n                twoFactorMode: true, // this will eventually be reworked\n            },\n        })\n\n        if (!user) {\n            // User not found - log the failed login attempt\n            try {\n                await createAuditLog(\n                    'LOGIN_FAILURE',\n                    null, // No user ID available\n                    null, // No target user ID\n                    {\n                        reason: 'USER_NOT_FOUND',\n                        email,\n                        ipAddress,\n                        userAgent,\n                        timestamp: new Date().toISOString(),\n                        attemptLogId\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create login failure audit log:', auditLogError);\n            }\n\n            // Simulate password check to prevent timing attacks\n            await bcrypt.compare(password, '$2a$12$dummy.hash.to.prevent.timing.attacks.with.enough.length.to.be.realistic')\n\n            return NextResponse.json(\n                {error: 'Invalid credentials'},\n                {status: 401}\n            )\n        }\n\n        userId = user.id\n\n        // Verify password\n        const isValidPassword = await bcrypt.compare(password, user.password)\n\n        if (!isValidPassword) {\n            // Invalid password - log the failed login attempt\n            try {\n                await createAuditLog(\n                    'LOGIN_FAILURE',\n                    user.id,\n                    user.id,\n                    {\n                        reason: 'INVALID_PASSWORD',\n                        email,\n                        ipAddress,\n                        userAgent,\n                        timestamp: new Date().toISOString(),\n                        attemptLogId\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create login failure audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                {error: 'Invalid credentials'},\n                {status: 401}\n            )\n        }\n\n        // Check if password has been breached (only if not bypassing)\n        if (!bypassBreachWarning) {\n            const breachResult = await checkPasswordBreach(password);\n\n            if (breachResult.isBreached) {\n                // Log the breach detection\n                try {\n                    await createAuditLog(\n                        'PASSWORD_BREACH_DETECTED',\n                        user.id,\n                        user.id,\n                        {\n                            email,\n                            breachCount: breachResult.breachCount,\n                            ipAddress,\n                            userAgent,\n                            timestamp: new Date().toISOString(),\n                            attemptLogId\n                        }\n                    );\n                } catch (auditLogError) {\n                    console.error('Failed to create breach detection audit log:', auditLogError);\n                }\n\n                // Return breach warning instead of logging in\n                return NextResponse.json(\n                    {\n                        error: 'password_breached',\n                        message: `Your password has been found in ${breachResult.breachCount.toLocaleString()} data breach${breachResult.breachCount === 1 ? '' : 'es'}. We recommend changing it for better security.`,\n                        breachCount: breachResult.breachCount,\n                        resetUrl: '/forgot-password'\n                    },\n                    {status: 422} // Unprocessable Entity\n                )\n            }\n        }\n\n        // Update user's last login timestamp\n        await db.user.update({\n            where: {id: user.id},\n            data: {lastLoginAt: new Date()},\n        })\n\n        // Generate tokens\n        const tokens = await generateTokens(user.id)\n        if (!tokens) {\n            throw new Error('Failed to generate authentication tokens')\n        }\n\n        // Update attempt log with successful user ID if we created one\n        if (attemptLogId) {\n            try {\n                await db.auditLog.update({\n                    where: {id: attemptLogId},\n                    data: {\n                        userId: user.id,\n                        targetUserId: user.id\n                    }\n                });\n            } catch (updateError) {\n                console.error('Failed to update login attempt audit log with user ID:', updateError);\n                // This is not critical, continue\n            }\n        }\n\n        // Log successful login\n        try {\n            await createAuditLog(\n                'LOGIN_SUCCESS',\n                user.id,\n                user.id,\n                {\n                    email: user.email,\n                    role: user.role,\n                    ipAddress,\n                    userAgent,\n                    timestamp: new Date().toISOString(),\n                    tokenGenerated: true,\n                    lastLoginAt: user.lastLoginAt?.toISOString(),\n                    attemptLogId,\n                    bypassedBreachWarning: bypassBreachWarning || false\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create login success audit log:', auditLogError);\n        }\n\n        // Create response with user data\n        const response = {\n            user: {\n                id: user.id,\n                email: user.email,\n                name: user.name,\n                role: user.role,\n                lastLoginAt: user.lastLoginAt,\n            },\n            ...tokens,\n        }\n\n        // Create response and set cookies\n        const nextResponse = NextResponse.json(response)\n\n        const useSecure = shouldUseSecureCookies(request)\n\n        nextResponse.cookies.set('accessToken', tokens.accessToken, {\n            httpOnly: true,\n            secure: useSecure,\n            sameSite: 'lax',\n            maxAge: 15 * 60,\n            path: '/'\n        })\n\n        nextResponse.cookies.set('refreshToken', tokens.refreshToken, {\n            httpOnly: true,\n            secure: useSecure,\n            sameSite: 'lax',\n            maxAge: 7 * 24 * 60 * 60,\n            path: '/'\n        })\n\n        return nextResponse\n    } catch (error) {\n        // Log token generation or login completion error\n        try {\n            await createAuditLog(\n                'LOGIN_COMPLETION_ERROR',\n                userId || null, // Use null if no userId available\n                userId || null,\n                {\n                    error: error instanceof Error ? error.message : 'Unknown error',\n                    stack: error instanceof Error ? error.stack : undefined,\n                    email: (await request.json().catch(() => ({})))?.email,\n                    ipAddress,\n                    userAgent,\n                    timestamp: new Date().toISOString(),\n                    attemptLogId\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create login completion error audit log:', auditLogError);\n        }\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {error: 'Validation failed', details: error.errors},\n                {status: 400}\n            )\n        }\n\n        console.error('Login error:', error)\n        return NextResponse.json(\n            {error: 'Internal server error'},\n            {status: 500}\n        )\n    }\n}\n\n/**\n * @method GET\n * @description Method not allowed - Login endpoint only accepts POST requests\n * @response 405 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": {\n *       \"type\": \"string\",\n *       \"example\": \"Method not allowed\"\n *     }\n *   }\n * }\n */\nexport async function GET(request: Request) {\n    const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'\n    const userAgent = request.headers.get('user-agent') || 'unknown'\n\n    // Log invalid method attempt\n    try {\n        await createAuditLog(\n            'LOGIN_INVALID_METHOD',\n            null,\n            null,\n            {\n                method: 'GET',\n                ipAddress,\n                userAgent,\n                timestamp: new Date().toISOString()\n            }\n        );\n    } catch (auditLogError) {\n        console.error('Failed to create invalid method audit log:', auditLogError);\n    }\n\n    return NextResponse.json(\n        {error: 'Method not allowed'},\n        {status: 405}\n    )\n}"
  },
  {
    "path": "app/api/auth/login/second-factor/route.ts",
    "content": "// app/api/auth/login/second-factor/route.ts\nimport { NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\nimport { verifyPassword } from '@/lib/auth/password'\nimport { generateTokens } from '@/lib/auth/tokens'\nimport { z } from 'zod'\nimport { verifyAuthentication } from '@/lib/auth/webauthn'\nimport { shouldUseSecureCookies } from '@/lib/utils/cookies'\n\nconst secondFactorSchema = z.object({\n    sessionToken: z.string(),\n    secondFactorPassword: z.string().min(8).optional(),\n    passkeyResponse: z.any().optional(),\n    challenge: z.string().optional(),\n    passkeyVerified: z.boolean().optional(),\n})\n\nexport async function POST(request: Request) {\n    try {\n        const body = await request.json()\n        const {\n            sessionToken,\n            secondFactorPassword,\n            passkeyResponse,\n            challenge\n        } = secondFactorSchema.parse(body)\n\n        // Find the 2FA session\n        const session = await db.twoFactorSession.findUnique({\n            where: { id: sessionToken },\n            include: { user: true }\n        })\n\n        if (!session || session.expiresAt < new Date()) {\n            return NextResponse.json(\n                { error: 'Invalid or expired session' },\n                { status: 401 }\n            )\n        }\n\n        // Verify based on the 2FA type\n        if (session.type === 'PASSWORD_PLUS_PASSKEY' && passkeyResponse && challenge) {\n            // Verify passkey\n            const passkeys = await db.passkey.findMany({\n                where: { userId: session.user.id }\n            })\n\n            let passkeyVerified = false\n            for (const passkey of passkeys) {\n                try {\n                    const verification = await verifyAuthentication(\n                        passkeyResponse,\n                        challenge,\n                        passkey.publicKey,\n                        passkey.counter\n                    )\n\n                    if (verification.verified) {\n                        // Update passkey counter\n                        await db.passkey.update({\n                            where: { id: passkey.id },\n                            data: {\n                                counter: verification.authenticationInfo.newCounter,\n                                lastUsedAt: new Date()\n                            }\n                        })\n                        passkeyVerified = true\n                        break\n                    }\n                } catch {\n                    // this continues\n                }\n            }\n\n            if (!passkeyVerified) {\n                return NextResponse.json(\n                    { error: 'Passkey verification failed' },\n                    { status: 401 }\n                )\n            }\n        }\n\n        if (session.type === 'PASSKEY_PLUS_PASSWORD' && secondFactorPassword) {\n            const isValidPassword = await verifyPassword(\n                secondFactorPassword,\n                session.user.password\n            )\n            if (!isValidPassword) {\n                return NextResponse.json(\n                    { error: 'Invalid password' },\n                    { status: 401 }\n                )\n            }\n        }\n\n        // Delete the 2FA session\n        await db.twoFactorSession.delete({\n            where: { id: sessionToken }\n        })\n\n        // Update last login\n        await db.user.update({\n            where: { id: session.user.id },\n            data: { lastLoginAt: new Date() }\n        })\n\n        // Generate tokens\n        const tokens = await generateTokens(session.user.id)\n\n        // Create response with user data\n        const response = {\n            user: {\n                id: session.user.id,\n                email: session.user.email,\n                name: session.user.name,\n                role: session.user.role,\n            },\n            ...tokens,\n        }\n\n        // Create response and set cookies\n        const nextResponse = NextResponse.json(response)\n\n        const useSecure = shouldUseSecureCookies(request)\n\n        nextResponse.cookies.set('accessToken', tokens.accessToken, {\n            httpOnly: true,\n            secure: useSecure,\n            sameSite: 'lax',\n            maxAge: 15 * 60,\n            path: '/'\n        })\n\n        nextResponse.cookies.set('refreshToken', tokens.refreshToken, {\n            httpOnly: true,\n            secure: useSecure,\n            sameSite: 'lax',\n            maxAge: 7 * 24 * 60 * 60,\n            path: '/'\n        })\n\n        return nextResponse\n    } catch (error) {\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: 'Validation failed', details: error.errors },\n                { status: 400 }\n            )\n        }\n\n        console.error('Second factor verification error:', error)\n        return NextResponse.json(\n            { error: 'Second factor verification failed' },\n            { status: 500 }\n        )\n    }\n}"
  },
  {
    "path": "app/api/auth/logout/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport { cookies } from 'next/headers'\nimport { db } from '@/lib/db'\n\n/**\n * @method POST\n * @description Clears the access and refresh tokens, and optionally invalidates the refresh token in the database\n * @path /api/logout\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\", \"example\": true }\n *   }\n * }\n * @error 500 An unexpected error occurred while logging out\n */\nexport async function POST() {\n    try {\n        const cookieStore = await cookies()\n\n        // Clear access token cookie\n        cookieStore.delete('accessToken')\n\n        // Clear refresh token cookie\n        cookieStore.delete('refreshToken')\n\n        // Optionally, invalidate refresh token in the database if you're tracking them\n        const refreshToken = cookieStore.get('refreshToken')?.value\n        if (refreshToken) {\n            await db.refreshToken.updateMany({\n                where: { token: refreshToken },\n                data: { invalidated: true }\n            })\n        }\n\n        return NextResponse.json({ success: true })\n    } catch (error) {\n        console.error('Logout error:', error)\n        return NextResponse.json({ error: 'Logout failed' }, { status: 500 })\n    }\n}"
  },
  {
    "path": "app/api/auth/me/route.ts",
    "content": "import {NextResponse} from 'next/server'\nimport {cookies, headers} from 'next/headers'\nimport {verifyAccessToken} from '@/lib/auth/tokens'\nimport {db} from '@/lib/db'\n\n/**\n * @method GET\n * @description Verifies the access token and retrieves the user's data\n * @path /api/user\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"email\": { \"type\": \"string\" },\n *     \"name\": { \"type\": \"string\" },\n *     \"role\": { \"type\": \"string\" }\n *   }\n * }\n * @error 401 Unauthorized - Invalid or expired access token\n * @error 404 User not found\n * @error 500 An unexpected error occurred during authentication\n */\nexport async function GET() {\n    try {\n        // Check for Bearer token (API key) first\n        const headersList = await headers();\n        const authHeader = headersList.get('authorization');\n\n        if (authHeader?.startsWith('Bearer ')) {\n            const apiKey = authHeader.substring(7);\n\n            // Log the received API key for debugging\n            // console.log('Received API key:', apiKey);\n\n            // First, let's check if the key exists at all ( used for debugging )\n            // const checkKey = await db.apiKey.findFirst({\n            //     where: {\n            //         key: apiKey\n            //     }\n            // });\n\n            // console.log('Basic key check result:', checkKey);\n\n            // Now, let's try the full validation :)\n            const validApiKey = await db.apiKey.findFirst({\n                where: {\n                    key: apiKey,\n                    OR: [\n                        {expiresAt: null},\n                        {expiresAt: {gt: new Date()}}\n                    ],\n                    isRevoked: false\n                }\n            });\n\n            if (!validApiKey) {\n                return NextResponse.json({\n                    error: 'Invalid API key',\n                    details: 'Key not found or invalid'\n                }, {status: 401});\n            }\n\n            // Update last used timestamp\n            await db.apiKey.update({\n                where: {id: validApiKey.id},\n                data: {lastUsed: new Date()}\n            });\n\n            // Return admin user data for API keys\n            return NextResponse.json({\n                id: validApiKey.userId,\n                email: 'api.key@changerawr.sys',\n                role: 'ADMIN',\n                name: 'API Key'\n            });\n        }\n\n        // Fall back to cookie authentication\n        const cookieStore = await cookies();\n        const accessToken = cookieStore.get('accessToken')?.value;\n\n        if (!accessToken) {\n            return NextResponse.json({error: 'No token'}, {status: 401});\n        }\n\n        // Verify token\n        const userId = await verifyAccessToken(accessToken);\n        if (!userId) {\n            return NextResponse.json({error: 'Invalid token'}, {status: 401});\n        }\n\n        // Fetch user data\n        const user = await db.user.findUnique({\n            where: {id: userId},\n            select: {\n                id: true,\n                email: true,\n                name: true,\n                role: true\n            }\n        });\n\n        if (!user) {\n            return NextResponse.json({error: 'User not found'}, {status: 404});\n        }\n\n        return NextResponse.json(user);\n    } catch (error) {\n        console.error('Authentication error:', error);\n        return NextResponse.json({error: 'Authentication failed'}, {status: 500});\n    }\n}"
  },
  {
    "path": "app/api/auth/oauth/authorize/[providerName]/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { getOAuthLoginUrl } from '@/lib/auth/oauth';\nimport { db } from '@/lib/db';\n\n/**\n * @method GET\n * @description Redirects to OAuth provider authorization URL\n * @param {string} providerName - The name of the OAuth provider\n * @query {\n *   state: Optional state parameter for security\n *   redirect: Where to redirect after successful authentication\n * }\n * @response 302 Redirect to OAuth provider\n * @error 400 Provider name is required\n * @error 404 Provider not found\n * @error 500 An unexpected error occurred\n */\nexport async function GET(\n    request: Request,\n    { params }: { params: Promise<{ providerName: string }> }\n) {\n    try {\n        // Await params before destructuring\n        const providerName = (await params).providerName;\n        const { searchParams } = new URL(request.url);\n        // const state = searchParams.get('state') || '';\n        const redirect = searchParams.get('redirect') || '/dashboard';\n\n        // Find provider by name with case-insensitive search\n        const provider = await db.oAuthProvider.findFirst({\n            where: {\n                name: {\n                    equals: providerName,\n                    mode: 'insensitive' // Case insensitive search\n                },\n                enabled: true\n            }\n        });\n\n        if (!provider) {\n            console.log(`Provider not found: ${providerName}`);\n            return NextResponse.json(\n                { error: `Provider not found: ${providerName}` },\n                { status: 404 }\n            );\n        }\n\n        // Create a state param that includes redirect info\n        const stateObj = {\n            redirect,\n            nonce: Math.random().toString(36).substring(2, 15)\n        };\n\n        const encodedState = Buffer.from(JSON.stringify(stateObj)).toString('base64');\n\n        // Get OAuth login URL using provider ID\n        const loginUrl = await getOAuthLoginUrl(provider.id, encodedState);\n\n        // Redirect to OAuth provider\n        return NextResponse.redirect(loginUrl);\n    } catch (error) {\n        console.error('OAuth authorization error:', error);\n\n        if ((error as Error).message === 'Provider not found') {\n            return NextResponse.json(\n                { error: 'Provider not found' },\n                { status: 404 }\n            );\n        }\n\n        return NextResponse.json(\n            { error: 'Failed to initiate OAuth flow' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/auth/oauth/callback/[providerName]/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { handleOAuthCallback } from '@/lib/auth/oauth';\nimport { db } from '@/lib/db';\nimport { shouldUseSecureCookies } from '@/lib/utils/cookies';\n\n/**\n * @method GET\n * @description Handles the OAuth callback and completes authentication\n * @param {string} providerName - The name of the OAuth provider\n * @query {\n *   code: Authorization code from OAuth provider\n *   state: State parameter returned from provider\n *   error: Error message if authorization failed\n * }\n * @response 302 Redirect to dashboard or specified redirect URL\n * @error 400 Invalid request\n * @error 500 Authentication failed\n */\n// app/api/auth/oauth/callback/[providerName]/route.ts\nexport async function GET(\n    request: Request,\n    { params }: { params: Promise<{ providerName: string }> }\n) {\n    // Get the base URL for redirects - prioritize environment variable\n    const baseUrl = process.env.NEXT_PUBLIC_APP_URL || new URL(request.url).origin;\n\n    // Log full request details for comprehensive debugging\n    // console.log('OAuth Callback Request Details:', {\n    //     url: request.url,\n    //     method: request.method,\n    //     headers: Object.fromEntries(request.headers),\n    //     params: params,\n    //     baseUrl\n    // });\n\n    try {\n        const providerName = (await params).providerName;\n        const { searchParams } = new URL(request.url);\n        const code = searchParams.get('code');\n        const state = searchParams.get('state');\n        const error = searchParams.get('error');\n\n        // Log search parameters for additional context\n        console.log('Search Parameters:', {\n            code: code ? 'Present' : 'Missing',\n            state: state ? 'Present' : 'Missing',\n            error: error || 'None'\n        });\n\n        if (error) {\n            console.error('OAuth error returned from provider:', error);\n            const errorDescription = searchParams.get('error_description') || 'OAuth authentication failed';\n            return NextResponse.redirect(`${baseUrl}/login?error=${encodeURIComponent(errorDescription)}`);\n        }\n\n        if (!code) {\n            return NextResponse.redirect(`${baseUrl}/login?error=Missing+authorization+code`);\n        }\n\n        // Comprehensive provider lookup with extensive logging\n        let provider;\n        try {\n            // First, try direct ID match with case-insensitive name or ID\n            provider = await db.oAuthProvider.findFirst({\n                where: {\n                    OR: [\n                        { id: providerName },\n                        {\n                            name: {\n                                equals: providerName,\n                                mode: 'insensitive'\n                            }\n                        }\n                    ],\n                    enabled: true\n                }\n            });\n\n            // Log results of lookup\n            console.log('Provider Lookup Result:', {\n                providerFound: !!provider,\n                lookupCriteria: {\n                    providerId: providerName,\n                    enabled: true\n                }\n            });\n\n            // If no provider found, do a full database check\n            if (!provider) {\n                // Fetch all providers to help diagnose the issue\n                const allProviders = await db.oAuthProvider.findMany();\n\n                console.log('All Providers in Database:', {\n                    totalProviders: allProviders.length,\n                    providerIds: allProviders.map(p => p.id),\n                    providerNames: allProviders.map(p => p.name)\n                });\n            }\n        } catch (dbError) {\n            console.error('Database error during provider lookup:', {\n                error: (dbError as Error).message,\n                stack: (dbError as Error).stack\n            });\n            return NextResponse.redirect(`${baseUrl}/login?error=${encodeURIComponent('Database lookup failed')}`);\n        }\n\n        // Confirm provider is fully valid before proceeding\n        if (!provider) {\n            console.error(`Provider not found: ${providerName}`);\n            return NextResponse.redirect(`${baseUrl}/login?error=${encodeURIComponent(`Provider unavailable: ${providerName}`)}`);\n        }\n\n        // Comprehensive logging of found provider\n        console.log('Verified Provider Details:', {\n            id: provider.id,\n            name: provider.name,\n            enabled: provider.enabled\n        });\n\n        // Complete OAuth flow using verified provider name\n        let authResult;\n        try {\n            authResult = await handleOAuthCallback(provider.name, code);\n        } catch (authError) {\n            console.error('OAuth callback handling error:', {\n                error: (authError as Error).message,\n                stack: (authError as Error).stack,\n                providerName: provider.name\n            });\n            return NextResponse.redirect(`${baseUrl}/login?error=${encodeURIComponent('Authentication failed')}`);\n        }\n\n        // Get redirect URL from state if available\n        let redirectUrl = `${baseUrl}/dashboard`;\n        if (state) {\n            try {\n                const stateObj = JSON.parse(Buffer.from(state, 'base64').toString());\n                if (stateObj.redirect) {\n                    // Ensure redirect is absolute\n                    redirectUrl = stateObj.redirect.startsWith('/')\n                        ? `${baseUrl}${stateObj.redirect}`\n                        : stateObj.redirect;\n                }\n            } catch (e) {\n                console.error('Failed to parse state:', {\n                    error: (e as Error).message,\n                    stack: (e as Error).stack\n                });\n            }\n        }\n\n        // Create a response with cookies and redirect to the client-side handler\n        // Pass the redirectUrl as a query parameter\n        const response = NextResponse.redirect(`${baseUrl}/oauth-callback?redirect=${encodeURIComponent(redirectUrl)}`, {\n            status: 302\n        });\n\n        const useSecure = shouldUseSecureCookies(request)\n\n        response.cookies.set('accessToken', authResult.accessToken, {\n            httpOnly: true,\n            secure: useSecure,\n            sameSite: 'lax',\n            maxAge: 15 * 60,\n            path: '/'\n        });\n\n        response.cookies.set('refreshToken', authResult.refreshToken, {\n            httpOnly: true,\n            secure: useSecure,\n            sameSite: 'lax',\n            maxAge: 7 * 24 * 60 * 60,\n            path: '/'\n        });\n\n        return response;\n    } catch (error) {\n        console.error('Unexpected OAuth callback error:', {\n            error: (error as Error).message,\n            stack: (error as Error).stack\n        });\n        return NextResponse.redirect(`${baseUrl}/login?error=${encodeURIComponent('Unexpected authentication error')}`);\n    }\n}"
  },
  {
    "path": "app/api/auth/oauth/providers/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { getOAuthProviders } from '@/lib/auth/oauth';\n\n/**\n * @method GET\n * @description Retrieves a list of available OAuth providers\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"providers\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"name\": { \"type\": \"string\" },\n *           \"urlName\": { \"type\": \"string\" },\n *           \"isDefault\": { \"type\": \"boolean\" }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 500 An unexpected error occurred while fetching providers\n */\nexport async function GET() {\n    try {\n        const providers = await getOAuthProviders();\n\n        const sanitizedProviders = providers.map(provider => ({\n            id: provider.id,\n            name: provider.name,\n            urlName: provider.urlName, // Include the URL-friendly name\n            isDefault: provider.isDefault\n        }));\n\n        return NextResponse.json({ providers: sanitizedProviders });\n    } catch (error) {\n        console.error('Failed to fetch OAuth providers:', error);\n        return NextResponse.json(\n            { error: 'Failed to fetch OAuth providers' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/auth/passkeys/[id]/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { db } from '@/lib/db';\n\nexport async function DELETE(\n    request: Request,\n    { params }: { params: Promise<{ id: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n        if (!user) {\n            return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });\n        }\n\n        const { id } = await params;\n\n        // Verify the passkey belongs to the user\n        const passkey = await db.passkey.findFirst({\n            where: {\n                id,\n                userId: user.id,\n            },\n        });\n\n        if (!passkey) {\n            return NextResponse.json(\n                { error: 'Passkey not found' },\n                { status: 404 }\n            );\n        }\n\n        await db.passkey.delete({\n            where: { id },\n        });\n\n        return NextResponse.json({ success: true });\n    } catch (error) {\n        console.error('Failed to delete passkey:', error);\n        return NextResponse.json(\n            { error: 'Failed to delete passkey' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/auth/passkeys/authenticate/options/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { generateAuthenticationOptionsForUser } from '@/lib/auth/webauthn';\nimport { db } from '@/lib/db';\n\nexport async function POST(request: Request) {\n    try {\n        const body = await request.json();\n        const { email } = body;\n\n        let allowCredentials: { id: string; transports?: string[] }[] = [];\n\n        if (email) {\n            // Get user's passkeys\n            const user = await db.user.findUnique({\n                where: { email: email.toLowerCase() },\n                include: {\n                    passkeys: {\n                        select: {\n                            credentialId: true,\n                            transports: true,\n                        },\n                    },\n                },\n            });\n\n            if (user?.passkeys) {\n                allowCredentials = user.passkeys.map(p => ({\n                    id: p.credentialId,\n                    transports: p.transports || undefined,\n                }));\n            }\n        }\n\n        const options = await generateAuthenticationOptionsForUser(allowCredentials);\n\n        // Store challenge temporarily (you might use Redis or session for this)\n        return NextResponse.json({\n            options,\n            challenge: options.challenge\n        });\n    } catch (error) {\n        console.error('Failed to generate authentication options:', error);\n        return NextResponse.json(\n            { error: 'Failed to generate authentication options' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/auth/passkeys/authenticate/verify/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { verifyAuthentication } from '@/lib/auth/webauthn';\nimport { generateTokens } from '@/lib/auth/tokens';\nimport { createAuditLog } from '@/lib/utils/auditLog'; // Add this import\nimport { db } from '@/lib/db';\nimport type { AuthenticationResponseJSON } from '@simplewebauthn/types';\nimport { shouldUseSecureCookies } from '@/lib/utils/cookies';\n\nexport async function POST(request: Request) {\n    // Capture request metadata for audit logs\n    const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';\n    const userAgent = request.headers.get('user-agent') || 'unknown';\n\n    // Create an initial audit log for the passkey authentication attempt\n    let attemptLogId: string | null = null;\n    try {\n        // Use a placeholder ID since we don't know the user yet\n        const placeholderId = 'passkey-attempt-' + Date.now().toString();\n\n        const attemptLog = await db.auditLog.create({\n            data: {\n                action: 'LOGIN_PASSKEY_ATTEMPT',\n                userId: placeholderId,\n                targetUserId: placeholderId, // Using placeholder to avoid FK constraint\n                details: JSON.stringify({\n                    ipAddress,\n                    userAgent,\n                    timestamp: new Date().toISOString()\n                })\n            }\n        });\n        attemptLogId = attemptLog.id;\n    } catch (auditLogError) {\n        console.error('Failed to create passkey login attempt audit log:', auditLogError);\n        // Continue with login process even if audit logging fails\n    }\n\n    try {\n        const body = await request.json();\n        const {\n            response,\n            challenge,\n        } = body as {\n            response: AuthenticationResponseJSON;\n            challenge: string;\n        };\n\n        // Find the passkey\n        const passkey = await db.passkey.findUnique({\n            where: { credentialId: response.id },\n            include: { user: true },\n        });\n\n        if (!passkey) {\n            // Log passkey not found error\n            try {\n                await createAuditLog(\n                    'LOGIN_PASSKEY_FAILURE',\n                    'system', // No user ID available\n                    'system', // No user ID available\n                    {\n                        reason: 'PASSKEY_NOT_FOUND',\n                        credentialId: response.id,\n                        ipAddress,\n                        userAgent,\n                        timestamp: new Date().toISOString(),\n                        attemptLogId\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create passkey not found audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                { error: 'Passkey not found' },\n                { status: 400 }\n            );\n        }\n\n        // Update the original login attempt log with the correct user ID\n        if (attemptLogId) {\n            try {\n                await db.auditLog.update({\n                    where: { id: attemptLogId },\n                    data: {\n                        userId: passkey.userId,\n                        targetUserId: passkey.userId\n                    }\n                });\n            } catch (updateError) {\n                console.error('Failed to update passkey login attempt audit log:', updateError);\n            }\n        }\n\n        const verification = await verifyAuthentication(\n            response,\n            challenge,\n            passkey.publicKey,\n            passkey.counter\n        );\n\n        if (!verification.verified) {\n            // Log verification failure\n            try {\n                await createAuditLog(\n                    'LOGIN_PASSKEY_FAILURE',\n                    passkey.userId,\n                    passkey.userId,\n                    {\n                        reason: 'VERIFICATION_FAILED',\n                        credentialId: passkey.credentialId,\n                        passkeyName: passkey.name,\n                        ipAddress,\n                        userAgent,\n                        timestamp: new Date().toISOString(),\n                        attemptLogId\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create passkey verification failure audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                { error: 'Authentication verification failed' },\n                { status: 400 }\n            );\n        }\n\n        // Update counter and last used\n        await db.passkey.update({\n            where: { id: passkey.id },\n            data: {\n                counter: verification.authenticationInfo.newCounter,\n                lastUsedAt: new Date(),\n            },\n        });\n\n        const user = passkey.user;\n\n        // Check if this passkey login requires a password as second factor\n        if (user.twoFactorMode === 'PASSKEY_PLUS_PASSWORD') {\n            // Create a 2FA session\n            const session = await db.twoFactorSession.create({\n                data: {\n                    userId: user.id,\n                    type: 'PASSKEY_PLUS_PASSWORD',\n                    expiresAt: new Date(Date.now() + 10 * 60 * 1000), // 10 minutes\n                },\n            });\n\n            // Log successful first factor authentication\n            try {\n                await createAuditLog(\n                    'LOGIN_PASSKEY_FIRST_FACTOR_SUCCESS',\n                    user.id,\n                    user.id,\n                    {\n                        email: user.email,\n                        twoFactorMode: user.twoFactorMode,\n                        passkeyId: passkey.id,\n                        passkeyName: passkey.name,\n                        sessionToken: session.id,\n                        ipAddress,\n                        userAgent,\n                        timestamp: new Date().toISOString(),\n                        attemptLogId\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create passkey first factor success audit log:', auditLogError);\n            }\n\n            return NextResponse.json({\n                requiresSecondFactor: true,\n                secondFactorType: 'password',\n                sessionToken: session.id,\n                message: 'Password verification required'\n            });\n        }\n\n        // Complete regular login\n        const tokens = await generateTokens(user.id);\n\n        // Update last login\n        await db.user.update({\n            where: { id: user.id },\n            data: { lastLoginAt: new Date() }\n        });\n\n        // Log successful passkey login\n        try {\n            await createAuditLog(\n                'LOGIN_PASSKEY_SUCCESS',\n                user.id,\n                user.id,\n                {\n                    email: user.email,\n                    role: user.role,\n                    passkeyId: passkey.id,\n                    passkeyName: passkey.name,\n                    counterUpdated: verification.authenticationInfo.newCounter,\n                    ipAddress,\n                    userAgent,\n                    timestamp: new Date().toISOString(),\n                    attemptLogId\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create passkey login success audit log:', auditLogError);\n        }\n\n        const authResponse = NextResponse.json({\n            user: {\n                id: user.id,\n                email: user.email,\n                name: user.name,\n                role: user.role,\n            },\n            ...tokens,\n        });\n\n        // Set cookies\n        const useSecure = shouldUseSecureCookies(request)\n\n        authResponse.cookies.set('accessToken', tokens.accessToken, {\n            httpOnly: true,\n            secure: useSecure,\n            sameSite: 'lax',\n            maxAge: 15 * 60,\n            path: '/'\n        });\n\n        authResponse.cookies.set('refreshToken', tokens.refreshToken, {\n            httpOnly: true,\n            secure: useSecure,\n            sameSite: 'lax',\n            maxAge: 7 * 24 * 60 * 60,\n            path: '/'\n        });\n\n        return authResponse;\n    } catch (error) {\n        // Log unexpected error during passkey authentication\n        try {\n            await createAuditLog(\n                'LOGIN_PASSKEY_ERROR',\n                'system', // No user ID available or might be unknown at this point\n                'system', // No user ID available or might be unknown at this point\n                {\n                    error: error instanceof Error ? error.message : 'Unknown error',\n                    stack: error instanceof Error ? error.stack : undefined,\n                    ipAddress,\n                    userAgent,\n                    timestamp: new Date().toISOString(),\n                    attemptLogId\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create passkey error audit log:', auditLogError);\n        }\n\n        console.error('Failed to verify authentication:', error);\n        return NextResponse.json(\n            { error: 'Failed to verify authentication' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/auth/passkeys/register/options/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { generateRegistrationOptionsForUser } from '@/lib/auth/webauthn';\nimport { db } from '@/lib/db';\n\nexport async function POST() {\n    try {\n        const user = await validateAuthAndGetUser();\n        if (!user) {\n            return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });\n        }\n\n        // Get the full user data with name field\n        const fullUser = await db.user.findUnique({\n            where: { id: user.id },\n            select: {\n                id: true,\n                email: true,\n                name: true,\n            },\n        });\n\n        if (!fullUser) {\n            return NextResponse.json({ error: 'User not found' }, { status: 404 });\n        }\n\n        // Get existing passkeys to exclude\n        const existingPasskeys = await db.passkey.findMany({\n            where: { userId: user.id },\n            select: {\n                credentialId: true,\n                transports: true\n            },\n        });\n\n        const options = await generateRegistrationOptionsForUser(\n            fullUser.id,\n            fullUser.name || '',\n            fullUser.email,\n            existingPasskeys.map(p => ({\n                id: p.credentialId,\n                transports: p.transports || undefined,\n            }))\n        );\n\n        // Store challenge in user record for verification\n        await db.user.update({\n            where: { id: user.id },\n            data: {\n                lastChallenge: options.challenge,\n            },\n        });\n\n        return NextResponse.json(options);\n    } catch (error) {\n        console.error('Failed to generate registration options:', error);\n        return NextResponse.json(\n            { error: 'Failed to generate registration options' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/auth/passkeys/register/verify/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { verifyRegistration } from '@/lib/auth/webauthn';\nimport { db } from '@/lib/db';\nimport type { RegistrationResponseJSON } from '@simplewebauthn/types';\n\nexport async function POST(request: Request) {\n    try {\n        const user = await validateAuthAndGetUser();\n        if (!user) {\n            return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });\n        }\n\n        const body = await request.json();\n        const { response, name } = body as {\n            response: RegistrationResponseJSON;\n            name: string;\n        };\n\n        // Get stored challenge\n        const dbUser = await db.user.findUnique({\n            where: { id: user.id },\n            select: { lastChallenge: true },\n        });\n\n        if (!dbUser?.lastChallenge) {\n            return NextResponse.json(\n                { error: 'Challenge not found' },\n                { status: 400 }\n            );\n        }\n\n        const verification = await verifyRegistration(response, dbUser.lastChallenge);\n\n        if (!verification.verified || !verification.registrationInfo) {\n            return NextResponse.json(\n                { error: 'Registration verification failed' },\n                { status: 400 }\n            );\n        }\n\n        const { credentialPublicKey, credentialID, counter } = verification.registrationInfo;\n\n        // Create passkey record\n        const passkey = await db.passkey.create({\n            data: {\n                userId: user.id,\n                credentialId: Buffer.from(credentialID).toString('base64url'),\n                publicKey: Buffer.from(credentialPublicKey).toString('base64'),\n                counter,\n                name,\n                transports: response.response.transports || [],\n            },\n        });\n\n        return NextResponse.json({ success: true, passkey });\n    } catch (error) {\n        console.error('Failed to verify registration:', error);\n        return NextResponse.json(\n            { error: 'Failed to verify registration' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/auth/passkeys/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { db } from '@/lib/db';\n\nexport async function GET() {\n    try {\n        const user = await validateAuthAndGetUser();\n        if (!user) {\n            return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });\n        }\n\n        const passkeys = await db.passkey.findMany({\n            where: { userId: user.id },\n            select: {\n                id: true,\n                name: true,\n                createdAt: true,\n                lastUsedAt: true,\n            },\n            orderBy: { createdAt: 'desc' },\n        });\n\n        return NextResponse.json({ passkeys });\n    } catch (error) {\n        console.error('Failed to fetch passkeys:', error);\n        return NextResponse.json(\n            { error: 'Failed to fetch passkeys' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/auth/preview/route.ts",
    "content": "import {NextResponse} from 'next/server'\nimport {db} from '@/lib/db'\nimport {z} from 'zod'\nimport {getGravatarUrl} from '@/lib/utils/gravatar'\n\nconst previewSchema = z.object({\n    email: z.string().email()\n})\n\n/**\n * @method POST\n * @description Creates a preview of a user's information\n * @path /api/preview\n */\nexport async function POST(request: Request) {\n    try {\n        const body = await request.json()\n        const {email} = previewSchema.parse(body)\n        const normalizedEmail = email.toLowerCase()\n\n        // Never acknowledge system accounts\n        if (normalizedEmail.endsWith('@changerawr.sys')) {\n            // Intentionally return as if no user exists\n            return NextResponse.json(\n                {error: 'User not found'},\n                {status: 404}\n            )\n        }\n\n        const user = await db.user.findUnique({\n            where: {email: normalizedEmail},\n            select: {\n                name: true,\n                email: true,\n            },\n        })\n\n        if (!user) {\n            return NextResponse.json(\n                {error: 'User not found'},\n                {status: 404}\n            )\n        }\n\n        const avatarUrl = getGravatarUrl(user.email, 160)\n\n        return NextResponse.json({\n            ...user,\n            avatarUrl\n        })\n    } catch (error) {\n        console.error('Preview error:', error)\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {error: error.errors},\n                {status: 400}\n            )\n        }\n\n        return NextResponse.json(\n            {error: 'Internal server error'},\n            {status: 500}\n        )\n    }\n}\n"
  },
  {
    "path": "app/api/auth/refresh/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport { refreshAccessToken } from '@/lib/auth/tokens'\nimport { cookies } from 'next/headers'\nimport { shouldUseSecureCookies } from '@/lib/utils/cookies'\n\n/**\n * @method POST\n * @description Refreshes the access token by providing a valid refresh token\n * @path /api/token/refresh\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"user\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"name\": { \"type\": \"string\" },\n *         \"email\": { \"type\": \"string\" },\n *         \"role\": { \"type\": \"string\" },\n *         \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *       }\n *     },\n *     \"accessToken\": { \"type\": \"string\" },\n *     \"refreshToken\": { \"type\": \"string\" }\n *   }\n * }\n * @error 400 Invalid or expired refresh token\n * @error 401 Unauthorized - No refresh token provided\n * @error 500 An unexpected error occurred while refreshing the token\n */\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport async function POST(request: Request) {\n    try {\n        const cookieStore = await cookies()\n        const refreshTokenCookie = await cookieStore.get('refreshToken')\n\n        if (!refreshTokenCookie?.value) {\n            return NextResponse.json({\n                error: 'No refresh token provided'\n            }, { status: 401 })\n        }\n\n        // Attempt to refresh the token\n        const result = await refreshAccessToken(refreshTokenCookie.value)\n\n        if (!result) {\n            const response = NextResponse.json({\n                error: 'Invalid or expired refresh token'\n            }, { status: 401 })\n\n            response.cookies.delete('refreshToken')\n            response.cookies.delete('accessToken')\n            return response\n        }\n\n        // Create response with new tokens\n        const response = NextResponse.json({\n            user: result.user\n        })\n\n        const useSecure = shouldUseSecureCookies(request)\n\n        response.cookies.set('accessToken', result.accessToken, {\n            httpOnly: true,\n            secure: useSecure,\n            sameSite: 'lax',\n            maxAge: 15 * 60,\n            path: '/'\n        })\n\n        response.cookies.set('refreshToken', result.refreshToken, {\n            httpOnly: true,\n            secure: useSecure,\n            sameSite: 'lax',\n            maxAge: 7 * 24 * 60 * 60,\n            path: '/'\n        })\n\n        return response\n    } catch (error) {\n        console.error('Token refresh error:', error)\n\n        const response = NextResponse.json({\n            error: 'Failed to refresh token',\n            details: process.env.NODE_ENV === 'development'\n                ? { message: error instanceof Error ? error.message : 'Unknown error' }\n                : undefined\n        }, { status: 500 })\n\n        response.cookies.delete('refreshToken')\n        response.cookies.delete('accessToken')\n        return response\n    }\n}"
  },
  {
    "path": "app/api/auth/register/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\nimport { hashPassword } from '@/lib/auth/password'\nimport { z } from 'zod'\nimport { checkRateLimit } from '@/lib/utils/rate-limit'\n\nconst registerSchema = z.object({\n    token: z.string(),\n    name: z.string().min(2),\n    password: z.string().min(8),\n})\n\n/**\n * @method POST\n * @description Registers a new user using an invitation token\n * @path /api/auth/register\n * @request {json}\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\", \"example\": true }\n *   }\n * }\n * @error 400 Invalid input - Token, name, or password is missing or invalid\n * @error 400 Invalid or expired invitation\n * @error 409 User already exists\n * @error 500 An unexpected error occurred during registration\n */\nexport async function POST(request: NextRequest) {\n    try {\n        // Rate limit: 10 registration attempts per hour per IP\n        const ip = request.headers.get('x-forwarded-for')?.split(',')[0].trim() || request.headers.get('x-real-ip') || 'unknown'\n        const rateLimit = checkRateLimit(`register:${ip}`, 10, 60 * 60 * 1000)\n        if (!rateLimit.allowed) {\n            return NextResponse.json(\n                { error: 'Too many registration attempts. Please try again later.' },\n                { status: 429 }\n            )\n        }\n\n        const body = await request.json()\n        const { token, name, password } = registerSchema.parse(body)\n\n        // Start a transaction\n        return await db.$transaction(async (tx) => {\n            // Find and validate invitation\n            const invitation = await tx.invitationLink.findFirst({\n                where: {\n                    token,\n                    usedAt: null,\n                    expiresAt: {\n                        gt: new Date(),\n                    },\n                },\n            })\n\n            if (!invitation) {\n                return NextResponse.json(\n                    { error: 'Invalid or expired invitation' },\n                    { status: 400 }\n                )\n            }\n\n            // Block registration for system accounts\n            if (invitation.email.endsWith('@changerawr.sys')) {\n                return NextResponse.json(\n                    { error: 'Cannot register with system email addresses' },\n                    { status: 400 }\n                )\n            }\n\n            // Hash password\n            const hashedPassword = await hashPassword(password)\n\n            // Check if user already exists\n            const existingUser = await tx.user.findFirst({\n                where: { email: invitation.email },\n            })\n\n            if (existingUser) {\n                return NextResponse.json(\n                    { error: 'User already exists' },\n                    { status: 409 }\n                )\n            }\n\n            // Create user\n            const user = await tx.user.create({\n                data: {\n                    email: invitation.email,\n                    name,\n                    password: hashedPassword,\n                    role: invitation.role,\n                },\n            })\n\n            // Mark invitation as used\n            await tx.invitationLink.update({\n                where: { id: invitation.id },\n                data: { usedAt: new Date() },\n            })\n\n            // Create default settings\n            await tx.settings.create({\n                data: {\n                    userId: user.id,\n                    theme: 'light',\n                },\n            })\n\n            return NextResponse.json({ success: true })\n        })\n    } catch (error) {\n        console.error('Registration error:', error)\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: error.errors },\n                { status: 400 }\n            )\n        }\n\n        return NextResponse.json(\n            { error: 'Registration failed' },\n            { status: 500 }\n        )\n    }\n}"
  },
  {
    "path": "app/api/auth/reset-password/[token]/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server';\nimport { z } from 'zod';\nimport {\n    validatePasswordResetToken,\n    resetPassword\n} from '@/lib/services/auth/password-reset';\nimport { checkRateLimit } from '@/lib/utils/rate-limit';\n\n// Validation schema for password reset\nconst resetPasswordSchema = z.object({\n    password: z.string().min(8, 'Password must be at least 8 characters'),\n    confirmPassword: z.string()\n}).refine(data => data.password === data.confirmPassword, {\n    message: \"Passwords don't match\",\n    path: [\"confirmPassword\"]\n});\n\n/**\n * @method GET\n * @description Validates a password reset token\n * @path /api/auth/reset-password/[token]\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"valid\": {\n *       \"type\": \"boolean\"\n *     },\n *     \"email\": {\n *       \"type\": \"string\"\n *     }\n *   }\n * }\n * @error 400 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"valid\": {\n *       \"type\": \"boolean\",\n *       \"example\": false\n *     },\n *     \"message\": {\n *       \"type\": \"string\",\n *       \"example\": \"Invalid or expired reset token\"\n *     }\n *   }\n * }\n */\nexport async function GET(\n    request: NextRequest,\n    context: { params: Promise<{ token: string }> }\n) {\n    // Rate limit token validation attempts to prevent enumeration\n    const ip = request.headers.get('x-forwarded-for')?.split(',')[0].trim() || 'unknown'\n    const rateLimit = checkRateLimit(`reset-token-check:${ip}`, 20, 15 * 60 * 1000)\n    if (!rateLimit.allowed) {\n        return NextResponse.json({ valid: false, error: 'Too many requests' }, { status: 429 })\n    }\n\n    const { token } = await context.params;\n\n    const validation = await validatePasswordResetToken(token);\n\n    if (!validation.valid) {\n        return NextResponse.json(validation, { status: 400 });\n    }\n\n    return NextResponse.json({\n        valid: true,\n        email: validation.email,\n    });\n}\n\n/**\n * @method POST\n * @description Resets a user's password using a valid token\n * @path /api/auth/reset-password/[token]\n * @body {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"password\": {\n *       \"type\": \"string\",\n *       \"minLength\": 8,\n *       \"description\": \"New password\"\n *     },\n *     \"confirmPassword\": {\n *       \"type\": \"string\",\n *       \"description\": \"Confirm new password\"\n *     }\n *   },\n *   \"required\": [\"password\", \"confirmPassword\"]\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": {\n *       \"type\": \"boolean\"\n *     },\n *     \"message\": {\n *       \"type\": \"string\"\n *     }\n *   }\n * }\n * @error 400 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": {\n *       \"type\": \"string\"\n *     }\n *   }\n * }\n * @error 500 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": {\n *       \"type\": \"string\"\n *     }\n *   }\n * }\n */\nexport async function POST(\n    request: NextRequest,\n    context: { params: Promise<{ token: string }> }\n) {\n    try {\n        // Rate limit: 5 password reset submissions per 15 minutes per IP\n        const ip = request.headers.get('x-forwarded-for')?.split(',')[0].trim() || 'unknown'\n        const rateLimit = checkRateLimit(`reset-password:${ip}`, 5, 15 * 60 * 1000)\n        if (!rateLimit.allowed) {\n            return NextResponse.json({ error: 'Too many attempts. Please try again later.' }, { status: 429 })\n        }\n        const { token } = await context.params;\n        const body = await request.json();\n\n        const { password } = resetPasswordSchema.parse(body);\n\n        const result = await resetPassword(token, password);\n\n        if (!result.success) {\n            return NextResponse.json({ error: result.message }, { status: 400 });\n        }\n\n        return NextResponse.json(result);\n    } catch (error) {\n        console.error('Password reset error:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: error.errors[0].message },\n                { status: 400 }\n            );\n        }\n\n        return NextResponse.json(\n            { error: 'Failed to reset password' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/auth/reset-password/request/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { createPasswordResetAndSendEmail } from '@/lib/services/auth/password-reset';\n\n/**\n * @method POST\n * @description Initiates a password reset for the currently logged-in user\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\" },\n *     \"message\": { \"type\": \"string\" }\n *   }\n * }\n * @error 401 Unauthorized - User not authenticated\n * @error 500 An unexpected error occurred while initiating password reset\n */\nexport async function POST() {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        if (!user) {\n            return NextResponse.json(\n                { success: false, error: 'User not authenticated' },\n                { status: 401 }\n            );\n        }\n\n        // Use the existing password reset service\n        const result = await createPasswordResetAndSendEmail({\n            email: user.email\n        });\n\n        if (!result.success) {\n            return NextResponse.json(\n                { success: false, error: result.message },\n                { status: 500 }\n            );\n        }\n\n        return NextResponse.json({\n            success: true,\n            message: 'Password reset email sent successfully'\n        });\n    } catch (error) {\n        console.error('Failed to initiate password reset:', error);\n        return NextResponse.json(\n            { success: false, error: 'Failed to initiate password reset' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/auth/saml/authorize/[providerName]/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { getSAMLLoginUrl } from '@/lib/auth/saml';\n\n/**\n * @method GET\n * @description Initiates SP-initiated SAML SSO by redirecting to the IdP\n */\nexport async function GET(\n    request: Request,\n    { params }: { params: Promise<{ providerName: string }> }\n) {\n    const baseUrl = process.env.NEXT_PUBLIC_APP_URL || new URL(request.url).origin;\n\n    try {\n        const { providerName } = await params;\n        const { searchParams } = new URL(request.url);\n        const redirect = searchParams.get('redirect') || `${baseUrl}/dashboard`;\n\n        // Build RelayState with redirect URL and a nonce\n        const relayState = Buffer.from(\n            JSON.stringify({ redirect, nonce: crypto.randomUUID() })\n        ).toString('base64');\n\n        const loginUrl = await getSAMLLoginUrl(providerName, relayState);\n        return NextResponse.redirect(loginUrl, { status: 302 });\n    } catch (error) {\n        console.error('SAML authorize error:', error);\n        return NextResponse.redirect(\n            `${baseUrl}/login?error=${encodeURIComponent('SAML provider not available')}`\n        );\n    }\n}\n"
  },
  {
    "path": "app/api/auth/saml/callback/[providerName]/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { handleSAMLCallback } from '@/lib/auth/saml';\nimport { shouldUseSecureCookies } from '@/lib/utils/cookies';\n\n/**\n * @method POST\n * @description ACS (Assertion Consumer Service) endpoint — handles SAMLResponse from IdP\n */\nexport async function POST(\n    request: Request,\n    { params }: { params: Promise<{ providerName: string }> }\n) {\n    const baseUrl = process.env.NEXT_PUBLIC_APP_URL || new URL(request.url).origin;\n\n    try {\n        const { providerName } = await params;\n\n        // Parse URL-encoded body from IdP POST binding\n        const contentType = request.headers.get('content-type') || '';\n        let samlResponse = '';\n        let relayState = '';\n\n        if (contentType.includes('application/x-www-form-urlencoded')) {\n            const text = await request.text();\n            const body = new URLSearchParams(text);\n            samlResponse = body.get('SAMLResponse') || '';\n            relayState = body.get('RelayState') || '';\n        } else {\n            const body = await request.json().catch(() => ({})) as Record<string, string>;\n            samlResponse = body.SAMLResponse || '';\n            relayState = body.RelayState || '';\n        }\n\n        if (!samlResponse) {\n            return NextResponse.redirect(\n                `${baseUrl}/login?error=${encodeURIComponent('Missing SAMLResponse')}`\n            );\n        }\n\n        const authResult = await handleSAMLCallback(providerName, samlResponse);\n\n        // Parse redirect from RelayState\n        let redirectUrl = `${baseUrl}/dashboard`;\n        if (relayState) {\n            try {\n                const stateObj = JSON.parse(Buffer.from(relayState, 'base64').toString());\n                if (stateObj.redirect) {\n                    redirectUrl = stateObj.redirect.startsWith('/')\n                        ? `${baseUrl}${stateObj.redirect}`\n                        : stateObj.redirect;\n                }\n            } catch {\n                // ignore malformed relay state\n            }\n        }\n\n        const response = NextResponse.redirect(\n            `${baseUrl}/oauth-callback?redirect=${encodeURIComponent(redirectUrl)}`,\n            { status: 302 }\n        );\n\n        const useSecure = shouldUseSecureCookies(request);\n\n        response.cookies.set('accessToken', authResult.accessToken, {\n            httpOnly: true,\n            secure: useSecure,\n            sameSite: 'lax',\n            maxAge: 15 * 60,\n            path: '/',\n        });\n\n        response.cookies.set('refreshToken', authResult.refreshToken, {\n            httpOnly: true,\n            secure: useSecure,\n            sameSite: 'lax',\n            maxAge: 7 * 24 * 60 * 60,\n            path: '/',\n        });\n\n        return response;\n    } catch (error) {\n        console.error('SAML callback error:', error);\n        return NextResponse.redirect(\n            `${baseUrl}/login?error=${encodeURIComponent('SAML authentication failed')}`\n        );\n    }\n}\n"
  },
  {
    "path": "app/api/auth/saml/metadata/[providerName]/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { getSAMLMetadata } from '@/lib/auth/saml';\n\n/**\n * @method GET\n * @description Returns SP metadata XML for IdP registration\n */\nexport async function GET(\n    request: Request,\n    { params }: { params: Promise<{ providerName: string }> }\n) {\n    try {\n        const { providerName } = await params;\n        const metadata = await getSAMLMetadata(providerName);\n\n        return new NextResponse(metadata, {\n            headers: {\n                'Content-Type': 'application/xml',\n                'Cache-Control': 'public, max-age=3600',\n            },\n        });\n    } catch (error) {\n        console.error('SAML metadata error:', error);\n        return NextResponse.json({ error: 'Provider not found' }, { status: 404 });\n    }\n}\n"
  },
  {
    "path": "app/api/auth/saml/providers/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { db } from '@/lib/db';\n\n/**\n * @method GET\n * @description Returns enabled SAML providers for the login page\n */\nexport async function GET() {\n    try {\n        const providers = await db.sAMLProvider.findMany({\n            where: { enabled: true },\n            select: { id: true, name: true, isDefault: true },\n            orderBy: { name: 'asc' },\n        });\n\n        return NextResponse.json({ providers });\n    } catch (error) {\n        console.error('Failed to fetch SAML providers:', error);\n        return NextResponse.json({ providers: [] });\n    }\n}\n"
  },
  {
    "path": "app/api/auth/security-settings/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { db } from '@/lib/db';\nimport { z } from 'zod';\n\nconst updateSchema = z.object({\n    twoFactorMode: z.enum(['NONE', 'PASSKEY_PLUS_PASSWORD', 'PASSWORD_PLUS_PASSKEY'])\n});\n\nexport async function GET() {\n    try {\n        const user = await validateAuthAndGetUser();\n        if (!user) {\n            return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });\n        }\n\n        const fullUser = await db.user.findUnique({\n            where: { id: user.id },\n            select: { twoFactorMode: true }\n        });\n\n        return NextResponse.json({\n            twoFactorMode: fullUser?.twoFactorMode || 'NONE'\n        });\n    } catch (error) {\n        console.error('Failed to fetch security settings:', error);\n        return NextResponse.json(\n            { error: 'Failed to fetch security settings' },\n            { status: 500 }\n        );\n    }\n}\n\nexport async function PATCH(request: Request) {\n    try {\n        const user = await validateAuthAndGetUser();\n        if (!user) {\n            return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });\n        }\n\n        const body = await request.json();\n        const { twoFactorMode } = updateSchema.parse(body);\n\n        // Check if user has passkeys before enabling 2FA\n        if (twoFactorMode !== 'NONE') {\n            const passkeys = await db.passkey.count({\n                where: { userId: user.id }\n            });\n\n            if (passkeys === 0) {\n                return NextResponse.json(\n                    { error: 'At least one passkey is required to enable additional security' },\n                    { status: 400 }\n                );\n            }\n        }\n\n        const updatedUser = await db.user.update({\n            where: { id: user.id },\n            data: { twoFactorMode },\n            select: { twoFactorMode: true }\n        });\n\n        return NextResponse.json({ twoFactorMode: updatedUser.twoFactorMode });\n    } catch (error) {\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: 'Invalid security settings', details: error.errors },\n                { status: 400 }\n            );\n        }\n\n        console.error('Failed to update security settings:', error);\n        return NextResponse.json(\n            { error: 'Failed to update security settings' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/auth/settings/route.ts",
    "content": "// app/api/auth/settings/route.ts\nimport { NextResponse } from 'next/server';\nimport { db } from '@/lib/db';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\n\n/**\n * @method GET\n * @description Retrieves or creates the user's settings\n * @path /api/settings\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"userId\": { \"type\": \"string\" },\n *     \"theme\": { \"type\": \"string\", \"enum\": [\"light\", \"dark\"] },\n *     \"name\": { \"type\": \"string\" },\n *     \"enableNotifications\": { \"type\": \"boolean\" }\n *   }\n * }\n * @error 401 Unauthorized - User not authenticated\n * @error 500 An unexpected error occurred while fetching settings\n */\nexport async function GET() {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        const settings = await db.settings.findUnique({\n            where: { userId: user.id },\n        });\n\n        if (!settings) {\n            // Create default settings if they don't exist\n            const defaultSettings = await db.settings.create({\n                data: {\n                    userId: user.id,\n                    theme: 'light',\n                    enableNotifications: true,\n                },\n            });\n            return NextResponse.json(defaultSettings);\n        }\n\n        return NextResponse.json(settings);\n    } catch (error) {\n        console.error('Failed to fetch settings:', error);\n        return NextResponse.json(\n            { error: 'Failed to fetch settings' },\n            { status: 500 }\n        );\n    }\n}\n\n/**\n * @method PATCH\n * @description Updates the user's settings\n * @path /api/settings\n * @request {json}\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"userId\": { \"type\": \"string\" },\n *     \"theme\": { \"type\": \"string\", \"enum\": [\"light\", \"dark\"] },\n *     \"name\": { \"type\": \"string\" },\n *     \"enableNotifications\": { \"type\": \"boolean\" }\n *   }\n * }\n * @error 400 Invalid request data\n * @error 401 Unauthorized - User not authenticated\n * @error 500 An unexpected error occurred while updating settings\n */\nexport async function PATCH(request: Request) {\n    try {\n        const user = await validateAuthAndGetUser();\n        const data = await request.json();\n\n        // Validate the request data\n        const validUpdates: {\n            theme?: string;\n            name?: string;\n            enableNotifications?: boolean;\n            timezone?: string | null;\n        } = {};\n\n        if (data.theme && ['light', 'dark'].includes(data.theme)) {\n            validUpdates.theme = data.theme;\n        }\n\n        if (data.enableNotifications !== undefined) {\n            validUpdates.enableNotifications = Boolean(data.enableNotifications);\n        }\n\n        if (data.timezone !== undefined) {\n            // null clears the override (use system default), string sets it\n            validUpdates.timezone = data.timezone === null ? null : String(data.timezone);\n        }\n\n        if (data.name !== undefined) {\n            // Update user name\n            await db.user.update({\n                where: { id: user.id },\n                data: { name: data.name },\n            });\n        }\n\n        if (Object.keys(validUpdates).length > 0) {\n            // Update settings\n            const settings = await db.settings.upsert({\n                where: { userId: user.id },\n                create: {\n                    userId: user.id,\n                    theme: validUpdates.theme || 'light',\n                    enableNotifications: validUpdates.enableNotifications !== undefined ? validUpdates.enableNotifications : true,\n                },\n                update: validUpdates,\n            });\n\n            return NextResponse.json(settings);\n        }\n\n        return NextResponse.json(\n            { error: 'No valid updates provided' },\n            { status: 400 }\n        );\n    } catch (error) {\n        console.error('Failed to update settings:', error);\n        return NextResponse.json(\n            { error: 'Failed to update settings' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/auth/validate/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server';\nimport {verifyAccessToken} from '@/lib/auth/tokens';\nimport {db} from '@/lib/db';\n\n/**\n * @method GET\n * @description Validate JWT access token and return user information\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"valid\": {\n *       \"type\": \"boolean\",\n *       \"example\": true\n *     },\n *     \"user\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"email\": { \"type\": \"string\" },\n *         \"name\": { \"type\": \"string\" },\n *         \"role\": { \"type\": \"string\" }\n *       }\n *     },\n *     \"expires_in\": {\n *       \"type\": \"number\",\n *       \"description\": \"Seconds until token expires\"\n *     }\n *   }\n * }\n * @error 401 Invalid or expired token\n * @error 404 User not found\n * @error 500 Internal server error\n * @secure bearerAuth\n */\nexport async function GET(request: NextRequest) {\n    try {\n        // Extract Bearer token from Authorization header\n        const authHeader = request.headers.get('authorization');\n\n        if (!authHeader || !authHeader.startsWith('Bearer ')) {\n            return NextResponse.json(\n                {\n                    valid: false,\n                    error: 'Missing or invalid Authorization header'\n                },\n                {status: 401}\n            );\n        }\n\n        const token = authHeader.substring(7); // Remove 'Bearer ' prefix\n\n        // Verify the access token\n        const userId = await verifyAccessToken(token);\n\n        if (!userId) {\n            return NextResponse.json(\n                {\n                    valid: false,\n                    error: 'Invalid or expired token'\n                },\n                {status: 401}\n            );\n        }\n\n        // Fetch user data\n        const user = await db.user.findUnique({\n            where: {id: userId},\n            select: {\n                id: true,\n                email: true,\n                name: true,\n                role: true,\n            },\n        });\n\n        if (!user) {\n            return NextResponse.json(\n                {\n                    valid: false,\n                    error: 'User not found'\n                },\n                {status: 404}\n            );\n        }\n\n        // Calculate token expiration (tokens are valid for 15 minutes)\n        // This is an approximation since we don't have exact expiration from JWT\n        const expiresIn = 15 * 60; // 15 minutes in seconds\n\n        return NextResponse.json({\n            valid: true,\n            user,\n            expires_in: expiresIn,\n        });\n\n    } catch (error) {\n        console.error('Token validation error:', error);\n\n        return NextResponse.json(\n            {\n                valid: false,\n                error: 'Token validation failed'\n            },\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/avatar/[hash]/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server';\n\n/**\n * Proxy Gravatar images to avoid tracking prevention blocks\n * GET /api/avatar/[hash]\n */\nexport async function GET(\n    req: NextRequest,\n    context: { params: Promise<{ hash: string }> }\n) {\n    try {\n        const { hash } = await context.params;\n\n        // Validate hash format (MD5 hex)\n        if (!/^[a-f0-9]{32}$/i.test(hash)) {\n            return new NextResponse('Invalid avatar hash', { status: 400 });\n        }\n\n        // Get size from query params, default to 200, max 2048\n        const { searchParams } = new URL(req.url);\n        const sizeParam = searchParams.get('s') || '200';\n        const size = Math.min(parseInt(sizeParam, 10) || 200, 2048);\n\n        // Fetch from Gravatar\n        const gravatarUrl = `https://www.gravatar.com/avatar/${hash}?d=mp&s=${size}`;\n        const response = await fetch(gravatarUrl, {\n            headers: {\n                'User-Agent': 'Changerawr'\n            }\n        });\n\n        if (!response.ok) {\n            return new NextResponse('Avatar not found', { status: 404 });\n        }\n\n        // Get image data\n        const imageBuffer = await response.arrayBuffer();\n        const contentType = response.headers.get('content-type') || 'image/jpeg';\n\n        // Return with proper caching headers\n        return new NextResponse(imageBuffer, {\n            status: 200,\n            headers: {\n                'Content-Type': contentType,\n                'Cache-Control': 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=604800',\n                'CDN-Cache-Control': 'public, max-age=604800',\n            },\n        });\n    } catch (error) {\n        console.error('Error proxying avatar:', error);\n        return new NextResponse('Error loading avatar', { status: 500 });\n    }\n}\n"
  },
  {
    "path": "app/api/changelog/[projectId]/entries/all/route.ts",
    "content": "import {NextResponse} from 'next/server'\nimport {db} from '@/lib/db'\n\nconst ITEMS_PER_PAGE = 10\n\n// Define type for search params\ntype SortOrder = 'asc' | 'desc'\n\n// Define interfaces for the where conditions\ninterface BaseWhereClause {\n    changelogId: string;\n    publishedAt: { not: null };\n    OR?: SearchCondition[] | CursorCondition[];\n    tags?: {\n        some: {\n            id: { in: string[] }\n        }\n    };\n}\n\ninterface SearchCondition {\n    title?: { contains: string; mode: 'insensitive' };\n    content?: { contains: string; mode: 'insensitive' };\n}\n\ninterface CursorCondition {\n    publishedAt: { lt: Date } | { gt: Date } | { equals: Date };\n    id?: { lt: string } | { gt: string };\n}\n\n/**\n * @method GET\n * @description Fetches the changelog entries with FULL CONTENT for a given public project\n * @query {\n *   projectId: String, required\n *   cursor?: String, optional\n *   search?: String, optional - Search in title and content\n *   tags?: String, optional - Comma-separated list of tag IDs\n *   sort?: 'newest'|'oldest', optional - Default is 'newest'\n * }\n */\nexport async function GET(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    return await (async () => {\n        try {\n            const {params} = context\n            const {projectId} = await (async () => params)();\n            const {searchParams} = new URL(request.url)\n\n            // Get pagination, search, and filter parameters\n            const cursor = searchParams.get('cursor')\n            const search = searchParams.get('search')\n            const tagsParam = searchParams.get('tags')\n            const sortParam = searchParams.get('sort') || 'newest'\n\n            const tagIds = tagsParam ? tagsParam.split(',') : []\n            const sortOrder: SortOrder = sortParam === 'oldest' ? 'asc' : 'desc'\n\n            // Get project and changelog\n            const project = await db.project.findUnique({\n                where: {\n                    id: projectId,\n                    isPublic: true\n                },\n                select: {\n                    id: true,\n                    name: true,\n                    changelog: {\n                        select: {\n                            id: true\n                        }\n                    },\n                    emailConfig: {\n                        select: {\n                            enabled: true\n                        }\n                    }\n                }\n            })\n\n            if (!project?.changelog) {\n                return NextResponse.json(\n                    {error: 'Changelog not found or not public'},\n                    {status: 404}\n                )\n            }\n\n            // Build base where clause\n            const baseWhere: BaseWhereClause = {\n                changelogId: project.changelog.id,\n                publishedAt: {not: null}\n            }\n\n            // Add search condition if present\n            if (search) {\n                const searchConditions: SearchCondition[] = [\n                    {title: {contains: search, mode: 'insensitive'}},\n                    {content: {contains: search, mode: 'insensitive'}}\n                ]\n                baseWhere.OR = searchConditions\n            }\n\n            // Add tags filter if present\n            if (tagIds.length > 0) {\n                baseWhere.tags = {\n                    some: {\n                        id: {in: tagIds}\n                    }\n                }\n            }\n\n            // Build cursor-based pagination where clause\n            let where: BaseWhereClause = {...baseWhere}\n\n            if (cursor) {\n                const cursorEntry = await db.changelogEntry.findUnique({\n                    where: {id: cursor},\n                    select: {publishedAt: true}\n                })\n\n                if (cursorEntry?.publishedAt) {\n                    // Different cursor logic based on sort order\n                    const cursorConditions: CursorCondition[] = sortOrder === 'desc'\n                        ? [\n                            {publishedAt: {lt: cursorEntry.publishedAt}},\n                            {\n                                publishedAt: {equals: cursorEntry.publishedAt},\n                                id: {lt: cursor}\n                            }\n                        ]\n                        : [\n                            {publishedAt: {gt: cursorEntry.publishedAt}},\n                            {\n                                publishedAt: {equals: cursorEntry.publishedAt},\n                                id: {gt: cursor}\n                            }\n                        ]\n\n                    // Create a new where clause with cursor conditions\n                    where = {\n                        ...baseWhere,\n                        OR: cursorConditions\n                    }\n                }\n            }\n\n            // Get entries with cursor-based pagination - INCLUDING FULL CONTENT\n            const entries = await db.changelogEntry.findMany({\n                where,\n                take: ITEMS_PER_PAGE + 1,\n                orderBy: [\n                    {publishedAt: sortOrder},\n                    {id: sortOrder}\n                ],\n                select: {\n                    id: true,\n                    title: true,\n                    content: true, // FULL CONTENT instead of excerpt\n                    excerpt: true,\n                    version: true,\n                    publishedAt: true,\n                    createdAt: true,\n                    updatedAt: true,\n                    tags: {\n                        select: {\n                            id: true,\n                            name: true,\n                            color: true\n                        }\n                    }\n                }\n            })\n\n            let nextCursor: string | undefined\n\n            // If we got more items than requested, we have a next page\n            if (entries.length > ITEMS_PER_PAGE) {\n                const nextItem = entries.pop()\n                nextCursor = nextItem?.id\n            }\n\n            return NextResponse.json({\n                project: {\n                    id: project.id,\n                    name: project.name,\n                    emailNotificationsEnabled: project.emailConfig?.enabled || false\n                },\n                items: entries,\n                nextCursor\n            })\n        } catch (error) {\n            console.error('Error fetching changelog entries:', error)\n            return NextResponse.json(\n                {error: 'Failed to fetch changelog entries'},\n                {status: 500}\n            )\n        }\n    })()\n}\n"
  },
  {
    "path": "app/api/changelog/[projectId]/entries/route.ts",
    "content": "import {NextResponse} from 'next/server'\nimport {db} from '@/lib/db'\n\nconst ITEMS_PER_PAGE = 10\n\n// Define type for search params\ntype SortOrder = 'asc' | 'desc'\n\n// Define interfaces for the where conditions\ninterface BaseWhereClause {\n    changelogId: string;\n    publishedAt: { not: null };\n    OR?: SearchCondition[] | CursorCondition[];\n    tags?: {\n        some: {\n            id: { in: string[] }\n        }\n    };\n}\n\ninterface SearchCondition {\n    title?: { contains: string; mode: 'insensitive' };\n    content?: { contains: string; mode: 'insensitive' };\n}\n\ninterface CursorCondition {\n    publishedAt: { lt: Date } | { gt: Date } | { equals: Date };\n    id?: { lt: string } | { gt: string };\n}\n\n/**\n * @method GET\n * @description Fetches the changelog entries for a given public project, with filtering, searching and pagination\n * @query {\n *   projectId: String, required\n *   cursor?: String, optional\n *   search?: String, optional - Search in title and content\n *   tags?: String, optional - Comma-separated list of tag IDs\n *   sort?: 'newest'|'oldest', optional - Default is 'newest'\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"project\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"name\": { \"type\": \"string\" }\n *       }\n *     },\n *     \"items\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"title\": { \"type\": \"string\" },\n *           \"content\": { \"type\": \"string\" },\n *           \"version\": { \"type\": \"string\" },\n *           \"publishedAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *           \"tags\": {\n *             \"type\": \"array\",\n *             \"items\": {\n *               \"type\": \"object\",\n *               \"properties\": {\n *                 \"id\": { \"type\": \"string\" },\n *                 \"name\": { \"type\": \"string\" },\n *                 \"color\": { \"type\": \"string\", \"nullable\": true }\n *               }\n *             }\n *           }\n *         }\n *       }\n *     },\n *     \"nextCursor\": { \"type\": \"string\", \"nullable\": true }\n *   }\n * }\n * @error 400 Invalid request data\n * @error 403 Unauthorized - User does not have 'ADMIN' role or the project is not public\n * @error 404 Project not found\n * @error 500 An unexpected error occurred while fetching the changelog entries\n */\nexport async function GET(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    return await (async () => {\n        try {\n            const {params} = context\n            const {projectId} = await (async () => params)();\n            const {searchParams} = new URL(request.url)\n\n            // Get pagination, search, and filter parameters\n            const cursor = searchParams.get('cursor')\n            const search = searchParams.get('search')\n            const tagsParam = searchParams.get('tags')\n            const sortParam = searchParams.get('sort') || 'newest'\n\n            const tagIds = tagsParam ? tagsParam.split(',') : []\n            const sortOrder: SortOrder = sortParam === 'oldest' ? 'asc' : 'desc'\n\n            // Get project and changelog\n            const project = await db.project.findUnique({\n                where: {\n                    id: projectId,\n                    isPublic: true\n                },\n                select: {\n                    id: true,\n                    name: true,\n                    changelog: {\n                        select: {\n                            id: true\n                        }\n                    },\n                    emailConfig: {\n                        select: {\n                            enabled: true\n                        }\n                    }\n                }\n            })\n\n            if (!project?.changelog) {\n                return NextResponse.json(\n                    {error: 'Changelog not found or not public'},\n                    {status: 404}\n                )\n            }\n\n            // Build base where clause\n            const baseWhere: BaseWhereClause = {\n                changelogId: project.changelog.id,\n                publishedAt: {not: null}\n            }\n\n            // Add search condition if present\n            if (search) {\n                const searchConditions: SearchCondition[] = [\n                    {title: {contains: search, mode: 'insensitive'}},\n                    {content: {contains: search, mode: 'insensitive'}}\n                ]\n                baseWhere.OR = searchConditions\n            }\n\n            // Add tags filter if present\n            if (tagIds.length > 0) {\n                baseWhere.tags = {\n                    some: {\n                        id: {in: tagIds}\n                    }\n                }\n            }\n\n            // Build cursor-based pagination where clause\n            let where: BaseWhereClause = {...baseWhere}\n\n            if (cursor) {\n                const cursorEntry = await db.changelogEntry.findUnique({\n                    where: {id: cursor},\n                    select: {publishedAt: true}\n                })\n\n                if (cursorEntry?.publishedAt) {\n                    // Different cursor logic based on sort order\n                    const cursorConditions: CursorCondition[] = sortOrder === 'desc'\n                        ? [\n                            {publishedAt: {lt: cursorEntry.publishedAt}},\n                            {\n                                publishedAt: {equals: cursorEntry.publishedAt},\n                                id: {lt: cursor}\n                            }\n                        ]\n                        : [\n                            {publishedAt: {gt: cursorEntry.publishedAt}},\n                            {\n                                publishedAt: {equals: cursorEntry.publishedAt},\n                                id: {gt: cursor}\n                            }\n                        ]\n\n                    // Create a new where clause with cursor conditions\n                    where = {\n                        ...baseWhere,\n                        OR: cursorConditions\n                    }\n                }\n            }\n\n            // Get entries with cursor-based pagination\n            const entries = await db.changelogEntry.findMany({\n                where,\n                take: ITEMS_PER_PAGE + 1,\n                orderBy: [\n                    {publishedAt: sortOrder},\n                    {id: sortOrder}\n                ],\n                select: {\n                    id: true,\n                    title: true,\n                    excerpt: true, // Use excerpt instead of full content for list view\n                    version: true,\n                    publishedAt: true,\n                    createdAt: true,\n                    updatedAt: true,\n                    tags: {\n                        select: {\n                            id: true,\n                            name: true,\n                            color: true\n                        }\n                    }\n                }\n            })\n\n            let nextCursor: string | undefined\n\n            // If we got more items than requested, we have a next page\n            if (entries.length > ITEMS_PER_PAGE) {\n                const nextItem = entries.pop()\n                nextCursor = nextItem?.id\n            }\n\n            return NextResponse.json({\n                project: {\n                    id: project.id,\n                    name: project.name,\n                    emailNotificationsEnabled: project.emailConfig?.enabled || false\n                },\n                items: entries,\n                nextCursor\n            })\n        } catch (error) {\n            console.error('Error fetching changelog entries:', error)\n            return NextResponse.json(\n                {error: 'Failed to fetch changelog entries'},\n                {status: 500}\n            )\n        }\n    })()\n}"
  },
  {
    "path": "app/api/changelog/entries/[entryId]/route.ts",
    "content": "import {db} from '@/lib/db'\nimport {\n    validateAuthAndGetUser,\n    changelogEntrySchema,\n    sendError,\n    sendSuccess,\n    generateExcerpt,\n    type ChangelogEntryInput\n} from '@/lib/utils/changelog'\nimport {z} from \"zod\";\nimport {NextResponse} from 'next/server';\nimport {useEntryViewTracking} from '@/app/changelog/[projectId]/changelog-view'\n\n// Helper to get project ID from changelog entry\nasync function getProjectIdFromEntry(entryId: string) {\n    const entry = await db.changelogEntry.findUnique({\n        where: {id: entryId},\n        select: {\n            changelog: {\n                select: {\n                    projectId: true\n                }\n            }\n        }\n    });\n    return entry?.changelog?.projectId;\n}\n\n/**\n * @method GET\n * @description Fetches a single public changelog entry by its ID\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"project\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"name\": { \"type\": \"string\" },\n *         \"description\": { \"type\": \"string\", \"nullable\": true }\n *       }\n *     },\n *     \"entry\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"title\": { \"type\": \"string\" },\n *         \"content\": { \"type\": \"string\" },\n *         \"excerpt\": { \"type\": \"string\", \"nullable\": true },\n *         \"version\": { \"type\": \"string\", \"nullable\": true },\n *         \"publishedAt\": { \"type\": \"string\" },\n *         \"createdAt\": { \"type\": \"string\" },\n *         \"updatedAt\": { \"type\": \"string\" },\n *         \"changelogId\": { \"type\": \"string\" },\n *         \"tags\": { \"type\": \"array\" }\n *       }\n *     }\n *   }\n * }\n * @error 404 Entry not found or not published\n * @error 500 Failed to fetch entry\n */\nexport async function GET(\n    request: Request,\n    {params}: { params: Promise<{ entryId: string }> }\n) {\n    try {\n        const {entryId} = await params;\n\n        // Fetch the entry with project info\n        const entry = await db.changelogEntry.findUnique({\n            where: {\n                id: entryId,\n                publishedAt: {not: null}, // Only published entries\n            },\n            select: {\n                id: true,\n                title: true,\n                content: true,\n                excerpt: true,\n                version: true,\n                publishedAt: true,\n                createdAt: true,\n                updatedAt: true,\n                changelogId: true,\n                tags: {\n                    select: {\n                        id: true,\n                        name: true,\n                        color: true,\n                    }\n                },\n                changelog: {\n                    select: {\n                        project: {\n                            select: {\n                                id: true,\n                                name: true,\n                                isPublic: true,\n                            }\n                        }\n                    }\n                }\n            }\n        });\n\n        if (!entry) {\n            return NextResponse.json(\n                {error: 'Entry not found'},\n                {status: 404}\n            );\n        }\n\n        // Check if project is public\n        if (!entry.changelog.project.isPublic) {\n            return NextResponse.json(\n                {error: 'Entry not found'},\n                {status: 404}\n            );\n        }\n\n        return NextResponse.json({\n            project: {\n                id: entry.changelog.project.id,\n                name: entry.changelog.project.name,\n            },\n            entry: {\n                id: entry.id,\n                title: entry.title,\n                content: entry.content,\n                excerpt: entry.excerpt,\n                version: entry.version,\n                publishedAt: entry.publishedAt,\n                createdAt: entry.createdAt,\n                updatedAt: entry.updatedAt,\n                changelogId: entry.changelogId,\n                tags: entry.tags,\n            }\n        });\n    } catch (error) {\n        console.error('Error fetching changelog entry:', error);\n        return NextResponse.json(\n            {error: 'Failed to fetch entry'},\n            {status: 500}\n        );\n    }\n}\n\n/**\n * @method PUT\n * @description Updates a changelog entry by its ID. Only authorized users with role 'STAFF' or 'ADMIN' can update an entry.\n * @body {\n *   \"type\": \"object\",\n *   \"required\": [\"title\", \"content\", \"version\"],\n *   \"properties\": {\n *     \"title\": { \"type\": \"string\", \"example\": \"New feature: dark mode\" },\n *     \"content\": { \"type\": \"string\", \"example\": \"The app now has a dark mode option.\" },\n *     \"version\": { \"type\": \"string\", \"example\": \"1.0.1\" },\n *     \"tags\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"string\",\n *         \"example\": \"feature\"\n *       }\n *     }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"data\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"title\": { \"type\": \"string\" },\n *         \"content\": { \"type\": \"string\" },\n *         \"version\": { \"type\": \"string\" },\n *         \"tags\": {\n *           \"type\": \"array\",\n *           \"items\": {\n *             \"type\": \"object\",\n *             \"properties\": {\n *               \"id\": { \"type\": \"string\" },\n *               \"name\": { \"type\": \"string\" }\n *             }\n *           }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 400 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": {\n *       \"type\": \"string\",\n *       \"example\": \"Invalid request data\"\n *     }\n *   }\n * }\n * @error 401 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": {\n *       \"type\": \"string\",\n *       \"example\": \"Unauthorized\"\n *     }\n *   }\n * }\n * @error 403 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": {\n *       \"type\": \"string\",\n *       \"example\": \"Forbidden\"\n *     }\n *   }\n * }\n * @error 404 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": {\n *       \"type\": \"string\",\n *       \"example\": \"Changelog entry not found\"\n *     }\n *   }\n * }\n * @error 500 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": {\n *       \"type\": \"string\",\n *       \"example\": \"Internal server error\"\n *     }\n *   }\n * }\n */\nexport async function PUT(\n    request: Request,\n    {params}: { params: Promise<{ entryId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser()\n\n        if (user.role === 'VIEWER') {\n            return sendError('Unauthorized', 403)\n        }\n\n        const json = await request.json()\n        const body: ChangelogEntryInput = changelogEntrySchema.parse(json)\n\n        const entry = await db.changelogEntry.update({\n            where: {id: (await params).entryId},\n            data: {\n                title: body.title,\n                content: body.content,\n                excerpt: generateExcerpt(body.content), // Regenerate excerpt when content changes\n                version: body.version,\n                tags: body.tags ? {\n                    set: [], // Clear existing tags\n                    connectOrCreate: body.tags.map(tag => ({\n                        where: {name: tag},\n                        create: {name: tag}\n                    }))\n                } : undefined\n            },\n            include: {\n                tags: true\n            }\n        })\n\n        return sendSuccess(entry)\n    } catch (error) {\n        if (error instanceof z.ZodError) {\n            return sendError('Invalid request data: ' + error.message, 400)\n        }\n        console.error('Error updating changelog entry:', error)\n        return sendError('Failed to update changelog entry', 500)\n    }\n}\n\n/**\n * @method DELETE\n * @description Deletes a changelog entry by its ID. Only authorized users with role 'ADMIN' can delete an entry.\n * @body {\n *   \"type\": \"null\"\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"data\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" }\n *       }\n *     }\n *   }\n * }\n * @error 400 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": {\n *       \"type\": \"string\",\n *       \"example\": \"Invalid request data\"\n *     }\n *   }\n * }\n * @error 401 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": {\n *       \"type\": \"string\",\n *       \"example\": \"Unauthorized\"\n *     }\n *   }\n * }\n * @error 403 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": {\n *       \"type\": \"string\",\n *       \"example\": \"Forbidden\"\n *     }\n *   }\n * }\n * @error 404 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": {\n *       \"type\": \"string\",\n *       \"example\": \"Changelog entry not found\"\n *     }\n *   }\n * }\n * @error 500 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": {\n *       \"type\": \"string\",\n *       \"example\": \"Internal server error\"\n *     }\n *   }\n * }\n */\nexport async function DELETE(\n    request: Request,\n    {params}: { params: Promise<{ entryId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser()\n\n        if (user.role === 'VIEWER') {\n            return sendError('Unauthorized', 403)\n        }\n\n        // Get the project ID from the changelog entry\n        const projectId = await getProjectIdFromEntry((await params).entryId)\n\n        if (!projectId) {\n            return sendError('Changelog entry not found', 404)\n        }\n\n        // If user is staff, create a deletion request\n        if (user.role === 'STAFF') {\n            const request = await db.changelogRequest.create({\n                data: {\n                    type: 'DELETE_ENTRY',\n                    status: 'PENDING',\n                    staffId: user.id,\n                    changelogEntryId: (await params).entryId,\n                    projectId: projectId\n                }\n            })\n\n            return sendSuccess({\n                message: 'Deletion request created',\n                request\n            })\n        }\n\n        // If admin, delete directly\n        const entry = await db.changelogEntry.delete({\n            where: {id: (await params).entryId}\n        })\n\n        return sendSuccess({\n            message: 'Changelog entry deleted',\n            entry\n        })\n    } catch (error) {\n        console.error('Error handling changelog entry deletion:', error)\n        return sendError('Failed to process deletion request', 500)\n    }\n}"
  },
  {
    "path": "app/api/changelog/requests/[requestId]/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { z } from 'zod';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { changelogRequestService } from '@/lib/services/request/changelog-request';\n\n// Schema validation\nconst updateRequestSchema = z.object({\n    status: z.enum(['APPROVED', 'REJECTED']),\n    timestamp: z.string().optional()\n});\n\n/**\n * @method PATCH\n * @description Updates the status of a changelog request\n * @query {\n *   requestId: String, required\n * }\n * @body {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"status\": { \"type\": \"string\", \"enum\": [\"APPROVED\", \"REJECTED\"] },\n *     \"timestamp\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"adminId\": { \"type\": \"string\" }\n *   },\n *   \"required\": [\n *     \"status\"\n *   ],\n *   \"additionalProperties\": false\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"title\": { \"type\": \"string\" },\n *     \"content\": { \"type\": \"string\" },\n *     \"version\": { \"type\": \"number\" },\n *     \"status\": { \"type\": \"string\", \"enum\": [\"PENDING\", \"APPROVED\", \"REJECTED\"] },\n *     \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"changelogId\": { \"type\": \"string\" },\n *     \"adminId\": { \"type\": \"string\" }\n *   }\n * }\n * @error 400 Invalid request data\n * @error 403 Unauthorized - User does not have 'ADMIN' role\n * @error 404 Request not found\n * @error 500 An unexpected error occurred while processing the request\n */\nexport async function PATCH(\n    req: Request,\n    context: { params: Promise<{ requestId: string }> }\n) {\n    console.log('=== Processing PATCH Request ===');\n\n    try {\n        // Get and validate requestId\n        const { requestId } = await (async () => context.params)();\n        console.log('RequestId:', requestId);\n\n        if (!requestId) {\n            console.log('Missing requestId in params');\n            return NextResponse.json({ error: 'Missing requestId' }, { status: 400 });\n        }\n\n        // Validate auth\n        const user = await validateAuthAndGetUser();\n        console.log('User:', { id: user.id, role: user.role });\n\n        if (user.role !== 'ADMIN') {\n            console.log('Non-admin user attempted to process request');\n            return NextResponse.json(\n                { error: 'Only admins can process requests' },\n                { status: 403 }\n            );\n        }\n\n        // Parse request body\n        let body;\n        try {\n            const text = await req.text();\n            console.log('Raw request body:', text);\n\n            if (!text) {\n                console.log('Empty request body');\n                return NextResponse.json({ error: 'Empty request body' }, { status: 400 });\n            }\n\n            body = JSON.parse(text);\n            console.log('Parsed body:', body);\n        } catch (error) {\n            console.log('Failed to parse request body:', error);\n            return NextResponse.json(\n                { error: 'Invalid JSON in request body' },\n                { status: 400 }\n            );\n        }\n\n        // Validate parsed body\n        try {\n            const validatedData = updateRequestSchema.parse(body);\n            console.log('Validated data:', validatedData);\n\n            // Process the request\n            const result = await changelogRequestService.processRequest({\n                requestId,\n                status: validatedData.status,\n                adminId: user.id\n            });\n\n            console.log('Request processed successfully:', result);\n            return NextResponse.json(result);\n\n        } catch (error) {\n            if (error instanceof z.ZodError) {\n                console.log('Validation error:', error.errors);\n                return NextResponse.json(\n                    { error: 'Invalid request data', details: error.errors },\n                    { status: 400 }\n                );\n            }\n            throw error;\n        }\n\n    } catch (error: unknown) {\n        // @ts-expect-error error might be null\n        console.error('Error stack:', error.stack);\n\n        return NextResponse.json(\n            { error: 'Failed to process request' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/changelog/requests/route.ts",
    "content": "// app/api/changelog/requests/route.ts\nimport {NextResponse} from 'next/server'\nimport {db} from '@/lib/db'\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog'\nimport {z} from 'zod'\nimport {Prisma} from \"@prisma/client\";\n\nconst requestSchema = z.object({\n    type: z.enum(['DELETE_PROJECT', 'DELETE_TAG', 'DELETE_ENTRY', 'ALLOW_PUBLISH', 'ALLOW_SCHEDULE']),\n    projectId: z.string(),\n    targetId: z.string().optional()\n})\n\n/**\n * @method GET\n * @description Retrieves pending changelog requests for authenticated user\n * @path /api/changelog/requests\n * @query {projectId: string} (optional)\n * @response 200 {\n *   \"type\": \"array\",\n *   \"items\": {\n *     \"type\": \"object\",\n *     \"properties\": {\n *       \"id\": { \"type\": \"string\" },\n *       \"type\": { \"type\": \"string\", \"enum\": [\"DELETE_PROJECT\", \"DELETE_TAG\", \"DELETE_ENTRY\", \"ALLOW_PUBLISH\", \"ALLOW_SCHEDULE\"] },\n *       \"status\": { \"type\": \"string\", \"enum\": [\"PENDING\", \"APPROVED\", \"REJECTED\"] },\n *       \"staffId\": { \"type\": \"string\" },\n *       \"targetId\": { \"type\": \"string\" },\n *       \"staff\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"email\": { \"type\": \"string\" },\n *           \"name\": { \"type\": \"string\" }\n *         }\n *       },\n *       \"project\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"name\": { \"type\": \"string\" },\n *           \"defaultTags\": {\n *             \"type\": \"array\",\n *             \"items\": {\n *               \"type\": \"string\"\n *             }\n *           }\n *         }\n *       },\n *       \"ChangelogEntry\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"title\": { \"type\": \"string\" }\n *         }\n *       },\n *       \"ChangelogTag\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"name\": { \"type\": \"string\" }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 401 Unauthorized - User not authenticated\n * @error 500 An unexpected error occurred while fetching requests\n */\nexport async function GET(request: Request) {\n    try {\n        const user = await validateAuthAndGetUser()\n        const {searchParams} = new URL(request.url)\n        const projectId = searchParams.get('projectId')\n\n        // Build the base query\n        const whereClause: Prisma.ChangelogRequestWhereInput = {\n            status: 'PENDING',\n        }\n\n        // If projectId is provided, filter by it\n        if (projectId) {\n            whereClause.projectId = projectId\n        }\n\n        // If not admin, only show user's own requests\n        if (user.role !== 'ADMIN') {\n            whereClause.staffId = user.id\n        }\n\n        const requests = await db.changelogRequest.findMany({\n            where: whereClause,\n            include: {\n                staff: {\n                    select: {\n                        id: true,\n                        email: true,\n                        name: true\n                    }\n                },\n                project: {\n                    select: {\n                        id: true,\n                        name: true,\n                        defaultTags: true\n                    }\n                },\n                ChangelogEntry: {\n                    select: {\n                        id: true,\n                        title: true\n                    }\n                },\n                ChangelogTag: {\n                    select: {\n                        id: true,\n                        name: true\n                    }\n                }\n            },\n            orderBy: {\n                createdAt: 'desc'\n            }\n        })\n\n        return NextResponse.json(requests)\n\n    } catch (error) {\n        console.error('Failed to fetch requests:', error)\n        return NextResponse.json(\n            {error: 'Failed to fetch requests'},\n            {status: 500}\n        )\n    }\n}\n\n/**\n * @method POST\n * @description Creates a new pending changelog request\n * @path /api/changelog/requests\n * @request {json}\n * @response 201 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"type\": { \"type\": \"string\", \"enum\": [\"DELETE_PROJECT\", \"DELETE_TAG\", \"DELETE_ENTRY\", \"ALLOW_PUBLISH\", \"ALLOW_SCHEDULE\"] },\n *     \"status\": { \"type\": \"string\", \"enum\": [\"PENDING\"] },\n *     \"staffId\": { \"type\": \"string\" },\n *     \"staff\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"email\": { \"type\": \"string\" },\n *         \"name\": { \"type\": \"string\" }\n *       }\n *     },\n *     \"project\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"name\": { \"type\": \"string\" },\n *         \"defaultTags\": {\n *             \"type\": \"array\",\n *             \"items\": {\n *               \"type\": \"string\"\n *             }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 401 Unauthorized - User not authenticated\n * @error 403 Forbidden - Only staff members can create requests\n * @error 400 Bad Request - Invalid request data\n * @error 500 An unexpected error occurred while creating the request\n */\nexport async function POST(request: Request) {\n    try {\n        const user = await validateAuthAndGetUser()\n\n        if (user.role === 'VIEWER') {\n            return NextResponse.json(\n                {error: 'Only staff members can create requests'},\n                {status: 403}\n            )\n        }\n\n        const body = await request.json()\n        const validatedData = requestSchema.parse(body)\n\n        // Create the request\n        const newRequest = await db.changelogRequest.create({\n            data: {\n                type: validatedData.type,\n                staffId: user.id,\n                projectId: validatedData.projectId,\n                targetId: validatedData.targetId,\n                status: 'PENDING'\n            },\n            include: {\n                staff: {\n                    select: {\n                        id: true,\n                        email: true,\n                        name: true\n                    }\n                },\n                project: {\n                    select: {\n                        id: true,\n                        name: true,\n                        defaultTags: true\n                    }\n                }\n            }\n        })\n\n        return NextResponse.json(newRequest, {status: 201})\n\n    } catch (error) {\n        console.error('Failed to create request:', error)\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {error: 'Invalid request data', details: error.errors},\n                {status: 400}\n            )\n        }\n\n        return NextResponse.json(\n            {error: 'Failed to create request'},\n            {status: 500}\n        )\n    }\n}"
  },
  {
    "path": "app/api/changelog/subscribe/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { z } from 'zod';\nimport { db } from '@/lib/db';\nimport { nanoid } from 'nanoid';\n\ninterface SubscribeRequest {\n    email: string;\n    projectId: string;\n    name?: string;\n    subscriptionType: 'ALL_UPDATES' | 'MAJOR_ONLY' | 'DIGEST_ONLY';\n    customDomain?: string | null; // Allow null values\n}\n\ninterface SubscribeResponse {\n    success: boolean;\n    message: string;\n}\n\ninterface ErrorResponse {\n    error: string;\n    details?: unknown;\n}\n\n/**\n * @method POST\n * @description Creates a subscription for a user to receive email notifications for changelog updates\n */\nexport async function POST(request: Request): Promise<NextResponse<SubscribeResponse | ErrorResponse>> {\n    try {\n        const subscribeSchema = z.object({\n            email: z.string().email('Invalid email format'),\n            projectId: z.string().min(1, 'Project ID is required'),\n            name: z.string().optional(),\n            subscriptionType: z.enum(['ALL_UPDATES', 'MAJOR_ONLY', 'DIGEST_ONLY']).default('ALL_UPDATES'),\n            customDomain: z.string().nullable().optional() // Allow null, undefined, or string\n        });\n\n        // Parse and validate request body\n        const body = await request.json();\n        const { email, projectId, name, subscriptionType, customDomain }: SubscribeRequest = subscribeSchema.parse(body);\n\n        // Verify project exists\n        const project = await db.project.findUnique({\n            where: { id: projectId }\n        });\n\n        if (!project) {\n            return NextResponse.json(\n                { error: 'Project not found' },\n                { status: 404 }\n            );\n        }\n\n        // Determine the domain to use - default to app domain if no custom domain provided\n        const appDomain = process.env.NEXT_PUBLIC_APP_URL?.replace(/^https?:\\/\\//, '') || 'localhost:3000';\n        const finalDomain = customDomain || appDomain; // customDomain can be null, so this will default to appDomain\n\n        // Find or create subscriber\n        let subscriber = await db.emailSubscriber.findUnique({\n            where: { email },\n            include: {\n                subscriptions: {\n                    where: { projectId }\n                }\n            }\n        });\n\n        if (!subscriber) {\n            // Create new subscriber\n            subscriber = await db.emailSubscriber.create({\n                data: {\n                    email,\n                    name,\n                    unsubscribeToken: nanoid(32),\n                    isActive: true\n                },\n                include: {\n                    subscriptions: {\n                        where: { projectId }\n                    }\n                }\n            });\n        } else if (name && !subscriber.name) {\n            // Update subscriber name if provided and not set before\n            await db.emailSubscriber.update({\n                where: { id: subscriber.id },\n                data: { name }\n            });\n        }\n\n        // Create or update subscription\n        if (subscriber.subscriptions.length === 0) {\n            // Create new subscription with domain\n            await db.projectSubscription.create({\n                data: {\n                    subscriberId: subscriber.id,\n                    projectId,\n                    subscriptionType,\n                    customDomain: finalDomain // Use finalDomain (either custom or app domain)\n                }\n            });\n        } else {\n            // Update existing subscription\n            await db.projectSubscription.update({\n                where: { id: subscriber.subscriptions[0].id },\n                data: {\n                    subscriptionType,\n                    customDomain: finalDomain // Update domain on existing subscription\n                }\n            });\n        }\n\n        return NextResponse.json({\n            success: true,\n            message: 'Subscription created successfully'\n        });\n    } catch (error) {\n        console.error('Error creating subscription:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: 'Validation failed', details: error.errors },\n                { status: 400 }\n            );\n        }\n\n        return NextResponse.json(\n            { error: 'Failed to create subscription' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/changelog/unsubscribe/[token]/route.ts",
    "content": "// app/api/changelog/unsubscribe/[token]/route.ts\nimport { NextRequest, NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\n\ninterface RouteParams {\n    params: Promise<{\n        token: string\n    }>\n}\n\nexport async function GET(\n    request: NextRequest,\n    { params }: RouteParams\n) {\n    try {\n        const { token } = await params\n        const { searchParams } = new URL(request.url)\n        const projectId = searchParams.get('projectId')\n\n        if (!token) {\n            return NextResponse.json(\n                { error: 'Unsubscribe token is required' },\n                { status: 400 }\n            )\n        }\n\n        // Find subscriber by token\n        const subscriber = await db.emailSubscriber.findUnique({\n            where: { unsubscribeToken: token },\n            include: {\n                subscriptions: projectId\n                    ? {\n                        where: { projectId },\n                        include: { project: true }\n                    }\n                    : { include: { project: true } }\n            }\n        })\n\n        if (!subscriber) {\n            return NextResponse.json(\n                { error: 'Invalid unsubscribe token' },\n                { status: 404 }\n            )\n        }\n\n        let customDomain: string | null = null\n\n        if (projectId) {\n            // Unsubscribe from specific project\n            if (subscriber.subscriptions.length > 0) {\n                const subscription = subscriber.subscriptions[0]\n                customDomain = subscription.customDomain // Get the custom domain they subscribed from\n\n                await db.projectSubscription.delete({\n                    where: { id: subscription.id }\n                })\n            }\n        } else {\n            // Unsubscribe from all - use the most recent custom domain or first one found\n            if (subscriber.subscriptions.length > 0) {\n                // Try to find a subscription with a custom domain\n                const subscriptionWithDomain = subscriber.subscriptions.find(sub => sub.customDomain)\n                customDomain = subscriptionWithDomain?.customDomain || null\n\n                await db.projectSubscription.deleteMany({\n                    where: { subscriberId: subscriber.id }\n                })\n            }\n        }\n\n        // Determine redirect URL - use custom domain if available, otherwise main app domain\n        let redirectUrl: string\n        if (customDomain) {\n            redirectUrl = `https://${customDomain}/unsubscribed?email=${encodeURIComponent(subscriber.email)}`\n        } else {\n            const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'\n            redirectUrl = `${appUrl}/unsubscribed?email=${encodeURIComponent(subscriber.email)}`\n        }\n\n        console.log(`Redirecting unsubscribe to: ${redirectUrl}`)\n\n        return NextResponse.redirect(redirectUrl)\n    } catch (error) {\n        console.error('Error processing unsubscribe request:', error)\n        return NextResponse.json(\n            { error: 'Failed to process unsubscribe request' },\n            { status: 500 }\n        )\n    }\n}"
  },
  {
    "path": "app/api/changelog/verify-domain/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server'\nimport {getDomainByDomain} from '@/lib/custom-domains/service'\n\nexport async function GET(request: NextRequest) {\n    try {\n        const {searchParams} = new URL(request.url)\n        const domain = searchParams.get('domain')\n        const token = searchParams.get('token')\n\n        if (!domain || !token) {\n            return NextResponse.json(\n                {error: 'Domain and token are required'},\n                {status: 400}\n            )\n        }\n\n        // Look up the domain configuration\n        const domainConfig = await getDomainByDomain(domain)\n\n        if (!domainConfig) {\n            return NextResponse.json(\n                {error: 'Domain not found'},\n                {status: 404}\n            )\n        }\n\n        // Verify the token matches\n        if (domainConfig.verificationToken !== token) {\n            return NextResponse.json(\n                {error: 'Invalid verification token'},\n                {status: 403}\n            )\n        }\n\n        // Return success response indicating the domain is properly routed\n        return NextResponse.json({\n            success: true,\n            domain: domainConfig.domain,\n            projectId: domainConfig.projectId,\n            verified: true,\n            message: 'Domain verification successful via HTTP check'\n        })\n\n    } catch (error) {\n        console.error('Error in domain verification endpoint:', error)\n        return NextResponse.json(\n            {error: 'Internal server error'},\n            {status: 500}\n        )\n    }\n}"
  },
  {
    "path": "app/api/check-setup/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\n\n/**\n * This is a special API route that only handles setup status check\n * and implements cache headers to prevent circular requests.\n */\nexport async function GET(request: Request) {\n    const headers = new Headers();\n\n    // Check for the special middleware header to prevent circular requests\n    const isMiddlewareCheck = request.headers.get('x-middleware-check') === 'true';\n\n    if (!isMiddlewareCheck) {\n        // If not coming from middleware, return 403\n        return NextResponse.json({ error: 'Forbidden' }, { status: 403 });\n    }\n\n    try {\n        // Add cache headers to prevent frequent checks\n        headers.set('Cache-Control', 'max-age=5');\n\n        // Check if any user exists\n        const userCount = await db.user.count();\n\n        return NextResponse.json(\n            { isComplete: userCount > 0 },\n            { headers, status: 200 }\n        );\n    } catch (error) {\n        console.error('Setup check error:', error);\n\n        // Even on error, set cache headers to prevent thundering herd\n        headers.set('Cache-Control', 'max-age=1');\n\n        return NextResponse.json(\n            { error: 'Failed to check setup status', isComplete: false },\n            { headers, status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/config/runtime/route.ts",
    "content": "import { NextResponse } from 'next/server'\n\n/**\n * Runtime configuration endpoint\n * Returns environment variables that need to be available at runtime (not build time)\n * This is the Next.js recommended pattern for NEXT_PUBLIC_* variables that need runtime flexibility\n */\nexport async function GET() {\n    return NextResponse.json({\n        sslEnabled: process.env.NEXT_PUBLIC_SSL_ENABLED === 'true',\n    })\n}\n"
  },
  {
    "path": "app/api/config/timezone/route.ts",
    "content": "import {NextResponse} from 'next/server'\nimport {db} from '@/lib/db'\nimport {cookies} from 'next/headers'\nimport {verifyAccessToken} from '@/lib/auth/tokens'\n\nexport async function GET() {\n    try {\n        const config = await db.systemConfig.findFirst({\n            select: {timezone: true, allowUserTimezone: true, customDateTemplates: true},\n        })\n\n        const globalTimezone = config?.timezone ?? 'UTC'\n        const allowUserTimezone = config?.allowUserTimezone ?? true\n        const customDateTemplates = (config?.customDateTemplates as { format: string; label: string }[] | null) ?? null\n\n        // If user timezone overrides are allowed, try to get the user's preference\n        if (allowUserTimezone) {\n            try {\n                const cookieStore = await cookies()\n                const token = cookieStore.get('accessToken')?.value\n                if (token) {\n                    const userId = await verifyAccessToken(token)\n                    if (userId) {\n                        const settings = await db.settings.findUnique({\n                            where: {userId},\n                            select: {timezone: true},\n                        })\n                        if (settings?.timezone) {\n                            return NextResponse.json({\n                                timezone: settings.timezone,\n                                source: 'user',\n                                allowUserTimezone: true,\n                                customDateTemplates,\n                            })\n                        }\n                    }\n                }\n            } catch {\n                // Token invalid or no auth — fall through to global\n            }\n        }\n\n        return NextResponse.json({\n            timezone: globalTimezone,\n            source: 'system',\n            allowUserTimezone,\n            customDateTemplates,\n        })\n    } catch {\n        return NextResponse.json({\n            timezone: 'UTC',\n            source: 'system',\n            allowUserTimezone: true,\n        })\n    }\n}\n"
  },
  {
    "path": "app/api/cron/ssl-renewal/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport { sslSupported } from '@/lib/custom-domains/ssl/is-supported'\nimport { runAutoRenewal, checkCertificateHealth } from '@/lib/custom-domains/ssl/auto-renewal'\n\nexport const runtime = 'nodejs'\nexport const maxDuration = 60 // Allow up to 60 seconds for renewal operations\n\n// Support both GET (for compatibility) and POST\nexport async function GET(request: NextRequest) {\n    return handleRenewal(request)\n}\n\nexport async function POST(request: NextRequest) {\n    return handleRenewal(request)\n}\n\nasync function handleRenewal(request: NextRequest) {\n    if (!sslSupported) {\n        return NextResponse.json(\n            { error: 'SSL certificate management is only available in Docker deployments' },\n            { status: 503 },\n        )\n    }\n\n    // Support both CRON_SECRET (legacy) and INTERNAL_API_SECRET (new)\n    const cronSecret = process.env.CRON_SECRET || process.env.INTERNAL_API_SECRET\n    if (!cronSecret) {\n        return NextResponse.json(\n            { error: 'CRON_SECRET or INTERNAL_API_SECRET not configured' },\n            { status: 503 },\n        )\n    }\n\n    const authHeader = request.headers.get('Authorization')\n    if (authHeader !== `Bearer ${cronSecret}`) {\n        return NextResponse.json(\n            { error: 'Unauthorized' },\n            { status: 401 },\n        )\n    }\n\n    try {\n        // Check if this is a health check request (via query param)\n        const url = new URL(request.url)\n        const action = url.searchParams.get('action')\n\n        if (action === 'health') {\n            const health = await checkCertificateHealth()\n            return NextResponse.json({\n                success: true,\n                health,\n            })\n        }\n\n        // Run the auto-renewal job\n        const result = await runAutoRenewal()\n\n        return NextResponse.json({\n            success: true,\n            message: `Processed ${result.checked} expiring certificates`,\n            result,\n        })\n    } catch (error) {\n        console.error('[ssl-renewal] Cron job error:', error)\n        return NextResponse.json(\n            { error: error instanceof Error ? error.message : 'Internal server error' },\n            { status: 500 },\n        )\n    }\n}\n"
  },
  {
    "path": "app/api/custom-domains/[domain]/browser-rules/[id]/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog'\nimport { canUserManageDomain } from '@/lib/custom-domains/service'\n\nexport const runtime = 'nodejs'\n\nexport async function PATCH(\n    request: NextRequest,\n    { params }: { params: Promise<{ domain: string; id: string }> }\n) {\n    const { domain, id } = await params\n\n    try {\n        let user;\n        try {\n            user = await validateAuthAndGetUser();\n        } catch {\n            return NextResponse.json({ error: 'Authentication required' }, { status: 401 })\n        }\n\n        const isAdmin = user.role === 'ADMIN'\n        const canManage = await canUserManageDomain(domain, user.id, isAdmin)\n        if (!canManage) {\n            return NextResponse.json({ error: 'Unauthorized to manage this domain' }, { status: 403 })\n        }\n\n        const body = await request.json()\n        const { isEnabled } = body\n\n        if (typeof isEnabled !== 'boolean') {\n            return NextResponse.json(\n                { error: 'isEnabled must be a boolean' },\n                { status: 400 }\n            )\n        }\n\n        // Verify the rule exists and belongs to this domain\n        const rule = await db.domainBrowserRule.findFirst({\n            where: {\n                id,\n                domain: {\n                    domain,\n                },\n            },\n        })\n\n        if (!rule) {\n            return NextResponse.json(\n                { error: 'Rule not found' },\n                { status: 404 }\n            )\n        }\n\n        // Update the rule\n        const updatedRule = await db.domainBrowserRule.update({\n            where: { id },\n            data: { isEnabled },\n        })\n\n        return NextResponse.json({\n            success: true,\n            rule: updatedRule,\n        })\n    } catch (error) {\n        console.error('[browser-rules/update] Error:', error)\n        return NextResponse.json(\n            { error: error instanceof Error ? error.message : 'Failed to update browser rule' },\n            { status: 500 }\n        )\n    }\n}\n\nexport async function DELETE(\n    request: NextRequest,\n    { params }: { params: Promise<{ domain: string; id: string }> }\n) {\n    const { domain, id } = await params\n\n    try {\n        let user;\n        try {\n            user = await validateAuthAndGetUser();\n        } catch {\n            return NextResponse.json({ error: 'Authentication required' }, { status: 401 })\n        }\n\n        const isAdmin = user.role === 'ADMIN'\n        const canManage = await canUserManageDomain(domain, user.id, isAdmin)\n        if (!canManage) {\n            return NextResponse.json({ error: 'Unauthorized to manage this domain' }, { status: 403 })\n        }\n\n        // Verify the rule exists and belongs to this domain\n        const rule = await db.domainBrowserRule.findFirst({\n            where: {\n                id,\n                domain: {\n                    domain,\n                },\n            },\n        })\n\n        if (!rule) {\n            return NextResponse.json(\n                { error: 'Rule not found' },\n                { status: 404 }\n            )\n        }\n\n        // Delete the rule\n        await db.domainBrowserRule.delete({\n            where: { id },\n        })\n\n        return NextResponse.json({\n            success: true,\n        })\n    } catch (error) {\n        console.error('[browser-rules/delete] Error:', error)\n        return NextResponse.json(\n            { error: error instanceof Error ? error.message : 'Failed to delete browser rule' },\n            { status: 500 }\n        )\n    }\n}\n"
  },
  {
    "path": "app/api/custom-domains/[domain]/browser-rules/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog'\nimport { canUserManageDomain } from '@/lib/custom-domains/service'\n\nexport const runtime = 'nodejs'\n\nexport async function POST(\n    request: NextRequest,\n    { params }: { params: Promise<{ domain: string }> }\n) {\n    const { domain } = await params\n\n    try {\n        let user;\n        try {\n            user = await validateAuthAndGetUser();\n        } catch {\n            return NextResponse.json({ error: 'Authentication required' }, { status: 401 })\n        }\n\n        const isAdmin = user.role === 'ADMIN'\n        const canManage = await canUserManageDomain(domain, user.id, isAdmin)\n        if (!canManage) {\n            return NextResponse.json({ error: 'Unauthorized to manage this domain' }, { status: 403 })\n        }\n\n        const body = await request.json()\n        const { userAgentPattern, ruleType } = body\n\n        if (!userAgentPattern?.trim()) {\n            return NextResponse.json(\n                { error: 'User agent pattern is required' },\n                { status: 400 }\n            )\n        }\n\n        if (!ruleType || !['BLOCK', 'ALLOW'].includes(ruleType)) {\n            return NextResponse.json(\n                { error: 'Invalid rule type. Must be BLOCK or ALLOW' },\n                { status: 400 }\n            )\n        }\n\n        // Find the domain\n        const customDomain = await db.customDomain.findUnique({\n            where: { domain },\n        })\n\n        if (!customDomain) {\n            return NextResponse.json(\n                { error: 'Domain not found' },\n                { status: 404 }\n            )\n        }\n\n        // Validate regex pattern\n        try {\n            new RegExp(userAgentPattern)\n        } catch {\n            return NextResponse.json(\n                { error: 'Invalid regex pattern' },\n                { status: 400 }\n            )\n        }\n\n        // Create the browser rule\n        const rule = await db.domainBrowserRule.create({\n            data: {\n                domainId: customDomain.id,\n                userAgentPattern,\n                ruleType,\n                isEnabled: true,\n            },\n        })\n\n        return NextResponse.json({\n            success: true,\n            rule,\n        })\n    } catch (error) {\n        console.error('[browser-rules] Error:', error)\n        return NextResponse.json(\n            { error: error instanceof Error ? error.message : 'Failed to create browser rule' },\n            { status: 500 }\n        )\n    }\n}\n"
  },
  {
    "path": "app/api/custom-domains/[domain]/dns-instructions/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\nimport type { DNSInstructions } from '@/lib/types/custom-domains'\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog'\nimport { canUserManageDomain } from '@/lib/custom-domains/service'\n\nexport const runtime = 'nodejs'\n\nexport async function GET(\n    request: NextRequest,\n    { params }: { params: Promise<{ domain: string }> }\n) {\n    try {\n        let user;\n        try {\n            user = await validateAuthAndGetUser();\n        } catch {\n            return NextResponse.json({ success: false, error: 'Authentication required' }, { status: 401 })\n        }\n\n        const { domain: domainName } = await params\n\n        const isAdmin = user.role === 'ADMIN'\n        const canManage = await canUserManageDomain(domainName, user.id, isAdmin)\n        if (!canManage) {\n            return NextResponse.json({ success: false, error: 'Unauthorized to manage this domain' }, { status: 403 })\n        }\n\n        const domain = await db.customDomain.findUnique({\n            where: { domain: domainName },\n            select: {\n                verificationToken: true,\n                domain: true,\n            },\n        })\n\n        if (!domain) {\n            return NextResponse.json(\n                { success: false, error: 'Domain not found' },\n                { status: 404 }\n            )\n        }\n\n        const appUrl = process.env.NEXT_PUBLIC_APP_URL\n        if (!appUrl) {\n            return NextResponse.json(\n                { success: false, error: 'App URL not configured' },\n                { status: 500 }\n            )\n        }\n\n        // Extract hostname from app URL\n        const appDomain = new URL(appUrl).hostname\n\n        const dnsInstructions: DNSInstructions = {\n            cname: {\n                name: domain.domain,\n                value: appDomain,\n                description: `Point your domain to ${appDomain}`,\n            },\n            txt: {\n                name: `_chrverify.${domain.domain}`,\n                value: domain.verificationToken,\n                description: 'Verify domain ownership',\n            },\n        }\n\n        return NextResponse.json({\n            success: true,\n            dnsInstructions,\n        })\n    } catch (error) {\n        console.error('[dns-instructions] Error:', error)\n        return NextResponse.json(\n            { success: false, error: 'Failed to generate DNS instructions' },\n            { status: 500 }\n        )\n    }\n}\n"
  },
  {
    "path": "app/api/custom-domains/[domain]/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server'\nimport {deleteDomain, getDomainByDomain, canUserManageDomain} from '@/lib/custom-domains/service'\nimport type {DeleteDomainResponse} from '@/lib/types/custom-domains'\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog'\n\ninterface RouteParams {\n    params: Promise<{\n        domain: string\n    }>\n}\n\nexport async function DELETE(\n    request: NextRequest,\n    {params}: RouteParams\n): Promise<NextResponse<DeleteDomainResponse>> {\n    try {\n        let user;\n        try {\n            user = await validateAuthAndGetUser();\n        } catch {\n            return NextResponse.json(\n                {success: false, error: 'Authentication required'},\n                {status: 401}\n            )\n        }\n\n        const {domain: encodedDomain} = await params\n        const domain = decodeURIComponent(encodedDomain)\n\n        if (!domain) {\n            return NextResponse.json(\n                {success: false, error: 'Domain is required'},\n                {status: 400}\n            )\n        }\n\n        // Check if domain exists\n        const domainConfig = await getDomainByDomain(domain)\n        if (!domainConfig) {\n            return NextResponse.json(\n                {success: false, error: 'Domain not found'},\n                {status: 404}\n            )\n        }\n\n        // Verify ownership via the authenticated user's role — never trust client-supplied flags\n        const isAdmin = user.role === 'ADMIN'\n        const canManage = await canUserManageDomain(domain, user.id, isAdmin)\n        if (!canManage) {\n            return NextResponse.json(\n                {success: false, error: 'Unauthorized to delete this domain'},\n                {status: 403}\n            )\n        }\n\n        await deleteDomain(domain)\n\n        return NextResponse.json({success: true})\n\n    } catch (error) {\n        console.error('Error deleting custom domain:', error)\n        return NextResponse.json(\n            {\n                success: false,\n                error: error instanceof Error ? error.message : 'Internal server error'\n            },\n            {status: 500}\n        )\n    }\n}"
  },
  {
    "path": "app/api/custom-domains/[domain]/ssl/mode/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\nimport { SslMode } from '@prisma/client'\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog'\nimport { canUserManageDomain } from '@/lib/custom-domains/service'\n\nexport const runtime = 'nodejs'\n\nexport async function POST(\n    request: NextRequest,\n    { params }: { params: Promise<{ domain: string }> }\n) {\n    const { domain } = await params\n\n    try {\n        let user;\n        try {\n            user = await validateAuthAndGetUser();\n        } catch {\n            return NextResponse.json({ error: 'Authentication required' }, { status: 401 })\n        }\n\n        const isAdmin = user.role === 'ADMIN'\n        const canManage = await canUserManageDomain(domain, user.id, isAdmin)\n        if (!canManage) {\n            return NextResponse.json({ error: 'Unauthorized to manage this domain' }, { status: 403 })\n        }\n\n        const body = await request.json()\n        const { sslMode } = body\n\n        if (!sslMode || !['LETS_ENCRYPT', 'EXTERNAL', 'NONE'].includes(sslMode)) {\n            return NextResponse.json(\n                { error: 'Invalid SSL mode. Must be LETS_ENCRYPT, EXTERNAL, or NONE' },\n                { status: 400 }\n            )\n        }\n\n        // Find the domain\n        const customDomain = await db.customDomain.findUnique({\n            where: { domain },\n        })\n\n        if (!customDomain) {\n            return NextResponse.json(\n                { error: 'Domain not found' },\n                { status: 404 }\n            )\n        }\n\n        // Update the SSL mode\n        await db.customDomain.update({\n            where: { domain },\n            data: {\n                sslMode: sslMode as SslMode,\n                // If switching to EXTERNAL or NONE, disable force HTTPS\n                forceHttps: sslMode === 'LETS_ENCRYPT' ? customDomain.forceHttps : false,\n            },\n        })\n\n        return NextResponse.json({\n            success: true,\n            message: `SSL mode updated to ${sslMode}`,\n        })\n    } catch (error) {\n        console.error('[ssl/mode] Error:', error)\n        return NextResponse.json(\n            { error: error instanceof Error ? error.message : 'Failed to update SSL mode' },\n            { status: 500 }\n        )\n    }\n}\n"
  },
  {
    "path": "app/api/custom-domains/[domain]/ssl/revoke/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog'\nimport { canUserManageDomain } from '@/lib/custom-domains/service'\n\nexport const runtime = 'nodejs'\n\n/**\n * DELETE /api/custom-domains/:domain/ssl/revoke\n * Completely removes the current SSL certificate from the database.\n * This allows re-issuing a fresh certificate.\n *\n * Also attempts to notify nginx-agent to clean up SSL files (non-blocking).\n */\nexport async function DELETE(\n    request: NextRequest,\n    { params }: { params: Promise<{ domain: string }> }\n) {\n    try {\n        let user;\n        try {\n            user = await validateAuthAndGetUser();\n        } catch {\n            return NextResponse.json({ success: false, error: 'Authentication required' }, { status: 401 })\n        }\n\n        const { domain: domainName } = await params\n\n        const isAdmin = user.role === 'ADMIN'\n        const canManage = await canUserManageDomain(domainName, user.id, isAdmin)\n        if (!canManage) {\n            return NextResponse.json({ success: false, error: 'Unauthorized to manage this domain' }, { status: 403 })\n        }\n\n        // Find the domain\n        const domain = await db.customDomain.findUnique({\n            where: { domain: domainName },\n            include: {\n                certificates: {\n                    where: {\n                        status: {\n                            in: ['ISSUED', 'PENDING_HTTP01', 'PENDING_DNS01', 'FAILED']\n                        }\n                    }\n                }\n            }\n        })\n\n        if (!domain) {\n            return NextResponse.json(\n                { success: false, error: 'Domain not found' },\n                { status: 404 }\n            )\n        }\n\n        // Delete all certificates for this domain\n        const deleteResult = await db.domainCertificate.deleteMany({\n            where: { domainId: domain.id }\n        })\n\n        console.log(`[ssl/revoke] Deleted ${deleteResult.count} certificates for ${domainName}`)\n\n        // Try to notify nginx-agent to clean up (don't fail if this doesn't work)\n        try {\n            const agentUrl = process.env.NGINX_AGENT_URL\n            const internalSecret = process.env.INTERNAL_API_SECRET\n\n            if (agentUrl && internalSecret) {\n                await fetch(`${agentUrl}/domain/${encodeURIComponent(domainName)}`, {\n                    method: 'DELETE',\n                    headers: {\n                        'X-Internal-Secret': internalSecret,\n                    },\n                    signal: AbortSignal.timeout(5000),\n                })\n                console.log(`[ssl/revoke] Notified nginx-agent to clean up ${domainName}`)\n            }\n        } catch (agentError) {\n            // Log but don't fail the operation\n            console.warn(`[ssl/revoke] Failed to notify nginx-agent (non-critical):`, agentError)\n        }\n\n        return NextResponse.json({\n            success: true,\n            message: `Deleted ${deleteResult.count} certificate(s)`,\n            count: deleteResult.count\n        })\n    } catch (error) {\n        console.error('[ssl/revoke] Error:', error)\n        return NextResponse.json(\n            { success: false, error: 'Failed to revoke certificate' },\n            { status: 500 }\n        )\n    }\n}\n"
  },
  {
    "path": "app/api/custom-domains/[domain]/ssl/toggle-https/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog'\nimport { canUserManageDomain } from '@/lib/custom-domains/service'\n\nexport const runtime = 'nodejs'\n\nexport async function POST(\n    request: NextRequest,\n    { params }: { params: Promise<{ domain: string }> },\n) {\n    const { domain } = await params\n\n    try {\n        let user;\n        try {\n            user = await validateAuthAndGetUser();\n        } catch {\n            return NextResponse.json({ error: 'Authentication required' }, { status: 401 })\n        }\n\n        const isAdmin = user.role === 'ADMIN'\n        const canManage = await canUserManageDomain(domain, user.id, isAdmin)\n        if (!canManage) {\n            return NextResponse.json({ error: 'Unauthorized to manage this domain' }, { status: 403 })\n        }\n\n        const body = await request.json()\n        const { forceHttps } = body\n\n        if (typeof forceHttps !== 'boolean') {\n            return NextResponse.json(\n                { error: 'forceHttps must be a boolean' },\n                { status: 400 },\n            )\n        }\n\n        const customDomain = await db.customDomain.findUnique({\n            where: { domain },\n        })\n\n        if (!customDomain) {\n            return NextResponse.json(\n                { error: 'Domain not found' },\n                { status: 404 },\n            )\n        }\n\n        // Only allow force HTTPS if SSL is enabled\n        if (forceHttps && customDomain.sslMode !== 'LETS_ENCRYPT') {\n            return NextResponse.json(\n                { error: 'SSL certificate required to enable force HTTPS' },\n                { status: 400 },\n            )\n        }\n\n        await db.customDomain.update({\n            where: { domain },\n            data: { forceHttps },\n        })\n\n        return NextResponse.json({\n            success: true,\n            forceHttps,\n        })\n    } catch (error) {\n        console.error('[toggle-https] Error:', error)\n        return NextResponse.json(\n            { error: 'Internal server error' },\n            { status: 500 },\n        )\n    }\n}\n"
  },
  {
    "path": "app/api/custom-domains/[domain]/throttle/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog'\nimport { canUserManageDomain } from '@/lib/custom-domains/service'\n\nexport const runtime = 'nodejs'\n\nexport async function POST(\n    request: NextRequest,\n    { params }: { params: Promise<{ domain: string }> }\n) {\n    const { domain } = await params\n\n    try {\n        let user;\n        try {\n            user = await validateAuthAndGetUser();\n        } catch {\n            return NextResponse.json({ error: 'Authentication required' }, { status: 401 })\n        }\n\n        const isAdmin = user.role === 'ADMIN'\n        const canManage = await canUserManageDomain(domain, user.id, isAdmin)\n        if (!canManage) {\n            return NextResponse.json({ error: 'Unauthorized to manage this domain' }, { status: 403 })\n        }\n\n        const body = await request.json()\n        const { enabled, requestsPerSecond, burstSize } = body\n\n        if (typeof enabled !== 'boolean') {\n            return NextResponse.json(\n                { error: 'enabled must be a boolean' },\n                { status: 400 }\n            )\n        }\n\n        if (enabled) {\n            if (!requestsPerSecond || requestsPerSecond < 1) {\n                return NextResponse.json(\n                    { error: 'requestsPerSecond must be at least 1' },\n                    { status: 400 }\n                )\n            }\n\n            if (!burstSize || burstSize < 1) {\n                return NextResponse.json(\n                    { error: 'burstSize must be at least 1' },\n                    { status: 400 }\n                )\n            }\n        }\n\n        // Find the domain\n        const customDomain = await db.customDomain.findUnique({\n            where: { domain },\n            include: {\n                throttleConfig: true,\n            },\n        })\n\n        if (!customDomain) {\n            return NextResponse.json(\n                { error: 'Domain not found' },\n                { status: 404 }\n            )\n        }\n\n        // Upsert the throttle config\n        const throttleConfig = await db.domainThrottleConfig.upsert({\n            where: {\n                domainId: customDomain.id,\n            },\n            create: {\n                domainId: customDomain.id,\n                enabled,\n                requestsPerSecond: enabled ? requestsPerSecond : 60,\n                burstSize: enabled ? burstSize : 20,\n            },\n            update: {\n                enabled,\n                requestsPerSecond: enabled ? requestsPerSecond : undefined,\n                burstSize: enabled ? burstSize : undefined,\n            },\n        })\n\n        return NextResponse.json({\n            success: true,\n            throttleConfig,\n        })\n    } catch (error) {\n        console.error('[throttle] Error:', error)\n        return NextResponse.json(\n            { error: error instanceof Error ? error.message : 'Failed to update throttle configuration' },\n            { status: 500 }\n        )\n    }\n}\n"
  },
  {
    "path": "app/api/custom-domains/add/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server'\nimport {createCustomDomain} from '@/lib/custom-domains/service'\nimport {getAppDomain} from '@/lib/custom-domains/utils'\nimport {DOMAIN_CONSTANTS} from '@/lib/custom-domains/constants'\nimport type {AddDomainRequest, AddDomainResponse} from '@/lib/types/custom-domains'\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog'\n\nexport async function POST(request: NextRequest): Promise<NextResponse<AddDomainResponse>> {\n    try {\n        let user;\n        try {\n            user = await validateAuthAndGetUser();\n        } catch {\n            return NextResponse.json(\n                { success: false, error: 'Authentication required' },\n                { status: 401 }\n            )\n        }\n\n        const body: AddDomainRequest = await request.json()\n        const {domain, projectId} = body\n        // Always use the authenticated user's ID, never trust the client-supplied userId\n        const userId = user.id\n\n        if (!domain || !projectId) {\n            return NextResponse.json(\n                {\n                    success: false,\n                    error: 'Domain and projectId are required'\n                },\n                {status: 400}\n            )\n        }\n\n        const domainConfig = await createCustomDomain(domain, projectId, userId)\n\n        let appDomain: string\n        try {\n            appDomain = getAppDomain()\n        } catch (error) {\n            console.error('Failed to get app domain:', error)\n            return NextResponse.json(\n                {success: false, error: 'App domain configuration error'},\n                {status: 500}\n            )\n        }\n\n        return NextResponse.json({\n            success: true,\n            domain: {\n                id: domainConfig.id,\n                domain: domainConfig.domain,\n                projectId: domainConfig.projectId,\n                verificationToken: domainConfig.verificationToken,\n                dnsInstructions: {\n                    cname: {\n                        name: domainConfig.domain,\n                        value: appDomain,\n                        description: `Create a CNAME record pointing ${domainConfig.domain} to ${appDomain}`\n                    },\n                    txt: {\n                        name: `${DOMAIN_CONSTANTS.VERIFICATION_SUBDOMAIN}.${domainConfig.domain}`,\n                        value: domainConfig.verificationToken,\n                        description: 'Create this TXT record to verify domain ownership'\n                    }\n                }\n            }\n        })\n\n    } catch (error) {\n        console.error('Error adding custom domain:', error)\n\n        const errorMessage = error instanceof Error ? error.message : 'Internal server error'\n        const statusCode = errorMessage.includes('already') ? 409 :\n            errorMessage.includes('Invalid') ? 400 :\n                errorMessage.includes('not found') ? 404 :\n                    errorMessage.includes('exceeded') ? 429 : 500\n\n        return NextResponse.json(\n            {success: false, error: errorMessage},\n            {status: statusCode}\n        )\n    }\n}"
  },
  {
    "path": "app/api/custom-domains/list/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport {\n    getAllDomains,\n    getDomainsByUser,\n    getDomainsByProject\n} from '@/lib/custom-domains/service'\nimport type { ListDomainsResponse } from '@/lib/types/custom-domains'\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog'\n\nexport async function GET(request: NextRequest): Promise<NextResponse<ListDomainsResponse>> {\n    try {\n        let user;\n        try {\n            user = await validateAuthAndGetUser();\n        } catch {\n            return NextResponse.json(\n                { success: false, error: 'Authentication required' },\n                { status: 401 }\n            )\n        }\n\n        const { searchParams } = new URL(request.url)\n        const projectId = searchParams.get('projectId')\n        const scope = searchParams.get('scope') // 'all', 'user', 'project'\n        const isAdmin = user.role === 'ADMIN'\n\n        let domains\n\n        if (projectId) {\n            // Project-specific domains — caller must own the project or be admin\n            domains = await getDomainsByProject(projectId)\n        } else if (scope === 'all' && isAdmin) {\n            // Admin-only: list all domains across the system\n            domains = await getAllDomains()\n        } else {\n            // Default: list the authenticated user's own domains\n            domains = await getDomainsByUser(user.id)\n        }\n\n        return NextResponse.json({\n            success: true,\n            domains\n        })\n\n    } catch (error) {\n        console.error('Error listing custom domains:', error)\n        return NextResponse.json(\n            {\n                success: false,\n                error: error instanceof Error ? error.message : 'Internal server error'\n            },\n            { status: 500 }\n        )\n    }\n}"
  },
  {
    "path": "app/api/custom-domains/verify/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server'\nimport {getDomainByDomain, updateDomainVerification, canUserManageDomain} from '@/lib/custom-domains/service'\nimport {verifyDNSRecords} from '@/lib/custom-domains/dns'\nimport {getAppDomain} from '@/lib/custom-domains/utils'\nimport type {VerifyDomainRequest, VerifyDomainResponse} from '@/lib/types/custom-domains'\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog'\n\nexport async function POST(request: NextRequest): Promise<NextResponse<VerifyDomainResponse>> {\n    try {\n        let user;\n        try {\n            user = await validateAuthAndGetUser();\n        } catch {\n            return NextResponse.json(\n                { success: false, error: 'Authentication required' },\n                { status: 401 }\n            )\n        }\n\n        const body: VerifyDomainRequest = await request.json()\n        const {domain} = body\n\n        if (!domain) {\n            return NextResponse.json(\n                {success: false, error: 'Domain is required'},\n                {status: 400}\n            )\n        }\n\n        const domainConfig = await getDomainByDomain(domain)\n        if (!domainConfig) {\n            return NextResponse.json(\n                {success: false, error: 'Domain configuration not found'},\n                {status: 404}\n            )\n        }\n\n        const isAdmin = user.role === 'ADMIN'\n        const canManage = await canUserManageDomain(domain, user.id, isAdmin)\n        if (!canManage) {\n            return NextResponse.json(\n                {success: false, error: 'Unauthorized to manage this domain'},\n                {status: 403}\n            )\n        }\n\n        let appDomain: string\n        try {\n            appDomain = getAppDomain()\n        } catch (error) {\n            console.error('Failed to get app domain:', error)\n            return NextResponse.json(\n                {success: false, error: 'App domain configuration error'},\n                {status: 500}\n            )\n        }\n\n        const verification = await verifyDNSRecords(\n            domain,\n            appDomain,\n            domainConfig.verificationToken\n        )\n\n        const verified = verification.cnameValid && verification.txtValid\n\n        // Update the verification status in our database to see if it has changed\n        if (verified && !domainConfig.verified) {\n            await updateDomainVerification(domain, true)\n        } else if (!verified && domainConfig.verified) {\n            await updateDomainVerification(domain, false)\n        }\n\n        return NextResponse.json({\n            success: true,\n            verification: {\n                ...verification,\n                verified\n            }\n        })\n\n    } catch (error) {\n        console.error('Error verifying custom domain:', error)\n        return NextResponse.json(\n            {\n                success: false,\n                error: error instanceof Error ? error.message : 'Internal server error'\n            },\n            {status: 500}\n        )\n    }\n}"
  },
  {
    "path": "app/api/dashboard/stats/route.ts",
    "content": "import {NextResponse} from 'next/server'\nimport {db} from '@/lib/db'\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog'\n\ninterface ProjectPreview {\n    id: string\n    name: string\n    lastUpdated: string\n    changelogCount: number\n}\n\ninterface DashboardActivity {\n    id: string\n    type: string\n    message: string\n    timestamp: string\n    projectId: string\n    projectName: string\n    updatedAt: string\n}\n\ninterface DashboardStats {\n    projectPreviews: ProjectPreview[]\n    totalProjects: number\n    totalChangelogs: number\n    recentActivity: DashboardActivity[]\n    adminStats?: {\n        pendingApprovals: number\n    }\n}\n\n/**\n * @method GET\n * @description Retrieves dashboard statistics for the authenticated user, including recent projects, total project and changelog counts, and recent activity\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"projectPreviews\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"name\": { \"type\": \"string\" },\n *           \"lastUpdated\": { \"type\": \"string\", \"format\": \"date-time\" },\n *           \"changelogCount\": { \"type\": \"number\" }\n *         }\n *       }\n *     },\n *     \"totalProjects\": { \"type\": \"number\" },\n *     \"totalChangelogs\": { \"type\": \"number\" },\n *     \"recentActivity\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"type\": { \"type\": \"string\", \"enum\": [\"CHANGELOG_ENTRY\"] },\n *           \"message\": { \"type\": \"string\" },\n *           \"timestamp\": { \"type\": \"string\", \"format\": \"date-time\" },\n *           \"projectId\": { \"type\": \"string\" },\n *           \"projectName\": { \"type\": \"string\" },\n *           \"updatedAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *         }\n *       }\n *     },\n *     \"adminStats\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"pendingApprovals\": { \"type\": \"number\" }\n *       },\n *       \"additionalProperties\": false\n *     }\n *   }\n * }\n * @error 401 Unauthorized - User not authenticated\n * @error 500 An unexpected error occurred while fetching dashboard statistics\n */\nexport async function GET(): Promise<NextResponse<DashboardStats | { error: string }>> {\n    try {\n        const user = await validateAuthAndGetUser()\n        if (!user) {\n            return NextResponse.json(\n                {error: 'Unauthorized'},\n                {status: 401}\n            )\n        }\n\n        // Access control filter for non-admin users\n        const accessFilter = user.role !== 'ADMIN' ? {\n            OR: [\n                {isPublic: true},\n                // Add additional access control conditions here when implemented\n            ]\n        } : {}\n\n        // Get recent projects with their latest changelog entry\n        const recentProjects = await db.project.findMany({\n            where: accessFilter,\n            include: {\n                changelog: {\n                    include: {\n                        entries: {\n                            orderBy: {\n                                updatedAt: 'desc'\n                            },\n                            take: 1\n                        },\n                        _count: {\n                            select: {entries: true}\n                        }\n                    }\n                }\n            },\n            orderBy: {\n                updatedAt: 'desc'\n            },\n            take: 6 // Get more than 3 to show variety, let our frontend decide how many to display\n        })\n\n        // Format projects for preview - no placeholder data\n        const projectPreviews: ProjectPreview[] = recentProjects.map(project => ({\n            id: project.id,\n            name: project.name,\n            lastUpdated: project.changelog?.entries[0]?.updatedAt.toISOString() || project.updatedAt.toISOString(),\n            changelogCount: project.changelog?._count.entries || 0\n        }))\n\n        // Get total counts\n        const [totalProjects, totalChangelogs] = await Promise.all([\n            db.project.count({\n                where: accessFilter\n            }),\n            db.changelogEntry.count({\n                where: {\n                    changelog: {\n                        project: accessFilter\n                    }\n                }\n            })\n        ])\n\n        // Get recent activity\n        const recentActivity = await db.changelogEntry.findMany({\n            where: {\n                changelog: {\n                    project: accessFilter\n                }\n            },\n            select: {\n                id: true,\n                title: true,\n                version: true,\n                createdAt: true,\n                updatedAt: true,\n                changelog: {\n                    select: {\n                        project: {\n                            select: {\n                                id: true,\n                                name: true\n                            }\n                        }\n                    }\n                }\n            },\n            orderBy: {\n                createdAt: 'desc'\n            },\n            take: 10\n        })\n\n        // Format activity\n        const formattedActivity = recentActivity.map(entry => ({\n            id: entry.id,\n            type: 'CHANGELOG_ENTRY',\n            message: `New changelog entry: ${entry.title}${entry.version ? ` (${entry.version})` : ''}`,\n            timestamp: entry.createdAt.toISOString(),\n            projectId: entry.changelog.project.id,\n            projectName: entry.changelog.project.name,\n            updatedAt: entry.updatedAt.toISOString()\n        }))\n\n        // Get admin-specific stats if applicable\n        let adminStats: { pendingApprovals: number } | undefined\n        if (user.role === 'ADMIN') {\n            const pendingApprovals = await db.changelogRequest.count({\n                where: {\n                    status: 'PENDING'\n                }\n            })\n\n            adminStats = {pendingApprovals}\n        }\n\n        const response: DashboardStats = {\n            projectPreviews,\n            totalProjects,\n            totalChangelogs,\n            recentActivity: formattedActivity,\n            ...(adminStats && {adminStats})\n        }\n\n        return NextResponse.json(response)\n    } catch (error) {\n        console.error('Dashboard stats error:', error)\n        return NextResponse.json(\n            {error: 'Failed to fetch dashboard statistics'},\n            {status: 500}\n        )\n    }\n}"
  },
  {
    "path": "app/api/domain-check/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\n\nexport const runtime = 'nodejs'\n\n// Used by Caddy's on_demand_tls ask endpoint.\n// Returns 200 if the domain is verified and has Let's Encrypt SSL enabled.\n// Returns 403 otherwise (Caddy will not issue a certificate).\nexport async function GET(request: NextRequest) {\n    const domain = request.nextUrl.searchParams.get('domain')\n\n    if (!domain) {\n        return new NextResponse('Missing domain parameter', { status: 400 })\n    }\n\n    try {\n        const customDomain = await db.customDomain.findUnique({\n            where: { domain },\n            select: {\n                verified: true,\n                sslMode: true,\n            },\n        })\n\n        // Only allow cert issuance if domain is verified AND using Let's Encrypt\n        if (customDomain?.verified && customDomain.sslMode === 'LETS_ENCRYPT') {\n            return new NextResponse('OK', { status: 200 })\n        }\n\n        return new NextResponse('Domain not eligible for automatic SSL', {\n            status: 403,\n        })\n    } catch (error) {\n        console.error('[domain-check] Database error:', error)\n        return new NextResponse('Internal server error', { status: 500 })\n    }\n}\n"
  },
  {
    "path": "app/api/health/route.ts",
    "content": "// app/api/health/route.ts\nimport { NextResponse } from 'next/server';\nimport { db } from '@/lib/db';\n\n/**\n * @method GET\n * @description Health check endpoint to verify application readiness\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"status\": { \"type\": \"string\" },\n *     \"timestamp\": { \"type\": \"string\" },\n *     \"services\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"database\": { \"type\": \"string\" },\n *         \"application\": { \"type\": \"string\" }\n *       }\n *     }\n *   }\n * }\n * @response 503 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"status\": { \"type\": \"string\" },\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n */\nexport async function GET() {\n    try {\n        // Check database connectivity\n        await db.$queryRaw`SELECT 1`;\n\n        return NextResponse.json({\n            status: 'healthy',\n            timestamp: new Date().toISOString(),\n            services: {\n                database: 'connected',\n                application: 'ready'\n            }\n        }, { status: 200 });\n\n    } catch (error) {\n        console.error('Health check failed:', error);\n\n        return NextResponse.json({\n            status: 'unhealthy',\n            error: 'Database connection failed'\n        }, { status: 503 });\n    }\n}"
  },
  {
    "path": "app/api/integrations/slack/callback/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server'\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog'\nimport {db} from '@/lib/db'\nimport {encryptToken} from '@/lib/utils/encryption'\n\n/**\n * GET /api/integrations/slack/callback\n * Handle OAuth callback from Slack\n * Exchanges auth code for access token and saves integration\n */\n/**\n * Get the correct app URL, handling proxies, internal IPs, and IPv6\n */\nfunction getAppUrl(req: NextRequest): string {\n    // Priority 1: Environment variable (most reliable)\n    if (process.env.NEXT_PUBLIC_APP_URL) {\n        const url = process.env.NEXT_PUBLIC_APP_URL.replace(/\\/$/, ''); // Remove trailing slash\n        console.log('[Slack Callback] Using NEXT_PUBLIC_APP_URL:', url);\n        return url;\n    }\n\n    // Priority 2: Check forwarded headers (for proxies)\n    const forwardedProto = req.headers.get('x-forwarded-proto') || req.headers.get('x-forwarded-protocol');\n    const forwardedHost = req.headers.get('x-forwarded-host');\n    const host = req.headers.get('host');\n\n    if (forwardedProto && (forwardedHost || host)) {\n        const finalHost = forwardedHost || host;\n        const url = `${forwardedProto}://${finalHost}`;\n        console.log('[Slack Callback] Using forwarded headers:', url);\n        return url;\n    }\n\n    // Priority 3: Parse from request URL\n    const url = new URL(req.url);\n    let hostname = url.hostname;\n    const port = url.port;\n    const protocol = url.protocol;\n\n    // Handle IPv6 addresses - ensure they're wrapped in brackets\n    if (hostname.includes(':') && !hostname.startsWith('[')) {\n        hostname = `[${hostname}]`;\n    }\n\n    // Construct URL with port if non-standard\n    const portSuffix = (\n        (protocol === 'https:' && port && port !== '443') ||\n        (protocol === 'http:' && port && port !== '80')\n    ) ? `:${port}` : '';\n\n    const finalUrl = `${protocol}//${hostname}${portSuffix}`;\n    console.log('[Slack Callback] Using request URL:', finalUrl, 'from req.url:', req.url);\n    return finalUrl;\n}\n\n/**\n * Create an absolute URL from a path and base URL\n */\nfunction createAbsoluteUrl(path: string, baseUrl: string): string {\n    // Ensure baseUrl doesn't have trailing slash and path starts with /\n    const cleanBase = baseUrl.replace(/\\/$/, '');\n    const cleanPath = path.startsWith('/') ? path : `/${path}`;\n    const absoluteUrl = `${cleanBase}${cleanPath}`;\n    console.log('[Slack Callback] Created absolute URL:', absoluteUrl);\n    return absoluteUrl;\n}\n\nexport async function GET(req: NextRequest) {\n    try {\n        // Use helper function to get correct app URL (supports proxies, internal IPs, IPv6)\n        const appUrl = getAppUrl(req);\n        console.log('[Slack Callback] Final appUrl determined:', appUrl);\n\n        // Get query parameters\n        const {searchParams} = new URL(req.url);\n        const code = searchParams.get('code');\n        const state = searchParams.get('state');\n        const error = searchParams.get('error');\n        const errorDescription = searchParams.get('error_description');\n\n        // Handle Slack errors\n        if (error) {\n            const errorMsg = errorDescription ? `${error}: ${errorDescription}` : error;\n            console.error('Slack OAuth error:', errorMsg);\n            return NextResponse.redirect(\n                createAbsoluteUrl(`/dashboard/projects?slack_error=${encodeURIComponent(errorMsg)}`, appUrl)\n            );\n        }\n\n        // Validate required parameters\n        if (!code || !state) {\n            console.error('Missing OAuth parameters');\n            return NextResponse.redirect(\n                createAbsoluteUrl('/dashboard/projects?slack_error=Missing%20OAuth%20parameters', appUrl)\n            );\n        }\n\n        // Decode state to get projectId\n        let projectId: string;\n        try {\n            const decodedState = JSON.parse(Buffer.from(state, 'base64').toString('utf-8'));\n            projectId = decodedState.projectId;\n            if (!projectId) {\n                throw new Error('No projectId in state');\n            }\n        } catch (error) {\n            console.error('Failed to decode state:', error);\n            return NextResponse.redirect(\n                createAbsoluteUrl('/dashboard/projects?slack_error=Invalid%20state%20parameter', appUrl)\n            );\n        }\n\n        // Authenticate the user\n        const user = await validateAuthAndGetUser();\n\n        // Verify project exists\n        const project = await db.project.findUnique({\n            where: {id: projectId},\n            select: {id: true}\n        });\n\n        if (!project) {\n            return NextResponse.redirect(\n                createAbsoluteUrl(`/dashboard/projects?slack_error=Project%20not%20found`, appUrl)\n            );\n        }\n\n        // Get Slack OAuth credentials from system config\n        const config = await db.systemConfig.findUnique({\n            where: {id: 1},\n            select: {\n                slackOAuthClientId: true,\n                slackOAuthClientSecret: true\n            }\n        });\n\n        if (!config?.slackOAuthClientId || !config?.slackOAuthClientSecret) {\n            return NextResponse.redirect(\n                createAbsoluteUrl(\n                    `/dashboard/projects/${projectId}/integrations/slack?error=Slack%20OAuth%20not%20configured`,\n                    appUrl\n                )\n            );\n        }\n\n        // Exchange code for access token\n        const tokenResponse = await fetch('https://slack.com/api/oauth.v2.access', {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/x-www-form-urlencoded'\n            },\n            body: new URLSearchParams({\n                client_id: config.slackOAuthClientId,\n                client_secret: config.slackOAuthClientSecret,\n                code,\n                redirect_uri: `${appUrl}/api/integrations/slack/callback`\n            }).toString()\n        });\n\n        if (!tokenResponse.ok) {\n            const error = await tokenResponse.json();\n            console.error('Slack token exchange failed:', error);\n            return NextResponse.redirect(\n                createAbsoluteUrl(\n                    `/dashboard/projects/${projectId}/integrations/slack?error=Failed%20to%20exchange%20token`,\n                    appUrl\n                )\n            );\n        }\n\n        const tokenData = await tokenResponse.json();\n\n        if (!tokenData.ok) {\n            console.error('Slack token exchange error:', tokenData.error);\n            return NextResponse.redirect(\n                createAbsoluteUrl(\n                    `/dashboard/projects/${projectId}/integrations/slack?error=${encodeURIComponent(tokenData.error)}`,\n                    appUrl\n                )\n            );\n        }\n\n        // Extract required data from Slack response\n        const {\n            access_token,\n            token_type,\n            scope,\n            bot_user_id,\n            app_id,\n            team = {},\n            enterprise = {}\n        } = tokenData;\n\n        const teamId = team.id || enterprise?.id;\n        const teamName = team.name;\n\n        // Get bot user info\n        let botUsername = '';\n        try {\n            const userInfoResponse = await fetch('https://slack.com/api/users.info', {\n                method: 'POST',\n                headers: {\n                    'Authorization': `Bearer ${access_token}`,\n                    'Content-Type': 'application/x-www-form-urlencoded'\n                },\n                body: new URLSearchParams({\n                    user: bot_user_id\n                }).toString()\n            });\n\n            if (userInfoResponse.ok) {\n                const userInfo = await userInfoResponse.json();\n                botUsername = userInfo.user?.real_name || userInfo.user?.name || bot_user_id;\n            }\n        } catch (error) {\n            console.error('Failed to fetch bot user info:', error);\n            // Continue without bot username, we have the ID\n        }\n\n        // Encrypt the access token before storing\n        const encryptedToken = encryptToken(access_token);\n\n        // Create or update Slack integration\n        const integration = await db.slackIntegration.upsert({\n            where: {projectId},\n            create: {\n                projectId,\n                accessToken: encryptedToken,\n                teamId,\n                teamName,\n                botUserId: bot_user_id,\n                botUsername,\n                channelId: '', // Will be configured by user in settings\n                channelName: '',\n                autoSend: true,\n                enabled: true,\n                postCount: 0\n            },\n            update: {\n                accessToken: encryptedToken,\n                teamId,\n                teamName,\n                botUserId: bot_user_id,\n                botUsername,\n                enabled: true\n            }\n        });\n\n        // Redirect back to Slack integration settings page\n        return NextResponse.redirect(\n            createAbsoluteUrl(\n                `/dashboard/projects/${projectId}/integrations/slack?connected=true`,\n                appUrl\n            )\n        );\n    } catch (error) {\n        console.error('Error in Slack OAuth callback:', error);\n        const appUrl = getAppUrl(req);\n        return NextResponse.redirect(\n            createAbsoluteUrl('/dashboard/projects?slack_error=An%20unexpected%20error%20occurred', appUrl)\n        );\n    }\n}"
  },
  {
    "path": "app/api/integrations/slack/manifest/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server'\n\n/**\n * Get the correct app URL, handling proxies, internal IPs, and IPv6\n */\nfunction getAppUrl(req: NextRequest): string {\n    // Priority 1: Environment variable (most reliable)\n    if (process.env.NEXT_PUBLIC_APP_URL) {\n        return process.env.NEXT_PUBLIC_APP_URL;\n    }\n\n    // Priority 2: Check forwarded headers (for proxies)\n    const forwardedProto = req.headers.get('x-forwarded-proto') || req.headers.get('x-forwarded-protocol');\n    const forwardedHost = req.headers.get('x-forwarded-host') || req.headers.get('host');\n\n    if (forwardedProto && forwardedHost) {\n        return `${forwardedProto}://${forwardedHost}`;\n    }\n\n    // Priority 3: Parse from request URL\n    const url = new URL(req.url);\n    let host = url.hostname;\n    const port = url.port;\n    const protocol = url.protocol;\n\n    // Handle IPv6 addresses - ensure they're wrapped in brackets\n    if (host.includes(':') && !host.startsWith('[')) {\n        host = `[${host}]`;\n    }\n\n    // Construct URL with port if non-standard\n    const portSuffix = (\n        (protocol === 'https:' && port && port !== '443') ||\n        (protocol === 'http:' && port && port !== '80')\n    ) ? `:${port}` : '';\n\n    return `${protocol}//${host}${portSuffix}`;\n}\n\n/**\n * GET /api/integrations/slack/manifest\n * Returns the Slack app manifest that users can import\n * This simplifies the setup process - users just copy/paste the manifest into Slack\n */\nexport async function GET(req: NextRequest) {\n    try {\n        // Use helper function to get correct app URL (supports proxies, internal IPs, IPv6)\n        const appUrl = getAppUrl(req);\n        const redirectUri = `${appUrl}/api/integrations/slack/callback`;\n\n        // Slack app manifest format\n        const manifest = {\n            _metadata: {\n                major_version: 1,\n                minor_version: 1\n            },\n            display_information: {\n                name: 'Changerawr',\n                description: 'Post changelog updates directly to your Slack workspace',\n                background_color: '#ffffff',\n                long_description: 'Changerawr is a changelog management platform. With this integration, automatically post your changelog updates to Slack channels so your team stays informed about product changes.'\n            },\n            features: {\n                bot_user: {\n                    display_name: 'Changerawr Bot',\n                    always_online: false\n                }\n            },\n            oauth_config: {\n                redirect_urls: [redirectUri],\n                scopes: {\n                    bot: [\n                        'chat:write',\n                        'channels:join',\n                        'channels:read',\n                        'groups:read',\n                        'im:read',\n                        'mpim:read',\n                        'users:read'\n                    ]\n                }\n            },\n            settings: {\n                org_deploy_enabled: false,\n                socket_mode_enabled: false,\n                token_rotation_enabled: false\n            }\n        };\n\n        return NextResponse.json(manifest, {\n            headers: {\n                'Content-Type': 'application/json',\n                'Content-Disposition': 'attachment; filename=\"changerawr-slack-manifest.json\"'\n            }\n        });\n    } catch (error) {\n        console.error('Error generating Slack manifest:', error);\n        return NextResponse.json(\n            {error: 'Failed to generate manifest'},\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/integrations/widget/[projectId]/[widgetId]/route.ts",
    "content": "import {NextResponse} from \"next/server\";\nimport {db} from \"@/lib/db\";\nimport {validateAuthAndGetUser, sendError, sendSuccess} from \"@/lib/utils/changelog\";\nimport {Prisma} from \"@prisma/client\";\n\n/**\n * @method GET\n * @description Get widget - serves loader script (public) or JSON (auth required based on Accept header)\n */\nexport async function GET(\n    request: Request,\n    context: { params: Promise<{ projectId: string; widgetId: string }> }\n) {\n    const {projectId, widgetId} = await context.params;\n    const acceptHeader = request.headers.get(\"accept\") || \"\";\n\n    // If requesting JSON (dashboard), require auth\n    if (acceptHeader.includes(\"application/json\")) {\n        try {\n            await validateAuthAndGetUser();\n\n            const widget = await db.widget.findUnique({\n                where: {id: widgetId},\n            });\n\n            if (!widget) {\n                return sendError(\"Widget not found\", 404);\n            }\n\n            return NextResponse.json({widget});\n        } catch (error) {\n            console.error(\"❌ Failed to get widget:\", error);\n            if (error instanceof Error && error.message === \"Authentication required\") {\n                return sendError(\"Unauthorized\", 401);\n            }\n            return sendError(\"Failed to get widget\", 500);\n        }\n    }\n\n    // Otherwise, serve public widget loader script\n    try {\n        const widget = await db.widget.findUnique({\n            where: {id: widgetId},\n            include: {\n                project: {\n                    select: {\n                        id: true,\n                        isPublic: true,\n                    },\n                },\n            },\n        });\n\n        if (!widget || widget.projectId !== projectId) {\n            return new NextResponse('console.error(\"Changerawr: Widget not found\");', {\n                status: 404,\n                headers: {\"Content-Type\": \"application/javascript\"},\n            });\n        }\n\n        if (!widget.project.isPublic || !widget.isActive) {\n            return new NextResponse('console.error(\"Changerawr: Widget is not available\");', {\n                status: 403,\n                headers: {\"Content-Type\": \"application/javascript\"},\n            });\n        }\n\n        const bundleFile = widget.variant === \"classic\" ? \"widget-classic.js\" : `widget-${widget.variant}.js`;\n\n        const script = `(function() {\n      const currentScript = document.currentScript;\n      if (!currentScript) {\n          console.error('Changerawr: Could not initialize widget');\n          return;\n      }\n\n      const widgetConfig = ${JSON.stringify(widget.settings)};\n      const options = {\n          projectId: '${projectId}',\n          widgetId: '${widgetId}',\n          variant: '${widget.variant}',\n          baseUrl: '${process.env.NEXT_PUBLIC_APP_URL}',\n          customCSS: ${widget.customCSS ? `\\`${widget.customCSS.replace(/`/g, \"\\\\`\")}\\`` : \"null\"},\n          theme: currentScript.getAttribute('data-theme') || widgetConfig.theme || 'light',\n          position: currentScript.getAttribute('data-position') || widgetConfig.position || 'bottom-right',\n          maxHeight: currentScript.getAttribute('data-max-height') || widgetConfig.maxHeight || '400px',\n          isPopup: currentScript.getAttribute('data-popup') === 'true' || widgetConfig.isPopup || false,\n          trigger: currentScript.getAttribute('data-trigger') || widgetConfig.trigger,\n          maxEntries: currentScript.getAttribute('data-max-entries')\n              ? parseInt(currentScript.getAttribute('data-max-entries'), 10)\n              : (widgetConfig.maxEntries || 3),\n          hidden: currentScript.getAttribute('data-popup') === 'true' || widgetConfig.isPopup || false\n      };\n\n      const initWidget = () => {\n          const container = document.createElement('div');\n          container.id = 'changerawr-widget-' + Math.random().toString(36).substr(2, 9);\n          const isPopupWithTrigger = options.isPopup && options.trigger;\n\n          if (isPopupWithTrigger) {\n              const triggerButton = document.getElementById(options.trigger);\n              if (!triggerButton) {\n                  console.error('Changerawr: Trigger button not found');\n                  return;\n              }\n              container.style.display = 'none';\n              document.body.appendChild(container);\n              triggerButton.setAttribute('aria-haspopup', 'dialog');\n          } else {\n              currentScript.parentNode.insertBefore(container, currentScript);\n          }\n\n          const script = document.createElement('script');\n          script.src = '${process.env.NEXT_PUBLIC_APP_URL}/${bundleFile}';\n          script.onload = () => {\n              if (!window.ChangerawrWidget || !window.ChangerawrWidget.init) {\n                  console.error('Changerawr: Widget failed to initialize');\n                  return;\n              }\n              const widget = window.ChangerawrWidget.init({ container, ...options });\n              if (isPopupWithTrigger) {\n                  const btn = document.getElementById(options.trigger);\n                  if (btn) {\n                      btn.addEventListener('click', () => {\n                          widget.toggle();\n                          btn.setAttribute('aria-expanded', widget.isOpen);\n                      });\n                  }\n              }\n          };\n          script.onerror = () => console.error('Changerawr: Failed to load widget');\n          document.head.appendChild(script);\n      };\n\n      if (document.readyState === 'loading') {\n          document.addEventListener('DOMContentLoaded', initWidget);\n      } else {\n          initWidget();\n      }\n  })();`;\n\n        return new NextResponse(script, {\n            headers: {\n                \"Content-Type\": \"application/javascript\",\n                \"Cache-Control\": \"public, max-age=3600\",\n                \"Access-Control-Allow-Origin\": \"*\",\n            },\n        });\n    } catch (error) {\n        console.error(\"❌ Failed to serve widget:\", error);\n        return new NextResponse('console.error(\"Changerawr: Failed to load widget\");', {\n            status: 500,\n            headers: {\"Content-Type\": \"application/javascript\"},\n        });\n    }\n}\n\n/**\n * @method PUT\n * @description Update a widget (auth required)\n */\nexport async function PUT(\n    request: Request,\n    context: { params: Promise<{ projectId: string; widgetId: string }> }\n) {\n    try {\n        await validateAuthAndGetUser();\n        const {widgetId} = await context.params;\n        const body = await request.json();\n\n        const {name, variant, settings, customCSS, isActive} = body;\n\n        if (variant) {\n            const validVariants = [\"classic\", \"floating\", \"modal\", \"announcement\"];\n            if (!validVariants.includes(variant)) {\n                return sendError(`Invalid variant. Must be one of: ${validVariants.join(\", \")}`, 400);\n            }\n        }\n\n        const widget = await db.widget.update({\n            where: {id: widgetId},\n            data: {\n                ...(name && {name}),\n                ...(variant && {variant}),\n                ...(settings !== undefined && {settings}),\n                ...(customCSS !== undefined && {customCSS}),\n                ...(isActive !== undefined && {isActive}),\n            },\n        });\n\n        return NextResponse.json({widget});\n    } catch (error) {\n        console.error(\"❌ Failed to update widget:\", error);\n        if (error instanceof Error && error.message === \"Authentication required\") {\n            return sendError(\"Unauthorized\", 401);\n        }\n        return sendError(\"Failed to update widget\", 500);\n    }\n}\n\n/**\n * @method DELETE\n * @description Delete a widget (auth required)\n */\nexport async function DELETE(\n    request: Request,\n    context: { params: Promise<{ projectId: string; widgetId: string }> }\n) {\n    try {\n        await validateAuthAndGetUser();\n        const {widgetId} = await context.params;\n\n        await db.widget.delete({\n            where: {id: widgetId},\n        });\n\n        return sendSuccess({success: true});\n    } catch (error) {\n        console.error(\"❌ Failed to delete widget:\", error);\n        if (error instanceof Error && error.message === \"Authentication required\") {\n            return sendError(\"Unauthorized\", 401);\n        }\n        return sendError(\"Failed to delete widget\", 500);\n    }\n}\n\n/**\n * @method POST\n * @description Create a new widget (auth required)\n */\nexport async function POST(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        await validateAuthAndGetUser();\n        const {projectId} = await context.params;\n        const body = await request.json();\n\n        const name = body.name as string | undefined;\n        const variant = body.variant as string | undefined;\n        const settings = body.settings as Record<string, any> | undefined;\n        const customCSS = body.customCSS as string | null | undefined;\n        const isActive = body.isActive as boolean | undefined;\n\n        if (!name || !variant) {\n            return sendError(\"Missing required fields: name, variant\", 400);\n        }\n\n        const validVariants = [\"classic\", \"floating\", \"modal\", \"announcement\"];\n        if (!validVariants.includes(variant)) {\n            return sendError(`Invalid variant. Must be one of: ${validVariants.join(\", \")}`, 400);\n        }\n\n        const widget = await db.widget.create({\n            data: {\n                project: {connect: {id: projectId}},\n                name,\n                variant,\n                settings: settings ?? {},\n                customCSS: customCSS ?? null,\n                isActive: isActive ?? true,\n            } as Prisma.WidgetCreateInput, // 👈 explicitly cast\n        });\n\n\n        return NextResponse.json({widget}, {status: 201});\n    } catch (error) {\n        console.error(\"❌ Widget creation failed:\");\n        console.error(error instanceof Error ? error.stack || error.message : error);\n\n        if (error instanceof Error && error.message === \"Authentication required\") {\n            return sendError(\"Unauthorized\", 401);\n        }\n\n        return sendError(\"Failed to create widget\", 500);\n    }\n}\n"
  },
  {
    "path": "app/api/integrations/widget/[projectId]/list/route.ts",
    "content": "import {NextResponse} from \"next/server\";\nimport {db} from \"@/lib/db\";\nimport {validateAuthAndGetUser, sendError} from \"@/lib/utils/changelog\";\n\n/**\n * @method GET\n * @description List all widgets for a project (auth required)\n */\nexport async function GET(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        await validateAuthAndGetUser();\n        const {projectId} = await context.params;\n\n        const widgets = await db.widget.findMany({\n            where: {projectId},\n            orderBy: {createdAt: 'desc'}\n        });\n\n        return NextResponse.json({widgets});\n\n    } catch (error) {\n        if (error instanceof Error && error.message === 'Authentication required') {\n            return sendError('Unauthorized', 401);\n        }\n        console.error('Failed to list widgets:', error);\n        return sendError('Failed to list widgets', 500);\n    }\n}\n"
  },
  {
    "path": "app/api/integrations/widget/[projectId]/route.ts",
    "content": "import { NextResponse } from \"next/server\";\nimport { db } from \"@/lib/db\";\n\n/**\n * @method GET\n * @description Gets the widget loader script for a public project\n * @query {\n *   projectId: String, required\n * }\n * @response 200 Widget loader script content\n * @error 403 Project is not public\n * @error 404 Project not found\n * @error 500 An unexpected error occurred\n */\nexport async function GET(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        const { projectId } = await (async () => context.params)();\n\n        // Check if project exists and is public\n        const project = await db.project.findUnique({\n            where: {\n                id: projectId\n            },\n            select: {\n                id: true,\n                isPublic: true,\n            }\n        });\n\n        if (!project) {\n            return new NextResponse(\n                JSON.stringify({ error: 'Project not found' }),\n                { status: 404 }\n            );\n        }\n\n        if (!project.isPublic) {\n            return new NextResponse(\n                JSON.stringify({ error: 'Project is not public' }),\n                { status: 403 }\n            );\n        }\n\n        // Generate the loader script\n        const script = `\n        (function() {\n            // Get current script and validate\n            const currentScript = document.currentScript;\n            if (!currentScript) {\n                console.error('Changerawr: Could not initialize widget - script context not found');\n                return;\n            }\n            \n            // Extract configuration from data attributes\n            const options = {\n                projectId: '${projectId}',\n                theme: currentScript.getAttribute('data-theme') || 'light',\n                position: currentScript.getAttribute('data-position') || 'bottom-right',\n                maxHeight: currentScript.getAttribute('data-max-height') || '400px',\n                isPopup: currentScript.getAttribute('data-popup') === 'true',\n                trigger: currentScript.getAttribute('data-trigger'),\n                maxEntries: currentScript.getAttribute('data-max-entries') \n                    ? parseInt(currentScript.getAttribute('data-max-entries'), 10) \n                    : 3,\n                hidden: currentScript.getAttribute('data-popup') === 'true'\n            };\n\n            // Validate position value\n            if (!['bottom-right', 'bottom-left', 'top-right', 'top-left'].includes(options.position)) {\n                console.warn(\\`Changerawr: Invalid position '\\${options.position}', defaulting to bottom-right\\`);\n                options.position = 'bottom-right';\n            }\n\n            // Create initialization function with proper container handling\n            const initWidget = () => {\n                // Create container with unique ID\n                const container = document.createElement('div');\n                container.id = \\`changerawr-widget-\\${Math.random().toString(36).substr(2, 9)}\\`;\n                \n                // Handle container placement based on widget type\n                const isPopupWithTrigger = options.isPopup && options.trigger && options.trigger !== 'immediate';\n                \n                if (isPopupWithTrigger) {\n                    // Find trigger button for popup widgets\n                    const triggerButton = document.getElementById(options.trigger);\n                    if (!triggerButton) {\n                        console.error(\\`Changerawr: Trigger button '\\${options.trigger}' not found\\`);\n                        return;\n                    }\n\n                    // Set initial container state for popup\n                    container.style.setProperty('display', 'none', 'important');\n                    document.body.appendChild(container);\n\n                    // Setup trigger button accessibility\n                    triggerButton.setAttribute('aria-expanded', 'false');\n                    triggerButton.setAttribute('aria-haspopup', 'dialog');\n                    triggerButton.setAttribute('aria-controls', container.id);\n                } else {\n                    // For inline widgets or immediate popups\n                    currentScript.parentNode.insertBefore(container, currentScript);\n                }\n\n                // Load and initialize widget bundle\n                const script = document.createElement('script');\n                script.src = '${process.env.NEXT_PUBLIC_APP_URL}/widget-bundle.js';\n                script.onload = () => {\n                    // Initialize the widget with proper positioning\n                    const widget = window.ChangerawrWidget.init({\n                        container,\n                        ...options,\n                        // Ensure popup state is properly set\n                        isPopup: isPopupWithTrigger\n                    });\n\n                    // Setup trigger button interactions if needed\n                    if (isPopupWithTrigger) {\n                        const triggerButton = document.getElementById(options.trigger);\n                        \n                        // Handle click\n                        triggerButton.addEventListener('click', (e) => {\n                            e.preventDefault();\n                            widget.toggle();\n                            triggerButton.setAttribute('aria-expanded', widget.isOpen.toString());\n                        });\n\n                        // Handle keyboard\n                        triggerButton.addEventListener('keydown', (e) => {\n                            if (e.key === 'Enter' || e.key === ' ') {\n                                e.preventDefault();\n                                widget.toggle();\n                                triggerButton.setAttribute('aria-expanded', widget.isOpen.toString());\n                            }\n                        });\n                    }\n                };\n\n                script.onerror = () => {\n                    console.error('Changerawr: Failed to load widget bundle');\n                    container.innerHTML = 'Failed to load widget';\n                };\n\n                document.head.appendChild(script);\n            };\n\n            // Initialize based on document readiness\n            if (document.readyState === 'loading') {\n                document.addEventListener('DOMContentLoaded', initWidget);\n            } else {\n                initWidget();\n            }\n        })();`;\n\n        return new NextResponse(script, {\n            status: 200,\n            headers: {\n                'Content-Type': 'application/javascript',\n                'Cache-Control': 'public, max-age=3600',\n                'Access-Control-Allow-Origin': '*',\n                'Access-Control-Allow-Methods': 'GET'\n            }\n        });\n\n    } catch (error) {\n        console.error('Failed to serve widget script:', error);\n        return new NextResponse(\n            JSON.stringify({ error: 'Failed to serve widget script' }),\n            {\n                status: 500,\n                headers: { 'Content-Type': 'application/json' }\n            }\n        );\n    }\n}\n\n// Handle preflight CORS requests\nexport async function OPTIONS() {\n    return new NextResponse(null, {\n        headers: {\n            'Access-Control-Allow-Origin': '*',\n            'Access-Control-Allow-Methods': 'GET',\n            'Access-Control-Allow-Headers': 'Content-Type',\n        },\n    });\n}"
  },
  {
    "path": "app/api/internal/agent/version/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\n\nexport const runtime = 'nodejs'\n\n/**\n * Internal API to fetch nginx-agent version and status.\n * This endpoint proxies the request to the nginx-agent /version endpoint.\n *\n * Authentication: X-Internal-Secret header must match INTERNAL_API_SECRET.\n */\nexport async function GET(request: NextRequest) {\n    const secret = process.env.INTERNAL_API_SECRET\n    const agentUrl = process.env.NGINX_AGENT_URL\n\n    if (!secret) {\n        return NextResponse.json(\n            { error: 'INTERNAL_API_SECRET not configured' },\n            { status: 503 },\n        )\n    }\n\n    if (!agentUrl) {\n        return NextResponse.json(\n            { error: 'NGINX_AGENT_URL not configured' },\n            { status: 503 },\n        )\n    }\n\n    const authHeader = request.headers.get('X-Internal-Secret')\n\n    if (authHeader !== secret) {\n        return NextResponse.json(\n            { error: 'Unauthorized' },\n            { status: 401 },\n        )\n    }\n\n    try {\n        const response = await fetch(`${agentUrl}/version`, {\n            signal: AbortSignal.timeout(5000),\n        })\n\n        if (!response.ok) {\n            return NextResponse.json(\n                { error: 'Failed to fetch agent version', status: response.status },\n                { status: 502 },\n            )\n        }\n\n        const data = await response.json()\n        return NextResponse.json(data)\n    } catch (error) {\n        console.error('[internal/agent/version] Error fetching version:', error)\n        return NextResponse.json(\n            { error: 'Failed to connect to nginx-agent' },\n            { status: 503 },\n        )\n    }\n}\n"
  },
  {
    "path": "app/api/internal/cert/[domain]/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport { getActiveCertBundle } from '@/lib/custom-domains/ssl/service'\n\nexport const runtime = 'nodejs'\n\n// Internal API for nginx-agent to fetch decrypted certificate bundles.\n// Authentication: X-Internal-Secret header must match INTERNAL_API_SECRET.\nexport async function GET(\n    request: NextRequest,\n    { params }: { params: Promise<{ domain: string }> },\n) {\n    const { domain } = await params\n    const secret = process.env.INTERNAL_API_SECRET\n\n    if (!secret) {\n        return NextResponse.json(\n            { error: 'INTERNAL_API_SECRET not configured' },\n            { status: 503 },\n        )\n    }\n\n    const authHeader = request.headers.get('X-Internal-Secret')\n\n    if (authHeader !== secret) {\n        return NextResponse.json(\n            { error: 'Unauthorized' },\n            { status: 401 },\n        )\n    }\n\n    if (!domain) {\n        return NextResponse.json(\n            { error: 'Domain parameter required' },\n            { status: 400 },\n        )\n    }\n\n    try {\n        const bundle = await getActiveCertBundle(domain)\n\n        if (!bundle) {\n            return NextResponse.json(\n                { error: 'No active certificate found for this domain' },\n                { status: 404 },\n            )\n        }\n\n        return NextResponse.json({\n            domain: domain,\n            privateKey: bundle.privateKey,\n            certificate: bundle.certificate,\n            fullChain: bundle.fullChain,\n            expiresAt: bundle.expiresAt.toISOString(),\n        })\n    } catch (error) {\n        console.error('[internal/cert] Error fetching certificate:', error)\n        return NextResponse.json(\n            { error: 'Internal server error' },\n            { status: 500 },\n        )\n    }\n}\n"
  },
  {
    "path": "app/api/internal/ip-config/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\n\n/**\n * Internal endpoint used by Next.js middleware to fetch the IP whitelist config.\n * Protected by INTERNAL_API_SECRET — not intended to be called by clients.\n */\nexport async function GET(request: NextRequest) {\n    const secret = request.headers.get('x-internal-secret')\n    const expected = process.env.INTERNAL_API_SECRET\n\n    if (!expected || !secret || secret !== expected) {\n        return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n    }\n\n    try {\n        const config = await db.systemConfig.findFirst({\n            select: {\n                panelIpWhitelistEnabled: true,\n                panelIpWhitelist: true,\n            },\n        })\n\n        return NextResponse.json({\n            enabled: config?.panelIpWhitelistEnabled ?? false,\n            whitelist: config?.panelIpWhitelist ?? [],\n        })\n    } catch {\n        // Fail open — if we can't read config, don't block users\n        return NextResponse.json({ enabled: false, whitelist: [] })\n    }\n}\n"
  },
  {
    "path": "app/api/projects/[projectId]/analytics/export/route.ts",
    "content": "import {NextResponse} from 'next/server';\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog';\nimport {getProjectAnalytics, getTimeRange} from '@/lib/utils/analytics';\nimport {db} from '@/lib/db';\nimport {z} from 'zod';\nimport type {AnalyticsPeriod} from '@/lib/types/analytics';\n\nconst exportQuerySchema = z.object({\n    period: z.enum(['7d', '30d', '90d', '1y']).optional().default('30d'),\n    format: z.enum(['csv', 'json']).optional().default('csv'),\n});\n\n/**\n * @method GET\n * @description Export project analytics data\n * @response 200 CSV or JSON file download\n * @error 403 Unauthorized\n * @error 404 Project not found\n * @error 500 Export failed\n */\nexport async function GET(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        const params = await context.params;\n        const projectId = params.projectId;\n\n        // Ensure we have a valid projectId\n        if (!projectId) {\n            return NextResponse.json(\n                {error: 'Project ID is required'},\n                {status: 400}\n            );\n        }\n\n        const user = await validateAuthAndGetUser();\n\n        // Check if analytics are enabled\n        const systemConfig = await db.systemConfig.findFirst();\n        if (!systemConfig?.enableAnalytics) {\n            return NextResponse.json(\n                {error: 'Analytics are disabled'},\n                {status: 403}\n            );\n        }\n\n        // Verify project exists and user has access\n        const project = await db.project.findUnique({\n            where: {id: projectId},\n            select: {id: true, name: true, isPublic: true}\n        });\n\n        if (!project) {\n            return NextResponse.json(\n                {error: 'Project not found'},\n                {status: 404}\n            );\n        }\n\n        // Analytics export is internal data — viewers cannot access it\n        if (user.role === 'VIEWER') {\n            return NextResponse.json(\n                {error: 'Not authorized'},\n                {status: 403}\n            );\n        }\n\n        // Parse query parameters\n        const url = new URL(request.url);\n        const queryParams = {\n            period: (url.searchParams.get('period') as AnalyticsPeriod) || '30d',\n            format: (url.searchParams.get('format') as 'csv' | 'json') || 'csv',\n        };\n\n        const validatedParams = exportQuerySchema.parse(queryParams);\n        const timeRange = getTimeRange(validatedParams.period);\n\n        // Get analytics data\n        const analyticsData = await getProjectAnalytics(projectId, timeRange);\n\n        const exportData = {\n            projectId: project.id,\n            projectName: project.name,\n            period: validatedParams.period,\n            timeRange: {\n                start: timeRange.start.toISOString(),\n                end: timeRange.end.toISOString()\n            },\n            exportedAt: new Date().toISOString(),\n            exportedBy: user.email,\n            data: {\n                summary: {\n                    totalViews: analyticsData.totalViews,\n                    uniqueVisitors: analyticsData.uniqueVisitors,\n                },\n                daily: analyticsData.dailyViews,\n                countries: analyticsData.topCountries,\n                entries: analyticsData.topEntries,\n                referrers: analyticsData.topReferrers,\n            }\n        };\n\n        if (validatedParams.format === 'json') {\n            return NextResponse.json(exportData, {\n                headers: {\n                    'Content-Disposition': `attachment; filename=\"analytics-${projectId}-${validatedParams.period}.json\"`,\n                    'Content-Type': 'application/json',\n                }\n            });\n        }\n\n        // Generate CSV\n        const csvLines = [\n            // Header\n            'Type,Date,Value,Label,Count',\n            // Summary\n            'Summary,,Total Views,,',\n            'Summary,,Unique Visitors,,',\n            // Daily data\n            ...analyticsData.dailyViews.map(day =>\n                `Daily,${day.date},Views,,${day.views}`\n            ),\n            ...analyticsData.dailyViews.map(day =>\n                `Daily,${day.date},Unique Visitors,,${day.uniqueVisitors}`\n            ),\n            // Countries\n            ...analyticsData.topCountries.map(country =>\n                `Country,,${country.country},,${country.count}`\n            ),\n            // Entries\n            ...analyticsData.topEntries.map(entry =>\n                `Entry,,${entry.title.replace(/,/g, ';')},Views,${entry.views}`\n            ),\n            ...analyticsData.topEntries.map(entry =>\n                `Entry,,${entry.title.replace(/,/g, ';')},Visitors,${entry.uniqueVisitors}`\n            ),\n            // Referrers\n            ...analyticsData.topReferrers.map(referrer =>\n                `Referrer,,${referrer.referrer},,${referrer.count}`\n            ),\n        ];\n\n        const csvContent = csvLines.join('\\n');\n\n        return new NextResponse(csvContent, {\n            headers: {\n                'Content-Disposition': `attachment; filename=\"analytics-${projectId}-${validatedParams.period}.csv\"`,\n                'Content-Type': 'text/csv',\n            }\n        });\n\n    } catch (error) {\n        console.error('Error exporting analytics:', error);\n        return NextResponse.json(\n            {error: 'Failed to export analytics data'},\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/analytics/route.ts",
    "content": "import {NextResponse} from 'next/server';\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog';\nimport {getProjectAnalytics, getTimeRange} from '@/lib/utils/analytics';\nimport {db} from '@/lib/db';\nimport {z} from 'zod';\nimport type {AnalyticsPeriod, AnalyticsQueryParams} from '@/lib/types/analytics';\n\nconst analyticsQuerySchema = z.object({\n    period: z.enum(['7d', '30d', '90d', '1y']).optional().default('30d'),\n    startDate: z.string().optional(),\n    endDate: z.string().optional(),\n});\n\n/**\n * @method GET\n * @description Get analytics data for a specific project\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\" },\n *     \"data\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"totalViews\": { \"type\": \"number\" },\n *         \"uniqueVisitors\": { \"type\": \"number\" },\n *         \"topCountries\": {\n *           \"type\": \"array\",\n *           \"items\": {\n *             \"type\": \"object\",\n *             \"properties\": {\n *               \"country\": { \"type\": \"string\" },\n *               \"count\": { \"type\": \"number\" }\n *             }\n *           }\n *         },\n *         \"dailyViews\": {\n *           \"type\": \"array\",\n *           \"items\": {\n *             \"type\": \"object\",\n *             \"properties\": {\n *               \"date\": { \"type\": \"string\" },\n *               \"views\": { \"type\": \"number\" },\n *               \"uniqueVisitors\": { \"type\": \"number\" }\n *             }\n *           }\n *         },\n *         \"topEntries\": {\n *           \"type\": \"array\",\n *           \"items\": {\n *             \"type\": \"object\",\n *             \"properties\": {\n *               \"entryId\": { \"type\": \"string\" },\n *               \"title\": { \"type\": \"string\" },\n *               \"views\": { \"type\": \"number\" },\n *               \"uniqueVisitors\": { \"type\": \"number\" }\n *             }\n *           }\n *         },\n *         \"topReferrers\": {\n *           \"type\": \"array\",\n *           \"items\": {\n *             \"type\": \"object\",\n *             \"properties\": {\n *               \"referrer\": { \"type\": \"string\" },\n *               \"count\": { \"type\": \"number\" }\n *             }\n *           }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 400 Invalid parameters\n * @error 403 Unauthorized - User not authorized to access project\n * @error 404 Project not found\n * @error 500 Internal server error\n */\nexport async function GET(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        const params = await context.params;\n        const projectId = params.projectId;\n\n        // Ensure we have a valid projectId\n        if (!projectId) {\n            return NextResponse.json(\n                {\n                    success: false,\n                    error: 'Project ID is required'\n                },\n                {status: 400}\n            );\n        }\n\n        const user = await validateAuthAndGetUser();\n\n        // Check if analytics are enabled\n        const systemConfig = await db.systemConfig.findFirst();\n        if (!systemConfig?.enableAnalytics) {\n            return NextResponse.json(\n                {\n                    success: false,\n                    error: 'Analytics are disabled'\n                },\n                {status: 403}\n            );\n        }\n\n        // Verify project exists and user has access\n        const project = await db.project.findUnique({\n            where: {id: projectId},\n            select: {\n                id: true,\n                name: true,\n                isPublic: true\n            }\n        });\n\n        if (!project) {\n            return NextResponse.json(\n                {\n                    success: false,\n                    error: 'Project not found'\n                },\n                {status: 404}\n            );\n        }\n\n        // Analytics is internal data — viewers cannot access it regardless of project visibility\n        if (user.role === 'VIEWER') {\n            return NextResponse.json(\n                {\n                    success: false,\n                    error: 'Not authorized to view analytics for this project'\n                },\n                {status: 403}\n            );\n        }\n\n        // Parse query parameters\n        const url = new URL(request.url);\n        const queryParams: AnalyticsQueryParams = {\n            period: (url.searchParams.get('period') as AnalyticsPeriod) || '30d',\n            startDate: url.searchParams.get('startDate') || undefined,\n            endDate: url.searchParams.get('endDate') || undefined,\n        };\n\n        const validatedParams = analyticsQuerySchema.parse(queryParams);\n\n        // Determine time range\n        let timeRange;\n        if (validatedParams.startDate && validatedParams.endDate) {\n            timeRange = {\n                start: new Date(validatedParams.startDate),\n                end: new Date(validatedParams.endDate)\n            };\n        } else {\n            timeRange = getTimeRange(validatedParams.period);\n        }\n\n        // Get analytics data\n        const analyticsData = await getProjectAnalytics(projectId, timeRange);\n\n        return NextResponse.json({\n            success: true,\n            data: {\n                ...analyticsData,\n                period: validatedParams.period,\n                timeRange: {\n                    start: timeRange.start.toISOString(),\n                    end: timeRange.end.toISOString()\n                },\n                projectId,\n                projectName: project.name\n            }\n        });\n\n    } catch (error) {\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {\n                    success: false,\n                    error: 'Invalid parameters',\n                    details: error.errors\n                },\n                {status: 400}\n            );\n        }\n\n        console.error('Error fetching project analytics:', error);\n        return NextResponse.json(\n            {\n                success: false,\n                error: 'Failed to fetch analytics data'\n            },\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/api-keys/[keyId]/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server';\nimport { z } from 'zod';\nimport { authenticateRequest } from '@/lib/auth/api-key';\nimport { createAuditLog } from '@/lib/utils/auditLog';\nimport { db } from '@/lib/db';\n\nconst updateApiKeySchema = z.object({\n    name: z.string().min(1).max(100).optional(),\n    isRevoked: z.boolean().optional(),\n    permissions: z.array(z.string()).optional(),\n});\n\nexport async function GET(\n    request: NextRequest,\n    { params }: { params: Promise<{ projectId: string; keyId: string }> }\n) {\n    try {\n        const ctx = await authenticateRequest(request);\n\n        if (!ctx) {\n            return NextResponse.json(\n                { error: 'Authentication required' },\n                { status: 401 }\n            );\n        }\n\n        const { projectId, keyId } = await params;\n\n        const apiKey = await db.apiKey.findFirst({\n            where: {\n                id: keyId,\n                projectId,\n                userId: ctx.userId\n            },\n            select: {\n                id: true,\n                name: true,\n                key: true,\n                lastUsed: true,\n                createdAt: true,\n                expiresAt: true,\n                isRevoked: true,\n                permissions: true,\n                projectId: true,\n            }\n        });\n\n        if (!apiKey) {\n            return NextResponse.json(\n                { error: 'API key not found' },\n                { status: 404 }\n            );\n        }\n\n        // Mask the key - don't return full key\n        return NextResponse.json({\n            ...apiKey,\n            key: apiKey.key.slice(0, 12) + '...'\n        });\n    } catch (error) {\n        console.error('Failed to fetch API key:', error);\n        return NextResponse.json(\n            { error: 'Failed to fetch API key' },\n            { status: 500 }\n        );\n    }\n}\n\nexport async function PATCH(\n    request: NextRequest,\n    { params }: { params: Promise<{ projectId: string; keyId: string }> }\n) {\n    try {\n        const ctx = await authenticateRequest(request);\n\n        if (!ctx) {\n            return NextResponse.json(\n                { error: 'Authentication required' },\n                { status: 401 }\n            );\n        }\n\n        const { projectId, keyId } = await params;\n\n        // Verify ownership\n        const existingKey = await db.apiKey.findFirst({\n            where: {\n                id: keyId,\n                projectId,\n                userId: ctx.userId\n            }\n        });\n\n        if (!existingKey) {\n            return NextResponse.json(\n                { error: 'API key not found' },\n                { status: 404 }\n            );\n        }\n\n        const body = await request.json();\n        const validatedData = updateApiKeySchema.parse(body);\n\n        const updatedKey = await db.apiKey.update({\n            where: { id: keyId },\n            data: validatedData\n        });\n\n        // Create audit log\n        try {\n            await createAuditLog(\n                'UPDATE_PROJECT_API_KEY',\n                ctx.userId,\n                ctx.userId,\n                {\n                    apiKeyId: keyId,\n                    apiKeyName: updatedKey.name,\n                    projectId,\n                    changes: validatedData\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n        }\n\n        return NextResponse.json({\n            ...updatedKey,\n            key: updatedKey.key.slice(0, 12) + '...' // Mask key\n        });\n    } catch (error) {\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: 'Invalid request data', details: error.errors },\n                { status: 400 }\n            );\n        }\n\n        console.error('Failed to update API key:', error);\n        return NextResponse.json(\n            { error: 'Failed to update API key' },\n            { status: 500 }\n        );\n    }\n}\n\nexport async function DELETE(\n    request: NextRequest,\n    { params }: { params: Promise<{ projectId: string; keyId: string }> }\n) {\n    try {\n        const ctx = await authenticateRequest(request);\n\n        if (!ctx) {\n            return NextResponse.json(\n                { error: 'Authentication required' },\n                { status: 401 }\n            );\n        }\n\n        const { projectId, keyId } = await params;\n\n        // Verify ownership\n        const existingKey = await db.apiKey.findFirst({\n            where: {\n                id: keyId,\n                projectId,\n                userId: ctx.userId\n            }\n        });\n\n        if (!existingKey) {\n            return NextResponse.json(\n                { error: 'API key not found' },\n                { status: 404 }\n            );\n        }\n\n        await db.apiKey.delete({\n            where: { id: keyId }\n        });\n\n        // Create audit log\n        try {\n            await createAuditLog(\n                'DELETE_PROJECT_API_KEY',\n                ctx.userId,\n                ctx.userId,\n                {\n                    apiKeyId: keyId,\n                    apiKeyName: existingKey.name,\n                    projectId\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n        }\n\n        return NextResponse.json({ success: true });\n    } catch (error) {\n        console.error('Failed to delete API key:', error);\n        return NextResponse.json(\n            { error: 'Failed to delete API key' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/api-keys/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server';\nimport { z } from 'zod';\nimport { authenticateRequest } from '@/lib/auth/api-key';\nimport { createAuditLog } from '@/lib/utils/auditLog';\nimport { db } from '@/lib/db';\nimport { nanoid } from 'nanoid';\nimport { Prisma } from '@prisma/client';\n\nconst createApiKeySchema = z.object({\n    name: z.string().min(1).max(100),\n    expiresAt: z.string().datetime().optional(),\n    permissions: z.array(z.string()).optional(),\n    isGlobal: z.boolean().optional(),\n});\n\nexport async function GET(\n    request: NextRequest,\n    { params }: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        const ctx = await authenticateRequest(request);\n\n        if (!ctx) {\n            return NextResponse.json(\n                { error: 'Authentication required' },\n                { status: 401 }\n            );\n        }\n\n        const { projectId } = await params;\n        const { searchParams } = new URL(request.url);\n        const userIdFilter = searchParams.get('userId');\n\n        // Verify project exists and user has access\n        const project = await db.project.findUnique({\n            where: { id: projectId }\n        });\n\n        if (!project) {\n            return NextResponse.json(\n                { error: 'Project not found' },\n                { status: 404 }\n            );\n        }\n\n        // Check if user is admin\n        const user = await db.user.findUnique({\n            where: { id: ctx.userId },\n            select: { role: true }\n        });\n\n        const isAdmin = user?.role === 'ADMIN';\n\n        // Build the where clause based on user role\n        const whereClause: Prisma.ApiKeyWhereInput = {\n            projectId,\n        };\n\n        if (isAdmin) {\n            // Admins can see:\n            // 1. Their own keys (both global and private)\n            // 2. Other users' global keys\n            // 3. If userIdFilter is provided, filter by that user\n            if (userIdFilter) {\n                whereClause.userId = userIdFilter;\n            } else {\n                whereClause.OR = [\n                    { userId: ctx.userId }, // Own keys\n                    { isGlobal: true }      // Others' global keys\n                ];\n            }\n        } else {\n            // Non-admins can only see their own keys\n            whereClause.userId = ctx.userId;\n        }\n\n        // Fetch project-specific API keys\n        const apiKeys = await db.apiKey.findMany({\n            where: whereClause,\n            select: {\n                id: true,\n                name: true,\n                key: true,\n                lastUsed: true,\n                createdAt: true,\n                expiresAt: true,\n                isRevoked: true,\n                permissions: true,\n                projectId: true,\n                isGlobal: true,\n                userId: true,\n                user: {\n                    select: {\n                        id: true,\n                        name: true,\n                        email: true,\n                    }\n                }\n            },\n            orderBy: {\n                createdAt: 'desc'\n            }\n        });\n\n        return NextResponse.json(apiKeys);\n    } catch (error) {\n        console.error('Failed to fetch project API keys:', error);\n        return NextResponse.json(\n            { error: 'Failed to fetch API keys' },\n            { status: 500 }\n        );\n    }\n}\n\nexport async function POST(\n    request: NextRequest,\n    { params }: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        const ctx = await authenticateRequest(request);\n\n        if (!ctx) {\n            return NextResponse.json(\n                { error: 'Authentication required' },\n                { status: 401 }\n            );\n        }\n\n        const { projectId } = await params;\n\n        // Check if admin-only API key creation is enabled\n        const systemConfig = await db.systemConfig.findFirst({\n            where: { id: 1 },\n            select: { adminOnlyApiKeyCreation: true }\n        });\n\n        if (systemConfig?.adminOnlyApiKeyCreation) {\n            // Verify user is an admin\n            const user = await db.user.findUnique({\n                where: { id: ctx.userId },\n                select: { role: true }\n            });\n\n            if (user?.role !== 'ADMIN') {\n                return NextResponse.json(\n                    { error: 'Only administrators can create API keys. Please request an API key from an administrator.' },\n                    { status: 403 }\n                );\n            }\n        }\n\n        // Verify project exists\n        const project = await db.project.findUnique({\n            where: { id: projectId }\n        });\n\n        if (!project) {\n            return NextResponse.json(\n                { error: 'Project not found' },\n                { status: 404 }\n            );\n        }\n\n        const body = await request.json();\n        const validatedData = createApiKeySchema.parse(body);\n\n        // Generate unique API key\n        const apiKeyString = `chr_${nanoid(32)}`;\n\n        const apiKey = await db.apiKey.create({\n            data: {\n                name: validatedData.name,\n                key: apiKeyString,\n                expiresAt: validatedData.expiresAt ? new Date(validatedData.expiresAt) : null,\n                permissions: validatedData.permissions || [],\n                userId: ctx.userId,\n                projectId, // Automatically scope to project\n                isGlobal: validatedData.isGlobal ?? false\n            }\n        });\n\n        // Create audit log\n        try {\n            await createAuditLog(\n                'CREATE_PROJECT_API_KEY',\n                ctx.userId,\n                ctx.userId,\n                {\n                    apiKeyId: apiKey.id,\n                    apiKeyName: apiKey.name,\n                    projectId,\n                    projectName: project.name,\n                    permissions: apiKey.permissions || []\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n        }\n\n        return NextResponse.json({\n            ...apiKey,\n            key: apiKeyString // Only return full key on creation\n        }, { status: 201 });\n    } catch (error) {\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: 'Invalid request data', details: error.errors },\n                { status: 400 }\n            );\n        }\n\n        console.error('Failed to create project API key:', error);\n        return NextResponse.json(\n            { error: 'Failed to create API key' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/catch-up/ai-summary/route.ts",
    "content": "import {NextResponse} from 'next/server';\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog';\nimport {db} from '@/lib/db';\nimport {z} from 'zod';\nimport {SectonClient} from '@/lib/utils/ai/secton';\nimport {AIMessage} from '@/lib/utils/ai/types';\n\nconst aiSummarySchema = z.object({\n    since: z.string(),\n    entries: z.array(z.object({\n        id: z.string(),\n        title: z.string(),\n        content: z.string(),\n        version: z.string().nullable(),\n        publishedAt: z.string().nullable(),\n        tags: z.array(z.object({\n            id: z.string(),\n            name: z.string(),\n            color: z.string().nullable(),\n        })),\n    })),\n    summary: z.object({\n        features: z.number(),\n        fixes: z.number(),\n        other: z.number(),\n    }),\n    fromDate: z.string(),\n});\n\ntype AISummaryRequest = z.infer<typeof aiSummarySchema>;\n\ninterface CatchUpEntry {\n    id: string;\n    title: string;\n    content: string;\n    version: string | null;\n    publishedAt: string | null;\n    tags: Array<{\n        id: string;\n        name: string;\n        color: string | null;\n    }>;\n}\n\n/**\n * @method POST\n * @description Generate an AI-powered catch-up summary for changelog entries\n * @body {\n *   \"type\": \"object\",\n *   \"required\": [\"since\", \"entries\", \"summary\", \"fromDate\"],\n *   \"properties\": {\n *     \"since\": {\n *       \"type\": \"string\",\n *       \"description\": \"The time period for the catch-up\"\n *     },\n *     \"entries\": {\n *       \"type\": \"array\",\n *       \"description\": \"Array of changelog entries to summarize\"\n *     },\n *     \"summary\": {\n *       \"type\": \"object\",\n *       \"description\": \"Summary statistics of the entries\"\n *     },\n *     \"fromDate\": {\n *       \"type\": \"string\",\n *       \"description\": \"Start date for the catch-up period\"\n *     }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"summary\": {\n *       \"type\": \"string\",\n *       \"description\": \"AI-generated narrative summary\"\n *     },\n *     \"highlights\": {\n *       \"type\": \"array\",\n *       \"items\": { \"type\": \"string\" },\n *       \"description\": \"Key highlights extracted from the changes\"\n *     },\n *     \"readingTime\": {\n *       \"type\": \"number\",\n *       \"description\": \"Estimated reading time in minutes\"\n *     }\n *   }\n * }\n * @error 400 Invalid request data\n * @error 401 Unauthorized\n * @error 404 Project not found\n * @error 500 AI not configured or generation failed\n */\nexport async function POST(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        await validateAuthAndGetUser();\n        const {projectId} = await context.params;\n\n        const body = await request.json();\n        const validatedData: AISummaryRequest = aiSummarySchema.parse(body);\n\n        // Verify project exists\n        const project = await db.project.findUnique({\n            where: {id: projectId},\n            select: {id: true, name: true},\n        });\n\n        if (!project) {\n            return NextResponse.json(\n                {error: 'Project not found'},\n                {status: 404}\n            );\n        }\n\n        // Check if AI is enabled and configured\n        const systemConfig = await db.systemConfig.findFirst({\n            where: {id: 1},\n            select: {\n                enableAIAssistant: true,\n                aiApiKey: true,\n                aiDefaultModel: true,\n            },\n        });\n\n        if (!systemConfig?.enableAIAssistant) {\n            return NextResponse.json(\n                {error: 'AI assistant is not enabled'},\n                {status: 500}\n            );\n        }\n\n        if (!systemConfig.aiApiKey) {\n            return NextResponse.json(\n                {error: 'AI API key is not configured'},\n                {status: 500}\n            );\n        }\n\n        if (validatedData.entries.length === 0) {\n            return NextResponse.json(\n                {error: 'No entries to summarize'},\n                {status: 400}\n            );\n        }\n\n        try {\n            // Decrypt the API key directly\n            // const decryptedKey = decryptToken(systemConfig.aiApiKey);\n\n            // Create the AI client\n            const aiClient = new SectonClient({\n                apiKey: systemConfig.aiApiKey,\n                defaultModel: systemConfig.aiDefaultModel || 'copilot-zero',\n            });\n\n            // Validate API key\n            const isValid = await aiClient.validateApiKey();\n            if (!isValid) {\n                return NextResponse.json(\n                    {error: 'AI API key is invalid'},\n                    {status: 500}\n                );\n            }\n\n            // Create prompt\n            const prompt = createPrompt(project.name, validatedData.entries, validatedData.fromDate);\n\n            const messages: AIMessage[] = [\n                {role: 'user', content: prompt}\n            ];\n\n            // Generate AI summary\n            const response = await aiClient.createCompletion({\n                model: systemConfig.aiDefaultModel || 'copilot-zero',\n                messages,\n                temperature: 0.3,\n                max_tokens: 600,\n            });\n\n            const aiContent = response.messages[response.messages.length - 1]?.content;\n\n            if (!aiContent || aiContent.trim().length < 20) {\n                return NextResponse.json(\n                    {error: 'AI generated empty or invalid response'},\n                    {status: 500}\n                );\n            }\n\n            // Generate highlights\n            const highlights = validatedData.entries\n                .slice(0, 5)\n                .map(entry => {\n                    const version = entry.version ? ` (${entry.version})` : '';\n                    const topTags = entry.tags.slice(0, 2).map(tag => tag.name);\n                    const tagText = topTags.length > 0 ? ` • ${topTags.join(', ')}` : '';\n                    return `${entry.title}${version}${tagText}`;\n                });\n\n            // Calculate reading time\n            const wordCount = aiContent.split(/\\s+/).length;\n            const readingTime = Math.max(1, Math.ceil(wordCount / 200));\n\n            return NextResponse.json({\n                summary: aiContent.trim(),\n                highlights,\n                readingTime,\n            });\n\n        } catch (aiError) {\n            console.error('AI generation error:', aiError);\n            return NextResponse.json(\n                {error: 'Failed to generate AI summary'},\n                {status: 500}\n            );\n        }\n\n    } catch (error) {\n        console.error('Error in AI summary endpoint:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {error: 'Invalid request data', details: error.errors},\n                {status: 400}\n            );\n        }\n\n        return NextResponse.json(\n            {error: 'Internal server error'},\n            {status: 500}\n        );\n    }\n}\n\nfunction createPrompt(projectName: string, entries: CatchUpEntry[], fromDate: string): string {\n    const cleanProjectName = projectName.replace(/[^\\w\\s]/g, '').trim();\n    const cleanFromDate = new Date(fromDate).toLocaleDateString();\n\n    const entryTexts = entries.slice(0, 5).map((entry, index) => {\n        const title = entry.title.replace(/[^\\w\\s.,!?-]/g, '').trim();\n        const content = entry.content.replace(/[^\\w\\s.,!?-]/g, '').trim().substring(0, 150);\n        const version = entry.version ? ` (${entry.version})` : '';\n        const tags = entry.tags.slice(0, 3).map(tag => tag.name).join(', ');\n\n        return `${index + 1}. ${title}${version}\n${content}\n${tags ? `Tags: ${tags}` : ''}`;\n    }).join('\\n\\n');\n\n    return `Write a friendly project update summary for ${cleanProjectName}.\n\nTime period: Since ${cleanFromDate}\nTotal updates: ${entries.length}\n\nRecent changes:\n${entryTexts}\n\nCreate a polished 3-4 paragraph summary of these updates in markdown format. Focus on the impact and benefits of the changes, using a professional but conversational tone. Explain what has improved and why these updates matter for the project. End with insights about the project's current direction based on these developments.`;\n}"
  },
  {
    "path": "app/api/projects/[projectId]/catch-up/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { CatchUpService } from '@/lib/services/projects/catch-up/catch-up.service';\nimport { z } from 'zod';\n\nconst catchUpQuerySchema = z.object({\n    since: z.string().optional().default('auto'),\n});\n\n/**\n * @method GET\n * @description Get changelog catch-up data for a project since a specified date/version\n * @query {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"since\": {\n *       \"type\": \"string\",\n *       \"description\": \"Starting point for catch-up. Can be 'auto' (user's last login), version (e.g. 'v1.2.0'), relative date (e.g. '7d'), or ISO date\",\n *       \"default\": \"auto\",\n *       \"examples\": [\"auto\", \"v1.2.0\", \"7d\", \"2025-01-01\"]\n *     }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"fromDate\": {\n *       \"type\": \"string\",\n *       \"format\": \"date-time\",\n *       \"description\": \"The date we started looking from\"\n *     },\n *     \"fromVersion\": {\n *       \"type\": \"string\",\n *       \"description\": \"Version we started from (if applicable)\",\n *       \"nullable\": true\n *     },\n *     \"toVersion\": {\n *       \"type\": \"string\",\n *       \"description\": \"Latest version in the range\",\n *       \"nullable\": true\n *     },\n *     \"totalEntries\": {\n *       \"type\": \"integer\",\n *       \"description\": \"Total number of changelog entries found\"\n *     },\n *     \"summary\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"features\": { \"type\": \"integer\" },\n *         \"fixes\": { \"type\": \"integer\" },\n *         \"other\": { \"type\": \"integer\" }\n *       }\n *     },\n *     \"entries\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"title\": { \"type\": \"string\" },\n *           \"content\": { \"type\": \"string\" },\n *           \"version\": { \"type\": \"string\", \"nullable\": true },\n *           \"publishedAt\": { \"type\": \"string\", \"format\": \"date-time\", \"nullable\": true },\n *           \"tags\": {\n *             \"type\": \"array\",\n *             \"items\": {\n *               \"type\": \"object\",\n *               \"properties\": {\n *                 \"id\": { \"type\": \"string\" },\n *                 \"name\": { \"type\": \"string\" },\n *                 \"color\": { \"type\": \"string\", \"nullable\": true }\n *               }\n *             }\n *           }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 400 Invalid query parameters\n * @error 401 Unauthorized\n * @error 404 Project not found\n * @error 500 Internal server error\n */\nexport async function GET(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n        const { projectId } = await context.params;\n        const { searchParams } = new URL(request.url);\n\n        const queryResult = catchUpQuerySchema.safeParse({\n            since: searchParams.get('since'),\n        });\n\n        if (!queryResult.success) {\n            return NextResponse.json(\n                { error: 'Invalid query parameters', details: queryResult.error.errors },\n                { status: 400 }\n            );\n        }\n\n        const { since } = queryResult.data;\n\n        const catchUpData = await CatchUpService.getCatchUpData(\n            projectId,\n            user.id,\n            since\n        );\n\n        return NextResponse.json(catchUpData);\n    } catch (error) {\n        console.error('Error fetching catch-up data:', error);\n\n        if (error instanceof Error && error.message === 'Project not found') {\n            return NextResponse.json(\n                { error: 'Project not found' },\n                { status: 404 }\n            );\n        }\n\n        return NextResponse.json(\n            { error: 'Failed to fetch catch-up data' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/changelog/[entryId]/route.ts",
    "content": "import {NextResponse} from 'next/server'\nimport {validateAuthAndGetUser, generateExcerpt} from '@/lib/utils/changelog'\nimport {createAuditLog} from '@/lib/utils/auditLog'\nimport {db} from '@/lib/db'\nimport {Role} from \"@/lib/types/auth\"\nimport {postToSlack} from '@/lib/services/slack';\n\n/**\n * Get a changelog entry by ID\n * @method GET\n * @description Returns the details of a changelog entry by its ID, including its title, content, version, tags, and creation/update timestamps. Requires user authentication and permission to view the project.\n * @param {string} projectId - The ID of the project the entry belongs to.\n * @param {string} entryId - The ID of the changelog entry to retrieve.\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"title\": { \"type\": \"string\" },\n *     \"content\": { \"type\": \"string\" },\n *     \"version\": { \"type\": \"number\" },\n *     \"tags\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"name\": { \"type\": \"string\" },\n *           \"color\": { \"type\": \"string\" }\n *         }\n *       }\n *     },\n *     \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"updatedAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"projectId\": { \"type\": \"string\" }\n *   }\n * }\n * @error 401 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n * @error 403 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n * @error 404 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n */\nexport async function GET(\n    request: Request,\n    context: { params: Promise<{ projectId: string; entryId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n        const {projectId, entryId} = await (async () => context.params)();\n\n        // Log entry view attempt\n        try {\n            await createAuditLog(\n                'VIEW_CHANGELOG_ENTRY_ATTEMPT',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    projectId,\n                    entryId,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create view attempt audit log:', auditLogError);\n        }\n\n        const entry = await db.changelogEntry.findUnique({\n            where: {id: entryId},\n            include: {\n                tags: true,\n                changelog: {\n                    select: {\n                        projectId: true\n                    }\n                }\n            }\n        });\n\n        if (!entry) {\n            // Log entry not found\n            try {\n                await createAuditLog(\n                    'CHANGELOG_ENTRY_NOT_FOUND',\n                    user.id,\n                    user.id, // Use user's own ID to avoid foreign key issues\n                    {\n                        projectId,\n                        entryId,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create not found audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                {error: 'Entry not found'},\n                {status: 404}\n            );\n        }\n\n        // Verify the entry belongs to the requested project\n        if (entry.changelog.projectId !== projectId) {\n            // Log project mismatch\n            try {\n                await createAuditLog(\n                    'CHANGELOG_ENTRY_PROJECT_MISMATCH',\n                    user.id,\n                    user.id, // Use user's own ID to avoid foreign key issues\n                    {\n                        requestedProjectId: projectId,\n                        actualProjectId: entry.changelog.projectId,\n                        entryId,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create project mismatch audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                {error: 'Entry does not belong to this project'},\n                {status: 400}\n            );\n        }\n\n        // Log successful entry view\n        try {\n            await createAuditLog(\n                'VIEW_CHANGELOG_ENTRY_SUCCESS',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    projectId,\n                    entryId: entry.id,\n                    entryTitle: entry.title,\n                    entryVersion: entry.version,\n                    tagCount: entry.tags.length,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create view success audit log:', auditLogError);\n        }\n\n        // Remove changelog field from response\n        // eslint-disable-next-line @typescript-eslint/no-unused-vars\n        const {changelog, ...entryData} = entry;\n        return NextResponse.json(entryData);\n    } catch (error) {\n        console.error('Error fetching changelog entry:', error);\n\n        // Log error\n        try {\n            const {projectId, entryId} = await (async () => context.params)();\n            const user = await validateAuthAndGetUser();\n            await createAuditLog(\n                'CHANGELOG_ENTRY_ERROR',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    action: 'VIEW_ENTRY',\n                    projectId,\n                    entryId,\n                    error: error instanceof Error ? error.message : 'Unknown error',\n                    stack: error instanceof Error ? error.stack : undefined,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create error audit log:', auditLogError);\n        }\n\n        return NextResponse.json(\n            {error: 'Failed to fetch changelog entry'},\n            {status: 500}\n        );\n    }\n}\n\n/**\n * Update a changelog entry by ID\n * @method PUT\n * @description Updates the title, content, version, and tags of a changelog entry by its ID. Requires user authentication and permission to edit the project.\n * @param {string} projectId - The ID of the project the entry belongs to.\n * @param {string} entryId - The ID of the changelog entry to update.\n * @requestBody {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"title\": { \"type\": \"string\" },\n *     \"content\": { \"type\": \"string\" },\n *     \"version\": { \"type\": \"number\" },\n *     \"tags\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"name\": { \"type\": \"string\" },\n *           \"color\": { \"type\": \"string\" }\n *         }\n *       }\n *     }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"title\": { \"type\": \"string\" },\n *     \"content\": { \"type\": \"string\" },\n *     \"version\": { \"type\": \"number\" },\n *     \"tags\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"name\": { \"type\": \"string\" },\n *           \"color\": { \"type\": \"string\" }\n *         }\n *       }\n *     },\n *     \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"updatedAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"projectId\": { \"type\": \"string\" }\n *   }\n * }\n * @error 401 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n * @error 403 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n * @error 404 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n * @error 400 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" },\n *     \"details\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"message\": { \"type\": \"string\" },\n *           \"path\": { \"type\": \"string\" }\n *         }\n *       }\n *     }\n *   }\n * }\n */\nexport async function PUT(\n    request: Request,\n    context: { params: Promise<{ projectId: string; entryId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n        const {projectId, entryId} = await (async () => context.params)();\n        const requestBody = await request.json();\n        const {title, content, version, tags} = requestBody;\n\n        // Log update attempt\n        try {\n            await createAuditLog(\n                'UPDATE_CHANGELOG_ENTRY_ATTEMPT',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    projectId,\n                    entryId,\n                    requestedChanges: {\n                        titleChanged: !!title,\n                        contentChanged: !!content,\n                        versionChanged: !!version,\n                        tagCount: tags?.length || 0\n                    },\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create update attempt audit log:', auditLogError);\n        }\n\n        // Verify the entry exists and get existing data for comparison\n        const existingEntry = await db.changelogEntry.findUnique({\n            where: {id: entryId},\n            include: {\n                tags: true,\n                changelog: {\n                    select: {\n                        projectId: true\n                    }\n                }\n            }\n        });\n\n        if (!existingEntry) {\n            // Log entry not found\n            try {\n                await createAuditLog(\n                    'CHANGELOG_ENTRY_NOT_FOUND',\n                    user.id,\n                    user.id, // Use user's own ID to avoid foreign key issues\n                    {\n                        action: 'UPDATE_ENTRY',\n                        projectId,\n                        entryId,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create not found audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                {error: 'Entry not found'},\n                {status: 404}\n            );\n        }\n\n        // Verify the entry belongs to the project\n        if (existingEntry.changelog.projectId !== projectId) {\n            // Log project mismatch\n            try {\n                await createAuditLog(\n                    'CHANGELOG_ENTRY_PROJECT_MISMATCH',\n                    user.id,\n                    user.id, // Use user's own ID to avoid foreign key issues\n                    {\n                        action: 'UPDATE_ENTRY',\n                        requestedProjectId: projectId,\n                        actualProjectId: existingEntry.changelog.projectId,\n                        entryId,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create project mismatch audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                {error: 'Entry does not belong to this project'},\n                {status: 400}\n            );\n        }\n\n        // Track changes for audit log\n        const changes: Record<string, { from: unknown; to: unknown }> = {};\n\n        if (title && title !== existingEntry.title) {\n            changes.title = {from: existingEntry.title, to: title};\n        }\n\n        if (content && content !== existingEntry.content) {\n            changes.content = {\n                from: `${existingEntry.content.substring(0, 50)}${existingEntry.content.length > 50 ? '...' : ''}`,\n                to: `${content.substring(0, 50)}${content.length > 50 ? '...' : ''}`\n            };\n        }\n\n        if (version && version !== existingEntry.version) {\n            changes.version = {from: existingEntry.version, to: version};\n        }\n\n        if (tags) {\n            const existingTagIds = existingEntry.tags.map(tag => tag.id).sort();\n            const newTagIds = tags.map((tag: { id: string }) => tag.id).sort();\n\n            if (JSON.stringify(existingTagIds) !== JSON.stringify(newTagIds)) {\n                changes.tags = {\n                    from: existingEntry.tags.map(tag => tag.name),\n                    to: tags.map((tag: { name: string }) => tag.name)\n                };\n            }\n        }\n\n        // Fix the tags connection structure\n        const updatedEntry = await db.changelogEntry.update({\n            where: {\n                id: entryId\n            },\n            data: {\n                title,\n                content,\n                excerpt: content ? generateExcerpt(content) : undefined, // Regenerate excerpt when content changes\n                version,\n                updatedAt: new Date(),\n                tags: {\n                    set: tags.map((tag: { id: string }) => ({\n                        id: tag.id\n                    }))\n                }\n            },\n            include: {\n                tags: true\n            }\n        });\n\n        // Log successful update with changes\n        try {\n            await createAuditLog(\n                'UPDATE_CHANGELOG_ENTRY_SUCCESS',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    projectId,\n                    entryId: updatedEntry.id,\n                    entryTitle: updatedEntry.title,\n                    entryVersion: updatedEntry.version,\n                    changes,\n                    changeCount: Object.keys(changes).length,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create update success audit log:', auditLogError);\n        }\n\n        return NextResponse.json(updatedEntry);\n    } catch (error) {\n        console.error('Error updating changelog entry:', error);\n\n        // Log error\n        try {\n            const {projectId, entryId} = await (async () => context.params)();\n            const user = await validateAuthAndGetUser();\n            await createAuditLog(\n                'CHANGELOG_ENTRY_ERROR',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    action: 'UPDATE_ENTRY',\n                    projectId,\n                    entryId,\n                    error: error instanceof Error ? error.message : 'Unknown error',\n                    stack: error instanceof Error ? error.stack : undefined,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create error audit log:', auditLogError);\n        }\n\n        return NextResponse.json(\n            {error: 'Failed to update changelog entry'},\n            {status: 500}\n        );\n    }\n}\n\n/**\n * Update the status of a changelog entry by ID\n * @method PATCH\n * @description Updates the status (published/unpublished) of a changelog entry by its ID. Requires user authentication and permission to edit the project.\n * @param {string} projectId - The ID of the project the entry belongs to.\n * @param {string} entryId - The ID of the changelog entry to update.\n * @requestBody {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"action\": { \"type\": \"string\", \"enum\": [\"publish\", \"unpublish\"] }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"title\": { \"type\": \"string\" },\n *     \"content\": { \"type\": \"string\" },\n *     \"version\": { \"type\": \"number\" },\n *     \"tags\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"name\": { \"type\": \"string\" },\n *           \"color\": { \"type\": \"string\" }\n *         }\n *       }\n *     },\n *     \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"updatedAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"projectId\": { \"type\": \"string\" },\n *     \"publishedAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *   }\n * }\n * @error 401 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n * @error 403 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n * @error 404 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n * @error 400 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" },\n *     \"details\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"message\": { \"type\": \"string\" },\n *           \"path\": { \"type\": \"string\" }\n *         }\n *       }\n *     }\n *   }\n * }\n */\nexport async function PATCH(\n    request: Request,\n    context: { params: Promise<{ projectId: string; entryId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n        const {action, publishedAt} = await request.json();\n        const {projectId, entryId} = await (async () => context.params)();\n\n        // Log status update attempt\n        try {\n            await createAuditLog(\n                `CHANGELOG_ENTRY_${action.toUpperCase()}_ATTEMPT`,\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    projectId,\n                    entryId,\n                    action,\n                    userRole: user.role,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create status update attempt audit log:', auditLogError);\n        }\n\n        // Verify user has permission\n        if (user.role === Role.VIEWER) {\n            // Log permission denied\n            try {\n                await createAuditLog(\n                    'CHANGELOG_ENTRY_PERMISSION_DENIED',\n                    user.id,\n                    user.id, // Use user's own ID to avoid foreign key issues\n                    {\n                        projectId,\n                        entryId,\n                        action,\n                        userRole: user.role,\n                        requiredRole: 'STAFF or ADMIN',\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create permission denied audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                {error: 'Insufficient permissions'},\n                {status: 403}\n            );\n        }\n\n        // First, verify the entry exists and belongs to the project\n        const existingEntry = await db.changelogEntry.findFirst({\n            where: {\n                id: entryId,\n                changelog: {\n                    projectId: projectId\n                }\n            },\n            include: {\n                changelog: {\n                    select: {\n                        project: {\n                            select: {\n                                requireApproval: true,\n                                allowAutoPublish: true\n                            }\n                        }\n                    }\n                }\n            }\n        });\n\n        if (!existingEntry) {\n            // Log entry not found\n            try {\n                await createAuditLog(\n                    'CHANGELOG_ENTRY_NOT_FOUND',\n                    user.id,\n                    user.id, // Use user's own ID to avoid foreign key issues\n                    {\n                        action: `${action.toUpperCase()}_ENTRY`,\n                        projectId,\n                        entryId,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create not found audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                {error: 'Entry not found or does not belong to this project'},\n                {status: 404}\n            );\n        }\n\n        const project = existingEntry.changelog.project;\n\n        // Handle publish/unpublish actions\n        if (action === 'publish' || action === 'unpublish') {\n            // Allow unpublishing for both ADMIN and STAFF\n            if (action === 'unpublish') {\n                const entry = await db.changelogEntry.update({\n                    where: {id: entryId},\n                    data: {\n                        publishedAt: null\n                    },\n                    include: {tags: true}\n                });\n\n                // Log successful unpublish\n                try {\n                    await createAuditLog(\n                        'CHANGELOG_ENTRY_UNPUBLISHED',\n                        user.id,\n                        user.id, // Use user's own ID to avoid foreign key issues\n                        {\n                            projectId,\n                            entryId: entry.id,\n                            entryTitle: entry.title,\n                            entryVersion: entry.version,\n                            userRole: user.role,\n                            timestamp: new Date().toISOString()\n                        }\n                    );\n                } catch (auditLogError) {\n                    console.error('Failed to create unpublish success audit log:', auditLogError);\n                }\n\n                return NextResponse.json(entry);\n            }\n\n            // Handle publishing\n            if (action === 'publish') {\n                // Admins can always publish directly\n                if (user.role === Role.ADMIN) {\n                    const entry = await db.changelogEntry.update({\n                        where: {id: entryId},\n                        data: {\n                            publishedAt: publishedAt ? new Date(publishedAt) : new Date()\n                        },\n                        include: {tags: true}\n                    });\n\n                    // Log successful admin publish\n                    try {\n                        await createAuditLog(\n                            'CHANGELOG_ENTRY_PUBLISHED',\n                            user.id,\n                            user.id, // Use user's own ID to avoid foreign key issues\n                            {\n                                projectId,\n                                entryId: entry.id,\n                                entryTitle: entry.title,\n                                entryVersion: entry.version,\n                                userRole: user.role,\n                                publishedAt: entry.publishedAt?.toISOString(),\n                                timestamp: new Date().toISOString()\n                            }\n                        );\n                    } catch (auditLogError) {\n                        console.error('Failed to create admin publish success audit log:', auditLogError);\n                    }\n\n                    // Auto-send to Slack if integration is configured and auto-send is enabled\n                    try {\n                        const slackIntegration = await db.slackIntegration.findUnique({\n                            where: {projectId},\n                            select: {\n                                enabled: true,\n                                autoSend: true,\n                                channelId: true,\n                            }\n                        })\n\n                        if (slackIntegration?.enabled && slackIntegration?.autoSend && slackIntegration?.channelId) {\n                            const entryUrl = `${new URL(request.url).origin.replace(/\\/api.*/, '')}/dashboard/projects/${projectId}/changelog/${entry.id}`\n\n                            await postToSlack({\n                                projectId,\n                                entryId: entry.id,\n                                channelId: slackIntegration.channelId,\n                                title: entry.title,\n                                description: entry.excerpt || entry.content.substring(0, 200),\n                                url: entryUrl,\n                                color: '#0099ff'\n                            })\n                        }\n                    } catch (slackError) {\n                        console.error('Failed to auto-send to Slack:', slackError)\n                        // Don't fail the request if Slack posting fails\n                    }\n\n                    return NextResponse.json(entry);\n                }\n\n                // Staff can publish directly if the project doesn't require approval OR allowAutoPublish is true\n                if (user.role === Role.STAFF && (!project.requireApproval || project.allowAutoPublish)) {\n                    const entry = await db.changelogEntry.update({\n                        where: {id: entryId},\n                        data: {\n                            publishedAt: publishedAt ? new Date(publishedAt) : new Date()\n                        },\n                        include: {tags: true}\n                    });\n\n                    // Log successful staff direct publish\n                    try {\n                        await createAuditLog(\n                            'CHANGELOG_ENTRY_PUBLISHED',\n                            user.id,\n                            user.id, // Use user's own ID to avoid foreign key issues\n                            {\n                                projectId,\n                                entryId: entry.id,\n                                entryTitle: entry.title,\n                                entryVersion: entry.version,\n                                userRole: user.role,\n                                publishedAt: entry.publishedAt?.toISOString(),\n                                projectRequiresApproval: project.requireApproval,\n                                projectAllowsAutoPublish: project.allowAutoPublish,\n                                timestamp: new Date().toISOString()\n                            }\n                        );\n                    } catch (auditLogError) {\n                        console.error('Failed to create staff publish success audit log:', auditLogError);\n                    }\n\n                    // Auto-send to Slack if integration is configured and auto-send is enabled\n                    try {\n                        const slackIntegration = await db.slackIntegration.findUnique({\n                            where: {projectId},\n                            select: {\n                                enabled: true,\n                                autoSend: true,\n                                channelId: true,\n                            }\n                        })\n\n                        if (slackIntegration?.enabled && slackIntegration?.autoSend && slackIntegration?.channelId) {\n                            const entryUrl = `${new URL(request.url).origin.replace(/\\/api.*/, '')}/dashboard/projects/${projectId}/changelog/${entry.id}`\n\n                            await postToSlack({\n                                projectId,\n                                entryId: entry.id,\n                                channelId: slackIntegration.channelId,\n                                title: entry.title,\n                                description: entry.excerpt || entry.content.substring(0, 200),\n                                url: entryUrl,\n                                color: '#0099ff'\n                            })\n                        }\n                    } catch (slackError) {\n                        console.error('Failed to auto-send to Slack:', slackError)\n                        // Don't fail the request if Slack posting fails\n                    }\n\n                    return NextResponse.json(entry);\n                }\n\n                // Staff needs approval if required\n                if (project.requireApproval && user.role === Role.STAFF) {\n                    // Check for existing pending request\n                    const existingRequest = await db.changelogRequest.findFirst({\n                        where: {\n                            type: 'ALLOW_PUBLISH',\n                            changelogEntryId: entryId,\n                            status: 'PENDING'\n                        }\n                    });\n\n                    if (existingRequest) {\n                        // Log duplicate request attempt\n                        try {\n                            await createAuditLog(\n                                'CHANGELOG_ENTRY_DUPLICATE_REQUEST',\n                                user.id,\n                                user.id, // Use user's own ID to avoid foreign key issues\n                                {\n                                    projectId,\n                                    entryId,\n                                    requestType: 'ALLOW_PUBLISH',\n                                    existingRequestId: existingRequest.id,\n                                    timestamp: new Date().toISOString()\n                                }\n                            );\n                        } catch (auditLogError) {\n                            console.error('Failed to create duplicate request audit log:', auditLogError);\n                        }\n\n                        return NextResponse.json(\n                            {error: 'A publish request for this entry is already pending'},\n                            {status: 400}\n                        );\n                    }\n\n                    // Create publish request\n                    const publishRequestData: {\n                        type: string;\n                        staffId: string;\n                        projectId: string;\n                        changelogEntryId: string;\n                        status: string;\n                        metadata?: {customPublishedAt: string};\n                    } = {\n                        type: 'ALLOW_PUBLISH',\n                        staffId: user.id,\n                        projectId,\n                        changelogEntryId: entryId,\n                        status: 'PENDING'\n                    };\n\n                    // Store custom publishedAt in metadata if provided\n                    if (publishedAt) {\n                        publishRequestData.metadata = {\n                            customPublishedAt: publishedAt\n                        };\n                    }\n\n                    const publishRequest = await db.changelogRequest.create({\n                        data: publishRequestData\n                    });\n\n                    // Log successful publish request creation\n                    try {\n                        await createAuditLog(\n                            'CHANGELOG_ENTRY_PUBLISH_REQUEST_CREATED',\n                            user.id,\n                            user.id, // Use user's own ID to avoid foreign key issues\n                            {\n                                projectId,\n                                entryId,\n                                requestId: publishRequest.id,\n                                timestamp: new Date().toISOString()\n                            }\n                        );\n                    } catch (auditLogError) {\n                        console.error('Failed to create publish request audit log:', auditLogError);\n                    }\n\n                    return NextResponse.json({\n                        message: 'Publish request created, awaiting admin approval',\n                        requiresApproval: true,\n                        request: publishRequest\n                    }, {status: 202});\n                }\n            }\n        }\n\n        // Log invalid action\n        try {\n            await createAuditLog(\n                'CHANGELOG_ENTRY_INVALID_ACTION',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    projectId,\n                    entryId,\n                    action,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create invalid action audit log:', auditLogError);\n        }\n\n        return NextResponse.json(\n            {error: 'Invalid action'},\n            {status: 400}\n        );\n    } catch (error) {\n        console.error('Error updating changelog entry status:', error);\n\n        // Log error\n        try {\n            const {projectId, entryId} = await (async () => context.params)();\n            const user = await validateAuthAndGetUser();\n            await createAuditLog(\n                'CHANGELOG_ENTRY_ERROR',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    action: 'UPDATE_ENTRY_STATUS',\n                    projectId,\n                    entryId,\n                    error: error instanceof Error ? error.message : 'Unknown error',\n                    stack: error instanceof Error ? error.stack : undefined,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create error audit log:', auditLogError);\n        }\n\n        return NextResponse.json(\n            {error: 'Failed to update changelog entry status'},\n            {status: 500}\n        );\n    }\n}\n\n/**\n * Delete a changelog entry or create a deletion request\n * @method DELETE\n * @description Deletes a changelog entry if the user is an admin, or creates a deletion request if the user is staff. Requires user authentication and appropriate permissions.\n * @param {string} projectId - The ID of the project the entry belongs to.\n * @param {string} entryId - The ID of the changelog entry to delete.\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"title\": { \"type\": \"string\" },\n *     \"content\": { \"type\": \"string\" },\n *     \"version\": { \"type\": \"number\" },\n *     \"projectId\": { \"type\": \"string\" },\n *     \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"updatedAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *   }\n * }\n * @response 202 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"message\": { \"type\": \"string\" },\n *     \"request\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"type\": { \"type\": \"string\", \"enum\": [\"DELETE_ENTRY\"] },\n *         \"status\": { \"type\": \"string\", \"enum\": [\"PENDING\"] },\n *         \"staffId\": { \"type\": \"string\" },\n *         \"projectId\": { \"type\": \"string\" },\n *         \"changelogEntryId\": { \"type\": \"string\" },\n *         \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *       }\n *     }\n *   }\n * }\n * @error 400 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n * @error 401 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n * @error 403 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n * @error 500 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n */\nexport async function DELETE(\n    request: Request,\n    context: { params: Promise<{ projectId: string; entryId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n        const {projectId, entryId} = await (async () => context.params)();\n\n        // Log deletion attempt\n        try {\n            await createAuditLog(\n                'DELETE_CHANGELOG_ENTRY_ATTEMPT',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    projectId,\n                    entryId,\n                    userRole: user.role,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create deletion attempt audit log:', auditLogError);\n        }\n\n        // Verify the entry exists and belongs to the project\n        const existingEntry = await db.changelogEntry.findFirst({\n            where: {\n                id: entryId,\n                changelog: {\n                    projectId\n                }\n            },\n            include: {\n                tags: true\n            }\n        });\n\n        if (!existingEntry) {\n            // Log entry not found\n            try {\n                await createAuditLog(\n                    'CHANGELOG_ENTRY_NOT_FOUND',\n                    user.id,\n                    user.id, // Use user's own ID to avoid foreign key issues\n                    {\n                        action: 'DELETE_ENTRY',\n                        projectId,\n                        entryId,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create not found audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                {error: 'Entry not found or does not belong to this project'},\n                {status: 404}\n            );\n        }\n\n        // Admin can delete directly\n        if (user.role === Role.ADMIN) {\n            // Save entry details for audit log before deleting\n            const entryDetails = {\n                id: existingEntry.id,\n                title: existingEntry.title,\n                content: existingEntry.content?.substring(0, 100) + (existingEntry.content?.length > 100 ? '...' : ''),\n                version: existingEntry.version,\n                tags: existingEntry.tags.map(tag => tag.name),\n                createdAt: existingEntry.createdAt,\n                updatedAt: existingEntry.updatedAt,\n                publishedAt: existingEntry.publishedAt\n            };\n\n            // Perform the deletion\n            const entry = await db.changelogEntry.delete({\n                where: {id: entryId}\n            });\n\n            // Log successful deletion by admin\n            try {\n                await createAuditLog(\n                    'DELETE_CHANGELOG_ENTRY_SUCCESS',\n                    user.id,\n                    user.id, // Use user's own ID to avoid foreign key issues\n                    {\n                        projectId,\n                        entryId: entryDetails.id,\n                        entryTitle: entryDetails.title,\n                        entryVersion: entryDetails.version,\n                        entryTags: entryDetails.tags,\n                        wasPublished: !!entryDetails.publishedAt,\n                        userRole: user.role,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create admin deletion success audit log:', auditLogError);\n            }\n\n            return NextResponse.json(entry);\n        }\n\n        // Staff must create a deletion request\n        if (user.role === Role.STAFF) {\n            // Check if there's already a pending request\n            const existingRequest = await db.changelogRequest.findFirst({\n                where: {\n                    changelogEntryId: entryId,\n                    status: 'PENDING'\n                }\n            });\n\n            if (existingRequest) {\n                // Log duplicate request attempt\n                try {\n                    await createAuditLog(\n                        'CHANGELOG_ENTRY_DUPLICATE_REQUEST',\n                        user.id,\n                        user.id, // Use user's own ID to avoid foreign key issues\n                        {\n                            projectId,\n                            entryId,\n                            requestType: 'DELETE_ENTRY',\n                            existingRequestId: existingRequest.id,\n                            timestamp: new Date().toISOString()\n                        }\n                    );\n                } catch (auditLogError) {\n                    console.error('Failed to create duplicate request audit log:', auditLogError);\n                }\n\n                return NextResponse.json(\n                    {error: 'A deletion request for this entry is already pending'},\n                    {status: 400}\n                );\n            }\n\n            // Create deletion request\n            const deleteRequest = await db.changelogRequest.create({\n                data: {\n                    type: 'DELETE_ENTRY',\n                    staffId: user.id,\n                    projectId,\n                    changelogEntryId: entryId,\n                    status: 'PENDING'\n                }\n            });\n\n            // Log successful deletion request creation\n            try {\n                await createAuditLog(\n                    'CHANGELOG_ENTRY_DELETION_REQUEST_CREATED',\n                    user.id,\n                    user.id, // Use user's own ID to avoid foreign key issues\n                    {\n                        projectId,\n                        entryId,\n                        entryTitle: existingEntry.title,\n                        entryVersion: existingEntry.version,\n                        requestId: deleteRequest.id,\n                        userRole: user.role,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create deletion request audit log:', auditLogError);\n            }\n\n            return NextResponse.json({\n                message: 'Deletion request created, awaiting admin approval',\n                request: deleteRequest\n            }, {status: 202});\n        }\n\n        // Handle viewers and other roles\n        // Log permission denied\n        try {\n            await createAuditLog(\n                'CHANGELOG_ENTRY_PERMISSION_DENIED',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    projectId,\n                    entryId,\n                    action: 'DELETE_ENTRY',\n                    userRole: user.role,\n                    requiredRole: 'STAFF or ADMIN',\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create permission denied audit log:', auditLogError);\n        }\n\n        return NextResponse.json(\n            {error: 'Insufficient permissions'},\n            {status: 403}\n        );\n    } catch (error) {\n        console.error('Error handling changelog entry deletion:', error);\n\n        // Log error\n        try {\n            const {projectId, entryId} = await (async () => context.params)();\n            const user = await validateAuthAndGetUser();\n            await createAuditLog(\n                'CHANGELOG_ENTRY_ERROR',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    action: 'DELETE_ENTRY',\n                    projectId,\n                    entryId,\n                    error: error instanceof Error ? error.message : 'Unknown error',\n                    stack: error instanceof Error ? error.stack : undefined,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create error audit log:', auditLogError);\n        }\n\n        return NextResponse.json(\n            {error: 'Failed to process deletion request'},\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/changelog/[entryId]/schedule/approval/route.ts",
    "content": "// app/api/projects/[projectId]/changelog/[entryId]/schedule/approval/route.ts\nimport {NextResponse} from \"next/server\";\nimport {z} from \"zod\";\nimport {validateAuthAndGetUser} from \"@/lib/utils/changelog\";\nimport {db} from \"@/lib/db\";\nimport {createAuditLog} from \"@/lib/utils/auditLog\";\nimport {Role, ScheduledJobType} from \"@prisma/client\";\nimport {User} from \"@/lib/types/auth\";\n\nconst scheduleApprovalSchema = z.object({\n    scheduledAt: z.string().datetime(),\n    action: z.enum([\"request_approval\", \"approve\", \"reject\"]),\n    reason: z.string().optional(),\n});\n\ninterface ScheduleApprovalBody {\n    scheduledAt: string;\n    action: \"request_approval\" | \"approve\" | \"reject\";\n    reason?: string;\n}\n\n/**\n * Handle schedule approval requests for changelog entries\n * @method POST\n * @description Creates, approves, or rejects a scheduled publish request for a changelog entry when approval is required\n * @body {\n *   \"type\": \"object\",\n *   \"required\": [\"action\"],\n *   \"properties\": {\n *     \"action\": {\n *       \"type\": \"string\",\n *       \"enum\": [\"request_approval\", \"approve\", \"reject\"],\n *       \"description\": \"Action to perform on the schedule request\"\n *     },\n *     \"scheduledAt\": {\n *       \"type\": \"string\",\n *       \"format\": \"date-time\",\n *       \"description\": \"ISO datetime string for when to publish (required for request_approval)\"\n *     },\n *     \"reason\": {\n *       \"type\": \"string\",\n *       \"description\": \"Reason for rejection (optional, used with reject action)\"\n *     }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\" },\n *     \"message\": { \"type\": \"string\" },\n *     \"request\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"type\": { \"type\": \"string\" },\n *         \"status\": { \"type\": \"string\" },\n *         \"scheduledAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *       }\n *     }\n *   }\n * }\n * @error 400 Bad Request - Invalid input or business logic violation\n * @error 401 Unauthorized - User not authenticated\n * @error 403 Forbidden - User lacks permission for the action\n * @error 404 Not Found - Changelog entry not found\n * @error 409 Conflict - Request already exists or invalid state\n */\nexport async function POST(\n    request: Request,\n    context: { params: Promise<{ projectId: string; entryId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n        const {projectId, entryId} = await context.params;\n        const body = await request.json() as ScheduleApprovalBody;\n\n        const validatedData = scheduleApprovalSchema.parse(body);\n        const {action, scheduledAt, reason} = validatedData;\n\n        // Get the changelog entry and project settings\n        const entry = await db.changelogEntry.findFirst({\n            where: {\n                id: entryId,\n                changelog: {\n                    project: {\n                        id: projectId,\n                    },\n                },\n            },\n            include: {\n                changelog: {\n                    include: {\n                        project: true,\n                    },\n                },\n            },\n        });\n\n        if (!entry) {\n            return NextResponse.json(\n                {error: 'Entry not found or does not belong to this project'},\n                {status: 404}\n            );\n        }\n\n        const project = entry.changelog.project;\n\n        if (action === \"request_approval\") {\n            // Staff can request approval to schedule\n            if (user.role !== Role.STAFF) {\n                return NextResponse.json(\n                    {error: 'Only staff members can request schedule approval'},\n                    {status: 403}\n                );\n            }\n\n            if (!project.requireApproval) {\n                return NextResponse.json(\n                    {error: 'This project does not require approval for scheduling'},\n                    {status: 400}\n                );\n            }\n\n            if (entry.publishedAt) {\n                return NextResponse.json(\n                    {error: 'Cannot schedule an already published entry'},\n                    {status: 409}\n                );\n            }\n\n            const scheduleDate = new Date(scheduledAt);\n            if (scheduleDate <= new Date()) {\n                return NextResponse.json(\n                    {error: 'Scheduled time must be in the future'},\n                    {status: 400}\n                );\n            }\n\n            // Check for existing schedule requests\n            const existingRequest = await db.changelogRequest.findFirst({\n                where: {\n                    type: 'ALLOW_SCHEDULE',\n                    changelogEntryId: entryId,\n                    status: 'PENDING'\n                }\n            });\n\n            if (existingRequest) {\n                return NextResponse.json(\n                    {error: 'A schedule approval request for this entry already exists'},\n                    {status: 409}\n                );\n            }\n\n            // Create the schedule approval request\n            const scheduleRequest = await db.changelogRequest.create({\n                data: {\n                    type: 'ALLOW_SCHEDULE',\n                    staffId: user.id,\n                    projectId,\n                    changelogEntryId: entryId,\n                    status: 'PENDING',\n                    targetId: scheduledAt, // Store the requested schedule time\n                },\n                include: {\n                    staff: {\n                        select: {\n                            id: true,\n                            email: true,\n                            name: true,\n                        },\n                    },\n                },\n            });\n\n            await createAuditLog(\n                'CHANGELOG_SCHEDULE_APPROVAL_REQUESTED',\n                user.id,\n                user.id,\n                {\n                    projectId,\n                    entryId,\n                    requestId: scheduleRequest.id,\n                    requestedScheduleTime: scheduledAt,\n                    timestamp: new Date().toISOString(),\n                }\n            );\n\n            return NextResponse.json({\n                success: true,\n                message: 'Schedule approval request created',\n                request: {\n                    id: scheduleRequest.id,\n                    type: scheduleRequest.type,\n                    status: scheduleRequest.status,\n                    scheduledAt: scheduledAt,\n                },\n            });\n        }\n\n        if (action === \"approve\" || action === \"reject\") {\n            // Only admins can approve/reject\n            if (user.role !== Role.ADMIN) {\n                return NextResponse.json(\n                    {error: 'Only administrators can approve or reject schedule requests'},\n                    {status: 403}\n                );\n            }\n\n            // Find the pending request\n            const pendingRequest = await db.changelogRequest.findFirst({\n                where: {\n                    type: 'ALLOW_SCHEDULE',\n                    changelogEntryId: entryId,\n                    status: 'PENDING'\n                },\n                include: {\n                    staff: {\n                        select: {\n                            id: true,\n                            email: true,\n                            name: true,\n                        },\n                    },\n                },\n            });\n\n            if (!pendingRequest) {\n                return NextResponse.json(\n                    {error: 'No pending schedule request found for this entry'},\n                    {status: 404}\n                );\n            }\n\n            const requestedScheduleTime = pendingRequest.targetId; // The stored schedule time\n\n            if (action === \"approve\") {\n                // Approve and schedule the entry\n                const scheduleDate = new Date(requestedScheduleTime!);\n\n                // Update the request status\n                await db.changelogRequest.update({\n                    where: {id: pendingRequest.id},\n                    data: {\n                        status: 'APPROVED',\n                        adminId: user.id,\n                        reviewedAt: new Date(),\n                    },\n                });\n\n                // Schedule the entry\n                // eslint-disable-next-line @typescript-eslint/no-unused-vars\n                const updatedEntry = await db.changelogEntry.update({\n                    where: {id: entryId},\n                    data: {scheduledAt: scheduleDate},\n                });\n\n                // Create scheduled job\n                const {\n                    ScheduledJobService,\n                } = await import('@/lib/services/jobs/scheduled-job.service');\n                const jobId = await ScheduledJobService.createJob({\n                    type: ScheduledJobType.PUBLISH_CHANGELOG_ENTRY,\n                    entityId: entryId,\n                    scheduledAt: scheduleDate,\n                });\n\n                await createAuditLog(\n                    'CHANGELOG_SCHEDULE_APPROVED',\n                    user.id,\n                    user.id,\n                    {\n                        projectId,\n                        entryId,\n                        requestId: pendingRequest.id,\n                        staffUserId: pendingRequest.staff?.id,\n                        scheduledAt: scheduleDate.toISOString(),\n                        jobId,\n                        timestamp: new Date().toISOString(),\n                    }\n                );\n\n                // Send notification to staff member\n                try {\n                    const {sendNotificationEmail} = await import('@/lib/services/email/notification');\n                    await sendNotificationEmail({\n                        userId: pendingRequest.staffId!,\n                        status: 'APPROVED',\n                        request: {\n                            type: 'Schedule Request',\n                            projectName: project.name,\n                            entryTitle: entry.title,\n                            adminName: user.name || user.email,\n                        },\n                        dashboardUrl: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard/projects/${projectId}`,\n                    });\n                } catch (emailError) {\n                    console.error('Failed to send approval notification:', emailError);\n                }\n\n                return NextResponse.json({\n                    success: true,\n                    message: 'Schedule request approved and entry scheduled',\n                    request: {\n                        id: pendingRequest.id,\n                        type: pendingRequest.type,\n                        status: 'APPROVED',\n                        scheduledAt: scheduleDate.toISOString(),\n                    },\n                });\n            } else {\n                // Reject the request\n                await db.changelogRequest.update({\n                    where: {id: pendingRequest.id},\n                    data: {\n                        status: 'REJECTED',\n                        adminId: user.id,\n                        reviewedAt: new Date(),\n                    },\n                });\n\n                await createAuditLog(\n                    'CHANGELOG_SCHEDULE_REJECTED',\n                    user.id,\n                    user.id,\n                    {\n                        projectId,\n                        entryId,\n                        requestId: pendingRequest.id,\n                        staffUserId: pendingRequest.staff?.id,\n                        reason: reason || 'No reason provided',\n                        timestamp: new Date().toISOString(),\n                    }\n                );\n\n                // Send notification to staff member\n                try {\n                    const {sendNotificationEmail} = await import('@/lib/services/email/notification');\n                    await sendNotificationEmail({\n                        userId: pendingRequest.staffId!,\n                        status: 'REJECTED',\n                        request: {\n                            type: 'Schedule Request',\n                            projectName: project.name,\n                            entryTitle: entry.title,\n                            adminName: user.name || user.email,\n                        },\n                        dashboardUrl: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard/projects/${projectId}`,\n                    });\n                } catch (emailError) {\n                    console.error('Failed to send rejection notification:', emailError);\n                }\n\n                return NextResponse.json({\n                    success: true,\n                    message: 'Schedule request rejected',\n                    request: {\n                        id: pendingRequest.id,\n                        type: pendingRequest.type,\n                        status: 'REJECTED',\n                        scheduledAt: null,\n                    },\n                });\n            }\n        }\n\n        return NextResponse.json(\n            {error: 'Invalid action'},\n            {status: 400}\n        );\n    } catch (error) {\n        console.error('Error handling schedule approval:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {\n                    error: 'Validation failed',\n                    details: error.errors.map((err) => ({\n                        message: err.message,\n                        path: err.path.join('.'),\n                    })),\n                },\n                {status: 400}\n            );\n        }\n\n        return NextResponse.json(\n            {error: 'Internal server error'},\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/changelog/[entryId]/schedule/route.ts",
    "content": "import {NextResponse} from \"next/server\";\nimport {z} from \"zod\";\nimport {validateAuthAndGetUser} from \"@/lib/utils/changelog\";\nimport {db} from \"@/lib/db\";\nimport {ScheduledJobService, ScheduledJobType} from \"@/lib/services/jobs/scheduled-job.service\";\nimport {createAuditLog} from \"@/lib/utils/auditLog\";\nimport {Role} from \"@prisma/client\";\n\nconst scheduleSchema = z.object({\n    scheduledAt: z.string().datetime().optional(),\n    action: z.enum([\"schedule\", \"unschedule\"]),\n});\n\ninterface ScheduleRequestBody {\n    scheduledAt?: string;\n    action: \"schedule\" | \"unschedule\";\n}\n\n/**\n * Schedule or unschedule a changelog entry for automatic publishing\n * @method POST\n * @description Schedules a changelog entry to be automatically published at a specified time. Only admins and staff with appropriate permissions can schedule entries.\n * @body {\n *   \"type\": \"object\",\n *   \"required\": [\"action\"],\n *   \"properties\": {\n *     \"action\": {\n *       \"type\": \"string\",\n *       \"enum\": [\"schedule\", \"unschedule\"],\n *       \"description\": \"Action to perform - schedule for future publishing or unschedule to cancel\"\n *     },\n *     \"scheduledAt\": {\n *       \"type\": \"string\",\n *       \"format\": \"date-time\",\n *       \"description\": \"ISO datetime string for when to publish (required when action is 'schedule')\"\n *     }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\" },\n *     \"message\": { \"type\": \"string\" },\n *     \"entry\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"title\": { \"type\": \"string\" },\n *         \"scheduledAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *         \"publishedAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *       }\n *     },\n *     \"jobId\": { \"type\": \"string\", \"description\": \"ID of the scheduled job (when scheduling)\" }\n *   }\n * }\n * @error 400 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": { \"type\": \"string\" },\n *     \"details\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"message\": { \"type\": \"string\" },\n *           \"path\": { \"type\": \"string\" }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 401 Unauthorized - User not authenticated\n * @error 403 Forbidden - User lacks permission to schedule entries\n * @error 404 Not Found - Changelog entry not found\n * @error 409 Conflict - Entry already published or scheduling conflict\n */\nexport async function POST(\n    request: Request,\n    context: { params: Promise<{ projectId: string; entryId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n        const {projectId, entryId} = await context.params;\n        const body = await request.json() as ScheduleRequestBody;\n\n        // Validate request body with custom validation for schedule action\n        const validatedData = scheduleSchema.parse(body);\n        const {action, scheduledAt} = validatedData;\n\n        // Additional validation: scheduledAt is required for schedule action\n        if (action === \"schedule\" && !scheduledAt) {\n            return NextResponse.json(\n                {\n                    error: 'Validation failed',\n                    details: [\n                        {\n                            message: 'scheduledAt is required when action is \"schedule\"',\n                            path: 'scheduledAt',\n                        }\n                    ],\n                },\n                {status: 400}\n            );\n        }\n\n        // Verify entry exists and user has permission\n        const entry = await db.changelogEntry.findFirst({\n            where: {\n                id: entryId,\n                changelog: {\n                    project: {\n                        id: projectId,\n                    },\n                },\n            },\n            include: {\n                changelog: {\n                    include: {\n                        project: true,\n                    },\n                },\n                scheduledJobs: {\n                    where: {\n                        status: \"PENDING\",\n                        type: ScheduledJobType.PUBLISH_CHANGELOG_ENTRY,\n                    },\n                },\n            },\n        });\n\n        if (!entry) {\n            await createAuditLog(\n                'CHANGELOG_SCHEDULE_ENTRY_NOT_FOUND',\n                user.id,\n                user.id,\n                {\n                    projectId,\n                    entryId,\n                    action,\n                    timestamp: new Date().toISOString(),\n                }\n            );\n\n            return NextResponse.json(\n                {error: 'Entry not found or does not belong to this project'},\n                {status: 404}\n            );\n        }\n\n        const project = entry.changelog.project;\n\n        // Check permissions - same logic as publishing\n        const canSchedule =\n            user.role === Role.ADMIN ||\n            (user.role === Role.STAFF && (!project.requireApproval || project.allowAutoPublish));\n\n        if (!canSchedule) {\n            await createAuditLog(\n                'CHANGELOG_SCHEDULE_PERMISSION_DENIED',\n                user.id,\n                user.id,\n                {\n                    projectId,\n                    entryId,\n                    action,\n                    userRole: user.role,\n                    projectRequiresApproval: project.requireApproval,\n                    projectAllowsAutoPublish: project.allowAutoPublish,\n                    timestamp: new Date().toISOString(),\n                }\n            );\n\n            return NextResponse.json(\n                {error: 'Not authorized to schedule entries for this project'},\n                {status: 403}\n            );\n        }\n\n        if (action === \"schedule\") {\n            // Validate scheduling constraints\n            if (entry.publishedAt) {\n                return NextResponse.json(\n                    {error: 'Cannot schedule an already published entry'},\n                    {status: 409}\n                );\n            }\n\n            const scheduleDate = new Date(scheduledAt!);\n            const now = new Date();\n\n            if (scheduleDate <= now) {\n                return NextResponse.json(\n                    {error: 'Scheduled time must be in the future'},\n                    {status: 400}\n                );\n            }\n\n            // For updating existing schedules, cancel old jobs first\n            if (entry.scheduledJobs.length > 0) {\n                for (const job of entry.scheduledJobs) {\n                    await ScheduledJobService.cancelJob(job.id, user.id);\n                }\n            }\n\n            // Update entry with new scheduled time\n            const updatedEntry = await db.changelogEntry.update({\n                where: {id: entryId},\n                data: {scheduledAt: scheduleDate},\n            });\n\n            // Create new scheduled job\n            const jobId = await ScheduledJobService.createJob({\n                type: ScheduledJobType.PUBLISH_CHANGELOG_ENTRY,\n                entityId: entryId,\n                scheduledAt: scheduleDate,\n            });\n\n            await createAuditLog(\n                entry.scheduledAt ? 'CHANGELOG_ENTRY_RESCHEDULED' : 'CHANGELOG_ENTRY_SCHEDULED',\n                user.id,\n                user.id,\n                {\n                    projectId,\n                    entryId,\n                    entryTitle: entry.title,\n                    previousScheduledAt: entry.scheduledAt?.toISOString(),\n                    newScheduledAt: scheduleDate.toISOString(),\n                    jobId,\n                    userRole: user.role,\n                    timestamp: new Date().toISOString(),\n                }\n            );\n\n            return NextResponse.json({\n                success: true,\n                message: entry.scheduledAt ? 'Entry rescheduled successfully' : 'Entry scheduled for publishing',\n                entry: {\n                    id: updatedEntry.id,\n                    title: updatedEntry.title,\n                    scheduledAt: updatedEntry.scheduledAt?.toISOString(),\n                    publishedAt: updatedEntry.publishedAt?.toISOString(),\n                },\n                jobId,\n            });\n        } else if (action === \"unschedule\") {\n            if (!entry.scheduledAt) {\n                return NextResponse.json(\n                    {error: 'Entry is not scheduled'},\n                    {status: 400}\n                );\n            }\n\n            // Cancel any pending scheduled jobs\n            for (const job of entry.scheduledJobs) {\n                await ScheduledJobService.cancelJob(job.id, user.id);\n            }\n\n            // Update entry to remove scheduled time\n            const updatedEntry = await db.changelogEntry.update({\n                where: {id: entryId},\n                data: {scheduledAt: null},\n            });\n\n            await createAuditLog(\n                'CHANGELOG_ENTRY_UNSCHEDULED',\n                user.id,\n                user.id,\n                {\n                    projectId,\n                    entryId,\n                    entryTitle: entry.title,\n                    previousScheduledAt: entry.scheduledAt?.toISOString(),\n                    userRole: user.role,\n                    timestamp: new Date().toISOString(),\n                }\n            );\n\n            return NextResponse.json({\n                success: true,\n                message: 'Entry unscheduled',\n                entry: {\n                    id: updatedEntry.id,\n                    title: updatedEntry.title,\n                    scheduledAt: null,\n                    publishedAt: updatedEntry.publishedAt?.toISOString(),\n                },\n            });\n        }\n\n        return NextResponse.json(\n            {error: 'Invalid action'},\n            {status: 400}\n        );\n    } catch (error) {\n        console.error('Error scheduling changelog entry:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {\n                    error: 'Validation failed',\n                    details: error.errors.map((err) => ({\n                        message: err.message,\n                        path: err.path.join('.'),\n                    })),\n                },\n                {status: 400}\n            );\n        }\n\n        return NextResponse.json(\n            {error: 'Internal server error'},\n            {status: 500}\n        );\n    }\n}\n\n/**\n * Get scheduled jobs for a changelog entry\n * @method GET\n * @description Retrieves information about scheduled jobs for a specific changelog entry\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"entry\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"title\": { \"type\": \"string\" },\n *         \"scheduledAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *         \"publishedAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *       }\n *     },\n *     \"jobs\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"type\": { \"type\": \"string\" },\n *           \"scheduledAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *           \"status\": { \"type\": \"string\" },\n *           \"errorMessage\": { \"type\": \"string\" }\n *         }\n *       }\n *     }\n *   }\n * }\n */\nexport async function GET(\n    request: Request,\n    context: { params: Promise<{ projectId: string; entryId: string }> }\n) {\n    try {\n        await validateAuthAndGetUser();\n        const {projectId, entryId} = await context.params;\n\n        const entry = await db.changelogEntry.findFirst({\n            where: {\n                id: entryId,\n                changelog: {\n                    project: {\n                        id: projectId,\n                    },\n                },\n            },\n            select: {\n                id: true,\n                title: true,\n                scheduledAt: true,\n                publishedAt: true,\n            },\n        });\n\n        if (!entry) {\n            return NextResponse.json(\n                {error: 'Entry not found'},\n                {status: 404}\n            );\n        }\n\n        const jobs = await ScheduledJobService.getJobsForEntity(entryId);\n\n        return NextResponse.json({\n            entry: {\n                id: entry.id,\n                title: entry.title,\n                scheduledAt: entry.scheduledAt?.toISOString(),\n                publishedAt: entry.publishedAt?.toISOString(),\n            },\n            jobs: jobs.map(job => ({\n                id: job.id,\n                type: job.type,\n                scheduledAt: job.scheduledAt.toISOString(),\n                status: job.status,\n                errorMessage: job.errorMessage,\n            })),\n        });\n    } catch (error) {\n        console.error('Error getting scheduled jobs:', error);\n        return NextResponse.json(\n            {error: 'Internal server error'},\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/changelog/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport { validateAuthAndGetUser, generateExcerpt } from '@/lib/utils/changelog'\nimport { createAuditLog } from '@/lib/utils/auditLog'\nimport { db } from '@/lib/db'\nimport { postToSlack } from '@/lib/services/slack'\nimport { SponsorService } from '@/lib/services/sponsor/service'\n\n/**\n * @method GET\n * @description Fetches the changelog entries for a given project\n * @query {\n *   projectId: String, required\n *   search?: String, optional\n *   tag?: String, optional\n *   startDate?: String, optional\n *   endDate?: String, optional\n *   page?: Number, optional\n *   limit?: Number, optional\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"entries\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"title\": { \"type\": \"string\" },\n *           \"content\": { \"type\": \"string\" },\n *           \"version\": { \"type\": \"number\" },\n *           \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *           \"tags\": {\n *             \"type\": \"array\",\n *             \"items\": {\n *               \"type\": \"object\",\n *               \"properties\": {\n *                 \"id\": { \"type\": \"string\" },\n *                 \"name\": { \"type\": \"string\" }\n *               }\n *             }\n *           }\n *         }\n *       }\n *     },\n *     \"pagination\": {\n *       \"page\": { \"type\": \"number\" },\n *       \"limit\": { \"type\": \"number\" },\n *       \"total\": { \"type\": \"number\" },\n *       \"totalPages\": { \"type\": \"number\" }\n *     }\n *   }\n * }\n * @error 403 Unauthorized - User does not have 'ADMIN' role\n * @error 404 Project not found\n * @error 500 An unexpected error occurred while fetching the changelog entries\n */\nexport async function GET(\n    request: Request,\n    { params }: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        const { projectId } = await params\n        const user = await validateAuthAndGetUser()\n        const { searchParams } = new URL(request.url)\n\n        // Get query parameters\n        const search = searchParams.get('search')\n        const tag = searchParams.get('tag')\n        const startDate = searchParams.get('startDate')\n        const endDate = searchParams.get('endDate')\n        const page = parseInt(searchParams.get('page') || '1')\n        const limit = parseInt(searchParams.get('limit') || '50')\n        const skip = (page - 1) * limit\n\n        // Log the changelog view attempt\n        try {\n            await createAuditLog(\n                'VIEW_CHANGELOG_ATTEMPT',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    projectId,\n                    filters: {\n                        search: search || null,\n                        tag: tag || null,\n                        startDate: startDate || null,\n                        endDate: endDate || null,\n                        page,\n                        limit\n                    },\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create view attempt audit log:', auditLogError);\n        }\n\n        // First get the changelog for this project\n        const changelog = await db.changelog.findUnique({\n            where: { projectId }\n        })\n\n        if (!changelog) {\n            // Log changelog not found error\n            try {\n                await createAuditLog(\n                    'CHANGELOG_NOT_FOUND',\n                    user.id,\n                    user.id, // Use user's own ID to avoid foreign key issues\n                    {\n                        projectId,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create not found audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                { error: 'Changelog not found' },\n                { status: 404 }\n            )\n        }\n\n        // Build where clause for filtering\n        const where = {\n            changelogId: changelog.id,\n            ...(search && {\n                OR: [\n                    { title: { contains: search, mode: 'insensitive' as const } },\n                    { content: { contains: search, mode: 'insensitive' as const } },\n                ],\n            }),\n            ...(tag && {\n                tags: {\n                    some: {\n                        name: tag\n                    }\n                }\n            }),\n            ...(startDate && endDate && {\n                createdAt: {\n                    gte: new Date(startDate),\n                    lte: new Date(endDate)\n                }\n            })\n        }\n\n        // Get entries with pagination\n        const [entries, total] = await Promise.all([\n            db.changelogEntry.findMany({\n                where,\n                select: {\n                    id: true,\n                    title: true,\n                    excerpt: true, // Use excerpt instead of full content for list view\n                    version: true,\n                    publishedAt: true,\n                    scheduledAt: true,\n                    createdAt: true,\n                    updatedAt: true,\n                    tags: {\n                        select: {\n                            id: true,\n                            name: true,\n                            color: true\n                        }\n                    }\n                },\n                orderBy: {\n                    createdAt: 'desc'\n                },\n                skip,\n                take: limit\n            }),\n            db.changelogEntry.count({ where })\n        ])\n\n        // Get all tags used in this project's changelog\n        const tags = await db.changelogTag.findMany({\n            where: {\n                entries: {\n                    some: {\n                        changelogId: changelog.id\n                    }\n                }\n            }\n        })\n\n        // Log the successful changelog view\n        try {\n            await createAuditLog(\n                'VIEW_CHANGELOG_SUCCESS',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    projectId,\n                    changelogId: changelog.id,\n                    entriesCount: entries.length,\n                    totalEntries: total,\n                    tagsCount: tags.length,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create view success audit log:', auditLogError);\n        }\n\n        return NextResponse.json({\n            entries,\n            tags,\n            pagination: {\n                page,\n                limit,\n                total,\n                totalPages: Math.ceil(total / limit)\n            }\n        })\n    } catch (error) {\n        console.error('Error fetching changelog:', error)\n\n        // Log error in fetching changelog\n        try {\n            const { projectId } = await params;\n            const user = await validateAuthAndGetUser();\n            await createAuditLog(\n                'CHANGELOG_ERROR',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    action: 'VIEW_CHANGELOG',\n                    projectId,\n                    error: error instanceof Error ? error.message : 'Unknown error',\n                    stack: error instanceof Error ? error.stack : undefined,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create error audit log:', auditLogError);\n        }\n\n        return NextResponse.json(\n            { error: 'Failed to fetch changelog' },\n            { status: 500 }\n        )\n    }\n}\n\n/**\n * @method POST\n * @description Creates a new changelog entry for a given project\n * @query {\n *   projectId: String, required\n * }\n * @body {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"title\": { \"type\": \"string\" },\n *     \"content\": { \"type\": \"string\" },\n *     \"version\": { \"type\": \"number\" },\n *     \"tags\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"name\": { \"type\": \"string\" }\n *         }\n *       }\n *     }\n *   },\n *   \"required\": [\n *     \"title\",\n *     \"content\",\n *     \"version\",\n *     \"tags\"\n *   ]\n * }\n * @response 201 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"title\": { \"type\": \"string\" },\n *     \"content\": { \"type\": \"string\" },\n *     \"version\": { \"type\": \"number\" },\n *     \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"tags\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"name\": { \"type\": \"string\" }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 403 Unauthorized - User does not have 'ADMIN' role\n * @error 404 Project not found\n * @error 500 An unexpected error occurred while creating the changelog entry\n */\nexport async function POST(\n    request: Request,\n    { params }: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser()\n\n        if (user.role === 'VIEWER') {\n            return NextResponse.json(\n                { error: 'Insufficient permissions to create changelog entries' },\n                { status: 403 }\n            )\n        }\n\n        const projectId = (await params).projectId;\n        const requestBody = await request.json();\n        const { title, content, version, tags } = requestBody;\n\n        // Log the changelog entry creation attempt\n        try {\n            await createAuditLog(\n                'CREATE_CHANGELOG_ENTRY_ATTEMPT',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    projectId,\n                    entryTitle: title,\n                    entryVersion: version,\n                    tagCount: tags?.length || 0,\n                    contentLength: content?.length || 0,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create attempt audit log:', auditLogError);\n        }\n\n        // Get the changelog for this project\n        const changelog = await db.changelog.findUnique({\n            where: { projectId }\n        })\n\n        if (!changelog) {\n            // Log changelog not found error\n            try {\n                await createAuditLog(\n                    'CHANGELOG_NOT_FOUND',\n                    user.id,\n                    user.id, // Use user's own ID to avoid foreign key issues\n                    {\n                        action: 'CREATE_CHANGELOG_ENTRY',\n                        projectId,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create not found audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                { error: 'Changelog not found' },\n                { status: 404 }\n            )\n        }\n\n        // Check for entry with same version\n        const existingEntry = await db.changelogEntry.findFirst({\n            where: {\n                changelogId: changelog.id,\n                version\n            }\n        });\n\n        if (existingEntry) {\n            // Log version conflict error\n            try {\n                await createAuditLog(\n                    'CHANGELOG_VERSION_CONFLICT',\n                    user.id,\n                    user.id, // Use user's own ID to avoid foreign key issues\n                    {\n                        projectId,\n                        changelogId: changelog.id,\n                        conflictingVersion: version,\n                        existingEntryId: existingEntry.id,\n                        existingEntryTitle: existingEntry.title,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create version conflict audit log:', auditLogError);\n            }\n\n            return NextResponse.json(\n                { error: `Entry with version ${version} already exists` },\n                { status: 409 }\n            )\n        }\n\n        const entryAllowed = await SponsorService.checkEntryAllowed(projectId);\n        if (!entryAllowed) {\n            return NextResponse.json(\n                { error: 'Maximum changelog entries reached for this project' },\n                { status: 403 }\n            )\n        }\n\n        const entry = await db.changelogEntry.create({\n            data: {\n                title,\n                content,\n                excerpt: generateExcerpt(content), // Auto-generate excerpt from content\n                version,\n                changelogId: changelog.id,\n                tags: {\n                    connectOrCreate: tags.map((tag: { id?: string; name: string }) => ({\n                        where: {\n                            // If we have an ID, use it; otherwise use the name\n                            id: tag.id || undefined,\n                            name: !tag.id ? tag.name : undefined\n                        },\n                        create: {\n                            name: tag.name\n                        }\n                    }))\n                }\n            },\n            include: {\n                tags: true\n            }\n        })\n\n        // Log the successful changelog entry creation\n        try {\n            await createAuditLog(\n                'CREATE_CHANGELOG_ENTRY_SUCCESS',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    projectId,\n                    changelogId: changelog.id,\n                    entryId: entry.id,\n                    entryTitle: entry.title,\n                    entryVersion: entry.version,\n                    tags: entry.tags.map(tag => tag.name),\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create success audit log:', auditLogError);\n        }\n\n        // Auto-send to Slack if integration is configured and auto-send is enabled\n        try {\n            const slackIntegration = await db.slackIntegration.findUnique({\n                where: {projectId},\n                select: {\n                    enabled: true,\n                    autoSend: true,\n                    channelId: true,\n                }\n            })\n\n            if (slackIntegration?.enabled && slackIntegration?.autoSend && slackIntegration?.channelId) {\n                const entryUrl = `${new URL(request.url).origin.replace('/api', '')}/dashboard/projects/${projectId}/changelog/${entry.id}`\n\n                await postToSlack({\n                    projectId,\n                    entryId: entry.id,\n                    channelId: slackIntegration.channelId,\n                    title: entry.title,\n                    description: entry.excerpt || entry.content.substring(0, 200),\n                    url: entryUrl,\n                    color: '#0099ff'\n                })\n            }\n        } catch (slackError) {\n            console.error('Failed to auto-send to Slack:', slackError)\n            // Don't fail the request if Slack posting fails\n        }\n\n        return NextResponse.json(entry, { status: 201 })\n    } catch (error) {\n        console.error('Error creating changelog entry:', error)\n\n        // Log error in creating changelog entry\n        try {\n            const projectId = (await params).projectId;\n            const user = await validateAuthAndGetUser();\n            await createAuditLog(\n                'CHANGELOG_ERROR',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    action: 'CREATE_CHANGELOG_ENTRY',\n                    projectId,\n                    error: error instanceof Error ? error.message : 'Unknown error',\n                    stack: error instanceof Error ? error.stack : undefined,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create error audit log:', auditLogError);\n        }\n\n        return NextResponse.json(\n            { error: 'Failed to create changelog entry' },\n            { status: 500 }\n        )\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/changelog/tags/[tagId]/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server';\nimport {z} from 'zod';\nimport {db} from '@/lib/db';\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog';\nimport {createAuditLog} from '@/lib/utils/auditLog';\n\n/**\n * @method GET\n * @description Get a specific tag by ID with usage statistics\n * @param {string} projectId - Project ID from the URL params\n * @param {string} tagId - Tag ID from the URL params\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"name\": { \"type\": \"string\" },\n *     \"color\": { \"type\": \"string\", \"nullable\": true },\n *     \"_count\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"entries\": { \"type\": \"number\" }\n *       }\n *     }\n *   }\n * }\n * @error 401 Unauthorized\n * @error 404 Tag not found\n * @error 500 Failed to fetch tag\n */\nexport async function GET(\n    request: NextRequest,\n    {params}: { params: Promise<{ projectId: string; tagId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        if (!user) {\n            return NextResponse.json(\n                {error: 'Unauthorized'},\n                {status: 401}\n            );\n        }\n\n        const {tagId} = await (async () => params)();\n\n        const tag = await db.changelogTag.findUnique({\n            where: {id: tagId},\n            include: {\n                _count: {\n                    select: {\n                        entries: true\n                    }\n                }\n            }\n        });\n\n        if (!tag) {\n            return NextResponse.json(\n                {error: 'Tag not found'},\n                {status: 404}\n            );\n        }\n\n        return NextResponse.json(tag);\n    } catch (error) {\n        console.error('Error fetching tag:', error);\n        return NextResponse.json(\n            {error: 'Failed to fetch tag'},\n            {status: 500}\n        );\n    }\n}\n\n/**\n * @method PATCH\n * @description Update a specific tag\n * @param {string} projectId - Project ID from the URL params\n * @param {string} tagId - Tag ID from the URL params\n * @body {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"name\": {\n *       \"type\": \"string\",\n *       \"description\": \"The new tag name\"\n *     },\n *     \"color\": {\n *       \"type\": \"string\",\n *       \"nullable\": true,\n *       \"description\": \"Hex color code for the tag (e.g., #3b82f6)\"\n *     }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"name\": { \"type\": \"string\" },\n *     \"color\": { \"type\": \"string\", \"nullable\": true }\n *   }\n * }\n * @error 400 Validation failed\n * @error 401 Unauthorized\n * @error 403 Forbidden\n * @error 404 Tag not found\n * @error 500 Server error\n */\nconst updateTagSchema = z.object({\n    name: z.string().min(1).max(50).trim().optional(),\n    color: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional().nullable(),\n});\n\nexport async function PATCH(\n    request: NextRequest,\n    {params}: { params: Promise<{ projectId: string; tagId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        if (!user) {\n            return NextResponse.json(\n                {error: 'Unauthorized'},\n                {status: 401}\n            );\n        }\n\n        if (user.role !== 'ADMIN' && user.role !== 'STAFF') {\n            return NextResponse.json(\n                {error: 'Insufficient permissions'},\n                {status: 403}\n            );\n        }\n\n        const {projectId, tagId} = await (async () => params)();\n        const body = await request.json();\n        const validationResult = updateTagSchema.safeParse(body);\n\n        if (!validationResult.success) {\n            return NextResponse.json(\n                {\n                    error: 'Validation failed',\n                    details: validationResult.error.format()\n                },\n                {status: 400}\n            );\n        }\n\n        const {name, color} = validationResult.data;\n\n        // Check if tag exists\n        const existingTag = await db.changelogTag.findUnique({\n            where: {id: tagId}\n        });\n\n        if (!existingTag) {\n            return NextResponse.json(\n                {error: 'Tag not found'},\n                {status: 404}\n            );\n        }\n\n        // Check for name conflicts if name is being updated\n        if (name && name !== existingTag.name) {\n            const conflictingTag = await db.changelogTag.findFirst({\n                where: {\n                    name: {\n                        equals: name,\n                        mode: 'insensitive'\n                    },\n                    id: {\n                        not: tagId\n                    }\n                }\n            });\n\n            if (conflictingTag) {\n                return NextResponse.json(\n                    {error: 'A tag with this name already exists'},\n                    {status: 409}\n                );\n            }\n        }\n\n        // Build update data\n        const updateData: { name?: string; color?: string | null } = {};\n        if (name !== undefined) updateData.name = name;\n        if (color !== undefined) updateData.color = color;\n\n        // Update tag\n        const updatedTag = await db.changelogTag.update({\n            where: {id: tagId},\n            data: updateData\n        });\n\n        // Log action\n        try {\n            await createAuditLog(\n                'UPDATE_TAG',\n                user.id,\n                user.id,\n                {\n                    tagId: updatedTag.id,\n                    tagName: updatedTag.name,\n                    tagColor: updatedTag.color,\n                    projectId,\n                    changes: updateData,\n                    timestamp: new Date().toISOString(),\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create tag update audit log:', auditLogError);\n        }\n\n        return NextResponse.json(updatedTag);\n\n    } catch (error) {\n        console.error('Error updating tag:', error);\n        return NextResponse.json(\n            {error: 'Failed to update tag'},\n            {status: 500}\n        );\n    }\n}\n\n/**\n * @method DELETE\n * @description Delete a specific tag\n * @param {string} projectId - Project ID from the URL params\n * @param {string} tagId - Tag ID from the URL params\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\" },\n *     \"message\": { \"type\": \"string\" }\n *   }\n * }\n * @error 401 Unauthorized\n * @error 403 Forbidden\n * @error 404 Tag not found\n * @error 500 Server error\n */\nexport async function DELETE(\n    request: NextRequest,\n    {params}: { params: Promise<{ projectId: string; tagId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        if (!user) {\n            return NextResponse.json(\n                {error: 'Unauthorized'},\n                {status: 401}\n            );\n        }\n\n        if (user.role !== 'ADMIN' && user.role !== 'STAFF') {\n            return NextResponse.json(\n                {error: 'Insufficient permissions'},\n                {status: 403}\n            );\n        }\n\n        const {projectId, tagId} = await (async () => params)();\n\n        // Check if tag exists and get usage count\n        const existingTag = await db.changelogTag.findUnique({\n            where: {id: tagId},\n            include: {\n                _count: {\n                    select: {\n                        entries: true\n                    }\n                }\n            }\n        });\n\n        if (!existingTag) {\n            return NextResponse.json(\n                {error: 'Tag not found'},\n                {status: 404}\n            );\n        }\n\n        // Delete the tag (this will automatically remove it from associated entries due to the many-to-many relationship)\n        await db.changelogTag.delete({\n            where: {id: tagId}\n        });\n\n        // Log action\n        try {\n            await createAuditLog(\n                'DELETE_TAG',\n                user.id,\n                user.id,\n                {\n                    tagId: existingTag.id,\n                    tagName: existingTag.name,\n                    tagColor: existingTag.color,\n                    projectId,\n                    entriesAffected: existingTag._count.entries,\n                    timestamp: new Date().toISOString(),\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create tag deletion audit log:', auditLogError);\n        }\n\n        return NextResponse.json({\n            success: true,\n            message: `Tag \"${existingTag.name}\" deleted successfully. Removed from ${existingTag._count.entries} entries.`\n        });\n\n    } catch (error) {\n        console.error('Error deleting tag:', error);\n        return NextResponse.json(\n            {error: 'Failed to delete tag'},\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/changelog/tags/route.ts",
    "content": "import {validateAuthAndGetUser} from \"@/lib/utils/changelog\"\nimport {NextRequest, NextResponse} from \"next/server\"\nimport {db} from \"@/lib/db\";\nimport {z} from 'zod';\nimport {createAuditLog} from \"@/lib/utils/auditLog\";\n\n// Constants\nconst DEFAULT_PAGE_SIZE = 20;\nconst MAX_PAGE_SIZE = 100;\n\n/**\n * Get a list of project tags\n * @method GET\n * @description Fetches a list of tags specifically for a project's changelog along with pagination metadata. Users must be authenticated to access this endpoint.\n * @param {string} projectId - Project ID from the URL params\n * @queryParams {\n *   page: The page number of the results, starting from 1. Defaults to 1.\n *   limit: The number of results per page. Must be between 1 and 100. Defaults to 20.\n *   search: A search query to filter tags by name. Case-insensitive.\n *   includeUsage: Whether to include usage count for each tag. Defaults to false.\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"tags\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"name\": { \"type\": \"string\" },\n *           \"color\": { \"type\": \"string\", \"nullable\": true },\n *           \"_count\": {\n *             \"type\": \"object\",\n *             \"properties\": {\n *               \"entries\": { \"type\": \"number\" }\n *             }\n *           }\n *         }\n *       }\n *     },\n *     \"pagination\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"page\": { \"type\": \"number\" },\n *         \"limit\": { \"type\": \"number\" },\n *         \"totalCount\": { \"type\": \"number\" },\n *         \"totalPages\": { \"type\": \"number\" },\n *         \"hasMore\": { \"type\": \"boolean\" }\n *       }\n *     }\n *   }\n * }\n * @error 401 Unauthorized\n * @error 404 Project not found\n * @error 500 Failed to fetch tags\n */\nexport async function GET(\n    request: Request,\n    {params}: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        if (!user) {\n            return NextResponse.json(\n                {error: 'Unauthorized'},\n                {status: 401}\n            );\n        }\n\n        // Parse and validate query parameters\n        const {searchParams} = new URL(request.url);\n        const page = Math.max(1, parseInt(searchParams.get('page') || '1'));\n        const limit = Math.min(\n            MAX_PAGE_SIZE,\n            Math.max(1, parseInt(searchParams.get('limit') || String(DEFAULT_PAGE_SIZE)))\n        );\n        const search = searchParams.get('search') || '';\n        const includeUsage = searchParams.get('includeUsage') === 'true';\n\n        const {projectId} = await (async () => params)();\n\n        // Verify project exists and user has access\n        const project = await db.project.findUnique({\n            where: {id: projectId},\n            select: {\n                id: true,\n                changelog: {\n                    select: {id: true}\n                }\n            }\n        });\n\n        if (!project?.changelog) {\n            return NextResponse.json(\n                {error: 'Project or changelog not found'},\n                {status: 404}\n            );\n        }\n\n        // Build optimized query - get tags that are used in this project's changelog entries\n        const whereClause = {\n            entries: {\n                some: {\n                    changelogId: project.changelog.id\n                }\n            },\n            ...(search && {\n                name: {\n                    contains: search,\n                    mode: 'insensitive' as const\n                }\n            })\n        };\n\n        // Build include clause for usage statistics\n        const includeClause = includeUsage ? {\n            _count: {\n                select: {\n                    entries: {\n                        where: {\n                            changelogId: project.changelog.id\n                        }\n                    }\n                }\n            }\n        } : {};\n\n        // Execute queries in parallel\n        const [tags, totalCount] = await Promise.all([\n            db.changelogTag.findMany({\n                where: whereClause,\n                select: {\n                    id: true,\n                    name: true,\n                    color: true,\n                    createdAt: true,\n                    updatedAt: true,\n                    ...includeClause\n                },\n                orderBy: {\n                    name: 'asc'\n                },\n                skip: (page - 1) * limit,\n                take: limit\n            }),\n            db.changelogTag.count({\n                where: whereClause\n            })\n        ]);\n\n        // Calculate pagination metadata\n        const totalPages = Math.ceil(totalCount / limit);\n        const hasMore = page < totalPages;\n\n        return NextResponse.json({\n            tags,\n            pagination: {\n                page,\n                limit,\n                totalCount,\n                totalPages,\n                hasMore\n            }\n        });\n    } catch (error) {\n        console.error('Error fetching tags:', error);\n        return NextResponse.json(\n            {error: 'Failed to fetch tags'},\n            {status: 500}\n        );\n    }\n}\n\n/**\n * @method POST\n * @description Creates a new tag for a project's changelog\n * @param {string} projectId - Project ID from the URL params\n * @body {\n *   \"type\": \"object\",\n *   \"required\": [\"name\"],\n *   \"properties\": {\n *     \"name\": {\n *       \"type\": \"string\",\n *       \"description\": \"The tag name\"\n *     },\n *     \"color\": {\n *       \"type\": \"string\",\n *       \"nullable\": true,\n *       \"description\": \"Hex color code for the tag (e.g., #3b82f6)\"\n *     }\n *   }\n * }\n * @response 201 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"name\": { \"type\": \"string\" },\n *     \"color\": { \"type\": \"string\", \"nullable\": true }\n *   }\n * }\n * @error 400 Validation failed - Invalid input format\n * @error 401 Unauthorized - Please log in\n * @error 403 Forbidden - Insufficient permissions\n * @error 404 Project not found\n * @error 409 Tag already exists\n * @error 500 Server error - Failed to create tag\n * @secure cookieAuth\n */\n\nconst createTagSchema = z.object({\n    name: z.string().min(1).max(50).trim(),\n    color: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional().nullable(),\n});\n\nexport async function POST(\n    request: NextRequest,\n    {params}: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        // Validate user\n        const user = await validateAuthAndGetUser();\n\n        if (!user) {\n            return NextResponse.json(\n                {error: 'Unauthorized'},\n                {status: 401}\n            );\n        }\n\n        if (user.role !== 'ADMIN' && user.role !== 'STAFF') {\n            return NextResponse.json(\n                {error: 'Insufficient permissions'},\n                {status: 403}\n            );\n        }\n\n        // Get project ID from route params\n        const {projectId} = await (async () => params)();\n\n        // Verify project exists and user has access\n        const project = await db.project.findUnique({\n            where: {id: projectId},\n            select: {\n                id: true,\n                name: true,\n                changelog: {\n                    select: {id: true}\n                }\n            }\n        });\n\n        if (!project) {\n            return NextResponse.json(\n                {error: 'Project not found'},\n                {status: 404}\n            );\n        }\n\n        if (!project.changelog) {\n            return NextResponse.json(\n                {error: 'Project changelog not found'},\n                {status: 404}\n            );\n        }\n\n        // Parse and validate request body\n        const body = await request.json();\n        const validationResult = createTagSchema.safeParse(body);\n\n        if (!validationResult.success) {\n            return NextResponse.json(\n                {\n                    error: 'Validation failed',\n                    details: validationResult.error.format()\n                },\n                {status: 400}\n            );\n        }\n\n        const {name, color} = validationResult.data;\n\n        // Check if tag already exists (case insensitive)\n        const existingTag = await db.changelogTag.findFirst({\n            where: {\n                name: {\n                    equals: name,\n                    mode: 'insensitive'\n                }\n            }\n        });\n\n        if (existingTag) {\n            // Return the existing tag instead of an error\n            return NextResponse.json(existingTag, {status: 200});\n        }\n\n        // Create new tag with color\n        const newTag = await db.changelogTag.create({\n            data: {\n                name: name,\n                color: color || null,\n            }\n        });\n\n        // Log action\n        try {\n            await createAuditLog(\n                'CREATE_TAG',\n                user.id,\n                user.id,\n                {\n                    reason: 'Tag created for project',\n                    tagId: newTag.id,\n                    tagName: newTag.name,\n                    tagColor: newTag.color,\n                    projectId,\n                    projectName: project.name,\n                    timestamp: new Date().toISOString(),\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create tag created audit log:', auditLogError);\n        }\n\n        return NextResponse.json(newTag, {status: 201});\n\n    } catch (error) {\n        console.error('Error creating tag:', error);\n        return NextResponse.json(\n            {error: 'Failed to create tag'},\n            {status: 500}\n        );\n    }\n}\n\n/**\n * @method PATCH\n * @description Updates an existing tag's properties\n * @param {string} projectId - Project ID from the URL params\n * @body {\n *   \"type\": \"object\",\n *   \"required\": [\"tagId\"],\n *   \"properties\": {\n *     \"tagId\": {\n *       \"type\": \"string\",\n *       \"description\": \"The ID of the tag to update\"\n *     },\n *     \"name\": {\n *       \"type\": \"string\",\n *       \"description\": \"The new tag name\"\n *     },\n *     \"color\": {\n *       \"type\": \"string\",\n *       \"nullable\": true,\n *       \"description\": \"Hex color code for the tag (e.g., #3b82f6)\"\n *     }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"name\": { \"type\": \"string\" },\n *     \"color\": { \"type\": \"string\", \"nullable\": true }\n *   }\n * }\n * @error 400 Validation failed\n * @error 401 Unauthorized\n * @error 403 Forbidden\n * @error 404 Tag or project not found\n * @error 409 Tag name already exists\n * @error 500 Server error\n */\nconst updateTagSchema = z.object({\n    tagId: z.string().min(1),\n    name: z.string().min(1).max(50).trim().optional(),\n    color: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional().nullable(),\n});\n\nexport async function PATCH(\n    request: NextRequest,\n    {params}: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        if (!user) {\n            return NextResponse.json(\n                {error: 'Unauthorized'},\n                {status: 401}\n            );\n        }\n\n        if (user.role !== 'ADMIN' && user.role !== 'STAFF') {\n            return NextResponse.json(\n                {error: 'Insufficient permissions'},\n                {status: 403}\n            );\n        }\n\n        const {projectId} = await (async () => params)();\n\n        // Verify project exists\n        const project = await db.project.findUnique({\n            where: {id: projectId},\n            select: {\n                id: true,\n                name: true,\n                changelog: {\n                    select: {id: true}\n                }\n            }\n        });\n\n        if (!project?.changelog) {\n            return NextResponse.json(\n                {error: 'Project or changelog not found'},\n                {status: 404}\n            );\n        }\n\n        const body = await request.json();\n        const validationResult = updateTagSchema.safeParse(body);\n\n        if (!validationResult.success) {\n            return NextResponse.json(\n                {\n                    error: 'Validation failed',\n                    details: validationResult.error.format()\n                },\n                {status: 400}\n            );\n        }\n\n        const {tagId, name, color} = validationResult.data;\n\n        // Check if tag exists\n        const existingTag = await db.changelogTag.findUnique({\n            where: {id: tagId},\n            include: {\n                _count: {\n                    select: {\n                        entries: {\n                            where: {\n                                changelogId: project.changelog.id\n                            }\n                        }\n                    }\n                }\n            }\n        });\n\n        if (!existingTag) {\n            return NextResponse.json(\n                {error: 'Tag not found'},\n                {status: 404}\n            );\n        }\n\n        // Check for name conflicts if name is being updated\n        if (name && name !== existingTag.name) {\n            const conflictingTag = await db.changelogTag.findFirst({\n                where: {\n                    name: {\n                        equals: name,\n                        mode: 'insensitive'\n                    },\n                    id: {\n                        not: tagId\n                    }\n                }\n            });\n\n            if (conflictingTag) {\n                return NextResponse.json(\n                    {error: 'A tag with this name already exists'},\n                    {status: 409}\n                );\n            }\n        }\n\n        // Build update data\n        const updateData: { name?: string; color?: string | null } = {};\n        if (name !== undefined) updateData.name = name;\n        if (color !== undefined) updateData.color = color;\n\n        // Update tag\n        const updatedTag = await db.changelogTag.update({\n            where: {id: tagId},\n            data: updateData\n        });\n\n        // Log action\n        try {\n            await createAuditLog(\n                'UPDATE_TAG',\n                user.id,\n                user.id,\n                {\n                    tagId: updatedTag.id,\n                    tagName: updatedTag.name,\n                    tagColor: updatedTag.color,\n                    projectId,\n                    projectName: project.name,\n                    changes: updateData,\n                    entriesAffected: existingTag._count.entries,\n                    timestamp: new Date().toISOString(),\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create tag update audit log:', auditLogError);\n        }\n\n        return NextResponse.json(updatedTag);\n\n    } catch (error) {\n        console.error('Error updating tag:', error);\n        return NextResponse.json(\n            {error: 'Failed to update tag'},\n            {status: 500}\n        );\n    }\n}\n\n/**\n * @method DELETE\n * @description Deletes a tag and removes it from all entries in the project\n * @param {string} projectId - Project ID from the URL params\n * @queryParams {\n *   tagId: The ID of the tag to delete (required)\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\" },\n *     \"message\": { \"type\": \"string\" },\n *     \"entriesAffected\": { \"type\": \"number\" }\n *   }\n * }\n * @error 400 Missing tagId parameter\n * @error 401 Unauthorized\n * @error 403 Forbidden\n * @error 404 Tag or project not found\n * @error 500 Server error\n */\nexport async function DELETE(\n    request: NextRequest,\n    {params}: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        if (!user) {\n            return NextResponse.json(\n                {error: 'Unauthorized'},\n                {status: 401}\n            );\n        }\n\n        if (user.role !== 'ADMIN' && user.role !== 'STAFF') {\n            return NextResponse.json(\n                {error: 'Insufficient permissions'},\n                {status: 403}\n            );\n        }\n\n        const {projectId} = await (async () => params)();\n        const {searchParams} = new URL(request.url);\n        const tagId = searchParams.get('tagId');\n\n        if (!tagId) {\n            return NextResponse.json(\n                {error: 'Missing tagId parameter'},\n                {status: 400}\n            );\n        }\n\n        // Verify project exists\n        const project = await db.project.findUnique({\n            where: {id: projectId},\n            select: {\n                id: true,\n                name: true,\n                changelog: {\n                    select: {id: true}\n                }\n            }\n        });\n\n        if (!project?.changelog) {\n            return NextResponse.json(\n                {error: 'Project or changelog not found'},\n                {status: 404}\n            );\n        }\n\n        // Check if tag exists and get usage count for this project\n        const existingTag = await db.changelogTag.findUnique({\n            where: {id: tagId},\n            include: {\n                _count: {\n                    select: {\n                        entries: {\n                            where: {\n                                changelogId: project.changelog.id\n                            }\n                        }\n                    }\n                }\n            }\n        });\n\n        if (!existingTag) {\n            return NextResponse.json(\n                {error: 'Tag not found'},\n                {status: 404}\n            );\n        }\n\n        const entriesAffected = existingTag._count.entries;\n\n        // Delete the tag (this will automatically remove it from associated entries due to the many-to-many relationship)\n        await db.changelogTag.delete({\n            where: {id: tagId}\n        });\n\n        // Log action\n        try {\n            await createAuditLog(\n                'DELETE_TAG',\n                user.id,\n                user.id,\n                {\n                    tagId: existingTag.id,\n                    tagName: existingTag.name,\n                    tagColor: existingTag.color,\n                    projectId,\n                    projectName: project.name,\n                    entriesAffected,\n                    timestamp: new Date().toISOString(),\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create tag deletion audit log:', auditLogError);\n        }\n\n        return NextResponse.json({\n            success: true,\n            message: `Tag \"${existingTag.name}\" deleted successfully`,\n            entriesAffected\n        });\n\n    } catch (error) {\n        console.error('Error deleting tag:', error);\n        return NextResponse.json(\n            {error: 'Failed to delete tag'},\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/cli/link/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server';\nimport {z} from 'zod';\nimport {db} from '@/lib/db';\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog';\nimport {\n    ProjectLinkRequest,\n    ProjectLinkResponse,\n    ProjectApiError,\n    ProjectApiSuccess\n} from '@/lib/types/cli/project-api';\n\nconst linkRequestSchema = z.object({\n    repositoryUrl: z.string().url().optional(),\n    branch: z.string().min(1).optional(),\n    localPath: z.string().optional(),\n});\n\n/**\n * @method POST\n * @description Link a project to a Git repository for CLI integration\n * @body {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"repositoryUrl\": {\n *       \"type\": \"string\",\n *       \"format\": \"uri\",\n *       \"description\": \"Git repository URL\"\n *     },\n *     \"branch\": {\n *       \"type\": \"string\",\n *       \"description\": \"Default branch for syncing\"\n *     },\n *     \"localPath\": {\n *       \"type\": \"string\",\n *       \"description\": \"Local path reference\"\n *     }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\" },\n *     \"data\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"success\": { \"type\": \"boolean\" },\n *         \"message\": { \"type\": \"string\" },\n *         \"linkId\": { \"type\": \"string\" },\n *         \"linkedAt\": { \"type\": \"string\" }\n *       }\n *     }\n *   }\n * }\n * @error 400 Invalid request data\n * @error 401 Unauthorized\n * @error 404 Project not found\n * @error 409 Project already linked\n * @secure bearerAuth\n */\nexport async function POST(\n    request: NextRequest,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        // Validate authentication\n        await validateAuthAndGetUser();\n        const {projectId} = await context.params;\n\n        // Parse and validate request body\n        const body = await request.json();\n        const validatedData = linkRequestSchema.parse(body) as ProjectLinkRequest;\n\n        // Check if project exists and user has access\n        const project = await db.project.findFirst({\n            where: {\n                id: projectId,\n            },\n            include: {\n                syncMetadata: true\n            }\n        });\n\n        if (!project) {\n            const errorResponse: ProjectApiError = {\n                success: false,\n                error: 'Project not found',\n                message: 'Project not found or you do not have access to it',\n                code: 'PROJECT_NOT_FOUND'\n            };\n            return NextResponse.json(errorResponse, {status: 404});\n        }\n\n        // Check if project is already linked\n        if (project.syncMetadata) {\n            const errorResponse: ProjectApiError = {\n                success: false,\n                error: 'Project already linked',\n                message: 'This project is already linked to a repository',\n                code: 'PROJECT_ALREADY_LINKED',\n                details: {\n                    repositoryUrl: project.syncMetadata.repositoryUrl,\n                    linkedAt: project.syncMetadata.createdAt.toISOString()\n                }\n            };\n            return NextResponse.json(errorResponse, {status: 409});\n        }\n\n        // Create sync metadata\n        const syncMetadata = await db.projectSyncMetadata.create({\n            data: {\n                projectId: project.id,\n                repositoryUrl: validatedData.repositoryUrl,\n                branch: validatedData.branch || 'main',\n                totalCommitsSynced: 0,\n            }\n        });\n\n        // Update project with link information\n        await db.project.update({\n            where: {id: project.id},\n            data: {\n                updatedAt: new Date()\n            }\n        });\n\n        // Create response\n        const linkResponse: ProjectLinkResponse = {\n            success: true,\n            message: 'Project linked successfully',\n            linkId: syncMetadata.id,\n            linkedAt: syncMetadata.createdAt.toISOString()\n        };\n\n        const successResponse: ProjectApiSuccess<ProjectLinkResponse> = {\n            success: true,\n            data: linkResponse,\n            message: 'Project linked to repository'\n        };\n\n        return NextResponse.json(successResponse);\n\n    } catch (error) {\n        console.error('Project link error:', error);\n\n        if (error instanceof z.ZodError) {\n            const errorResponse: ProjectApiError = {\n                success: false,\n                error: 'Validation failed',\n                message: 'Invalid request data',\n                code: 'VALIDATION_ERROR',\n                details: {errors: error.errors}\n            };\n            return NextResponse.json(errorResponse, {status: 400});\n        }\n\n        if (error instanceof Error && error.message.includes('token')) {\n            const errorResponse: ProjectApiError = {\n                success: false,\n                error: 'Unauthorized',\n                message: 'Authentication required',\n                code: 'AUTH_REQUIRED'\n            };\n            return NextResponse.json(errorResponse, {status: 401});\n        }\n\n        const errorResponse: ProjectApiError = {\n            success: false,\n            error: 'Internal server error',\n            message: 'An unexpected error occurred',\n            code: 'INTERNAL_ERROR'\n        };\n        return NextResponse.json(errorResponse, {status: 500});\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/cli/sync/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server';\nimport {z} from 'zod';\nimport {db} from '@/lib/db';\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog';\nimport {\n    SyncRequest,\n    SyncResponse,\n    ProjectApiError,\n    ProjectApiSuccess\n} from '@/lib/types/cli/project-api';\n\nconst conventionalCommitTypes = [\n    'feat', 'fix', 'docs', 'style', 'refactor', 'perf',\n    'test', 'build', 'ci', 'chore', 'revert'\n] as const;\n\n// Define proper type for syncMetadata\ninterface SyncMetadata {\n    id: string;\n    lastSyncHash: string | null;\n    repositoryUrl: string | null;\n    lastSyncedAt: Date | null;\n    totalCommitsSynced: number;\n    branch: string;\n    // Add other properties as needed based on your Prisma schema\n}\n\nconst commitDataSchema = z.object({\n    hash: z.string().min(1),\n    message: z.string().min(1),\n    author: z.string().min(1),\n    email: z.string().email(),\n    date: z.string(),\n    files: z.array(z.string()),\n    type: z.enum(conventionalCommitTypes).optional(),\n    scope: z.string().optional(),\n    breaking: z.boolean().optional(),\n    body: z.string().optional(),\n    footer: z.string().optional(),\n});\n\nconst syncRequestSchema = z.object({\n    commits: z.array(commitDataSchema),\n    lastSyncHash: z.string().optional(),\n    branch: z.string().min(1).default('main'),\n    repositoryUrl: z.string().url().optional(),\n    metadata: z.object({\n        cliVersion: z.string().optional(),\n        platform: z.string().optional(),\n        timestamp: z.string(),\n    }).optional(),\n});\n\n/**\n * @method POST\n * @description Sync commits from CLI to Changerawr project\n * @body {\n *   \"type\": \"object\",\n *   \"required\": [\"commits\", \"branch\"],\n *   \"properties\": {\n *     \"commits\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"required\": [\"hash\", \"message\", \"author\", \"email\", \"date\", \"files\"],\n *         \"properties\": {\n *           \"hash\": { \"type\": \"string\" },\n *           \"message\": { \"type\": \"string\" },\n *           \"author\": { \"type\": \"string\" },\n *           \"email\": { \"type\": \"string\", \"format\": \"email\" },\n *           \"date\": { \"type\": \"string\" },\n *           \"files\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n *           \"type\": { \"type\": \"string\" },\n *           \"scope\": { \"type\": \"string\" },\n *           \"breaking\": { \"type\": \"boolean\" },\n *           \"body\": { \"type\": \"string\" },\n *           \"footer\": { \"type\": \"string\" }\n *         }\n *       }\n *     },\n *     \"lastSyncHash\": { \"type\": \"string\" },\n *     \"branch\": { \"type\": \"string\" },\n *     \"repositoryUrl\": { \"type\": \"string\", \"format\": \"uri\" },\n *     \"metadata\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"cliVersion\": { \"type\": \"string\" },\n *         \"platform\": { \"type\": \"string\" },\n *         \"timestamp\": { \"type\": \"string\" }\n *       }\n *     }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\" },\n *     \"data\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"success\": { \"type\": \"boolean\" },\n *         \"processed\": { \"type\": \"number\" },\n *         \"skipped\": { \"type\": \"number\" },\n *         \"errors\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n *         \"warnings\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n *         \"newSyncHash\": { \"type\": \"string\" },\n *         \"syncedAt\": { \"type\": \"string\" },\n *         \"nextSyncRecommendedAt\": { \"type\": \"string\" }\n *       }\n *     }\n *   }\n * }\n * @error 400 Invalid request data\n * @error 401 Unauthorized\n * @error 404 Project not found or not linked\n * @secure bearerAuth\n */\nexport async function POST(\n    request: NextRequest,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        // Validate authentication\n        await validateAuthAndGetUser();\n        const {projectId} = await context.params;\n\n        // Parse and validate request body\n        const body = await request.json();\n\n        // Log the raw body for debugging\n        console.log('Raw sync request body:', JSON.stringify(body, null, 2));\n\n        const validatedData = syncRequestSchema.parse(body) as SyncRequest;\n\n        // Check if project exists and user has access\n        const project = await db.project.findFirst({\n            where: {\n                id: projectId,\n            },\n            include: {\n                syncMetadata: true\n            }\n        });\n\n        if (!project) {\n            const errorResponse: ProjectApiError = {\n                success: false,\n                error: 'Project not found',\n                message: 'Project not found or you do not have access to it',\n                code: 'PROJECT_NOT_FOUND'\n            };\n            return NextResponse.json(errorResponse, {status: 404});\n        }\n\n        // Check if project is linked\n        if (!project.syncMetadata) {\n            const errorResponse: ProjectApiError = {\n                success: false,\n                error: 'Project not linked',\n                message: 'This project is not linked to any repository. Please link it first.',\n                code: 'PROJECT_NOT_LINKED'\n            };\n            return NextResponse.json(errorResponse, {status: 404});\n        }\n\n        // Process commits\n        const syncResult = await processSyncRequest(project.id, validatedData, project.syncMetadata);\n\n        // Update sync metadata\n        await db.projectSyncMetadata.update({\n            where: {id: project.syncMetadata.id},\n            data: {\n                lastSyncHash: syncResult.newSyncHash,\n                lastSyncedAt: new Date(),\n                totalCommitsSynced: {\n                    increment: syncResult.processed\n                },\n                branch: validatedData.branch,\n                repositoryUrl: validatedData.repositoryUrl || project.syncMetadata.repositoryUrl,\n            }\n        });\n\n        const successResponse: ProjectApiSuccess<SyncResponse> = {\n            success: true,\n            data: syncResult,\n            message: `Synced ${syncResult.processed} commits successfully`\n        };\n\n        return NextResponse.json(successResponse);\n\n    } catch (error) {\n        console.error('Project sync error:', error);\n\n        if (error instanceof z.ZodError) {\n            console.log('Zod validation errors:', error.errors);\n            const errorResponse: ProjectApiError = {\n                success: false,\n                error: 'Validation failed',\n                message: 'Invalid request data',\n                code: 'VALIDATION_ERROR',\n                details: {errors: error.errors}\n            };\n            return NextResponse.json(errorResponse, {status: 400});\n        }\n\n        if (error instanceof Error && error.message.includes('token')) {\n            const errorResponse: ProjectApiError = {\n                success: false,\n                error: 'Unauthorized',\n                message: 'Authentication required',\n                code: 'AUTH_REQUIRED'\n            };\n            return NextResponse.json(errorResponse, {status: 401});\n        }\n\n        const errorResponse: ProjectApiError = {\n            success: false,\n            error: 'Internal server error',\n            message: 'An unexpected error occurred during sync',\n            code: 'SYNC_ERROR'\n        };\n        return NextResponse.json(errorResponse, {status: 500});\n    }\n}\n\nasync function processSyncRequest(\n    projectId: string,\n    syncData: SyncRequest,\n    syncMetadata: SyncMetadata\n): Promise<SyncResponse> {\n    const syncedAt = new Date();\n    let processed = 0;\n    let skipped = 0;\n    const errors: string[] = [];\n    const warnings: string[] = [];\n\n    // Get existing commits to avoid duplicates\n    const existingCommits = await db.syncedCommit.findMany({\n        where: {projectId},\n        select: {commitHash: true}\n    });\n\n    const existingHashes = new Set(existingCommits.map(c => c.commitHash));\n\n    // Process each commit\n    for (const commit of syncData.commits) {\n        try {\n            // Skip if commit already exists\n            if (existingHashes.has(commit.hash)) {\n                skipped++;\n                continue;\n            }\n\n            // Validate commit date\n            const commitDate = new Date(commit.date);\n            if (isNaN(commitDate.getTime())) {\n                errors.push(`Invalid date for commit ${commit.hash}: ${commit.date}`);\n                continue;\n            }\n\n            // Create synced commit record\n            await db.syncedCommit.create({\n                data: {\n                    projectId,\n                    commitHash: commit.hash,\n                    commitMessage: commit.message,\n                    commitAuthor: commit.author,\n                    commitEmail: commit.email,\n                    commitDate,\n                    commitFiles: commit.files,\n                    conventionalType: commit.type,\n                    conventionalScope: commit.scope,\n                    isBreaking: commit.breaking || false,\n                    commitBody: commit.body,\n                    commitFooter: commit.footer,\n                    syncedAt,\n                    branch: syncData.branch,\n                }\n            });\n\n            processed++;\n\n            // Add warning for non-conventional commits\n            if (!commit.type && commit.message.includes(':')) {\n                warnings.push(`Commit ${commit.hash.substring(0, 7)} appears to follow conventional format but type was not parsed`);\n            }\n\n        } catch (commitError) {\n            console.error(`Error processing commit ${commit.hash}:`, commitError);\n            errors.push(`Failed to process commit ${commit.hash}: ${commitError instanceof Error ? commitError.message : 'Unknown error'}`);\n        }\n    }\n\n    // Determine new sync hash (last processed commit or provided hash)\n    const newSyncHash = syncData.commits.length > 0\n        ? syncData.commits[syncData.commits.length - 1]?.hash || syncMetadata.lastSyncHash\n        : syncMetadata.lastSyncHash;\n\n    // Calculate next sync recommendation (24 hours from now)\n    const nextSyncRecommendedAt = new Date();\n    nextSyncRecommendedAt.setHours(nextSyncRecommendedAt.getHours() + 24);\n\n    return {\n        success: true,\n        processed,\n        skipped,\n        errors,\n        warnings,\n        newSyncHash: newSyncHash || '',\n        syncedAt: syncedAt.toISOString(),\n        nextSyncRecommendedAt: nextSyncRecommendedAt.toISOString(),\n    };\n}"
  },
  {
    "path": "app/api/projects/[projectId]/cli/sync/status/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server';\nimport { db } from '@/lib/db';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport {\n    SyncStatusResponse,\n    ProjectApiError,\n    ProjectApiSuccess\n} from '@/lib/types/cli/project-api';\n\n/**\n * @method GET\n * @description Get sync status for a project\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\" },\n *     \"data\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"success\": { \"type\": \"boolean\" },\n *         \"lastSync\": {\n *           \"type\": \"object\",\n *           \"properties\": {\n *             \"syncHash\": { \"type\": \"string\" },\n *             \"syncedAt\": { \"type\": \"string\" },\n *             \"commitCount\": { \"type\": \"number\" },\n *             \"branch\": { \"type\": \"string\" }\n *           }\n *         },\n *         \"pendingCommits\": { \"type\": \"number\" },\n *         \"totalCommits\": { \"type\": \"number\" },\n *         \"repositoryInfo\": {\n *           \"type\": \"object\",\n *           \"properties\": {\n *             \"url\": { \"type\": \"string\" },\n *             \"branch\": { \"type\": \"string\" },\n *             \"lastCommitHash\": { \"type\": \"string\" },\n *             \"linkedAt\": { \"type\": \"string\" }\n *           }\n *         },\n *         \"syncSettings\": {\n *           \"type\": \"object\",\n *           \"properties\": {\n *             \"autoSync\": { \"type\": \"boolean\" },\n *             \"lastSyncInterval\": { \"type\": \"number\" },\n *             \"maxCommitsPerSync\": { \"type\": \"number\" }\n *           }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 401 Unauthorized\n * @error 404 Project not found or not linked\n * @secure bearerAuth\n */\nexport async function GET(\n    request: NextRequest,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        // Validate authentication\n        await validateAuthAndGetUser();\n        const { projectId } = await context.params;\n\n        // Get project with sync metadata and commit counts - include user access check\n        const project = await db.project.findFirst({\n            where: {\n                id: projectId,\n            },\n            include: {\n                syncMetadata: true,\n                _count: {\n                    select: {\n                        syncedCommits: true\n                    }\n                }\n            }\n        });\n\n        if (!project) {\n            const errorResponse: ProjectApiError = {\n                success: false,\n                error: 'Project not found',\n                message: 'Project not found or you do not have access to it',\n                code: 'PROJECT_NOT_FOUND'\n            };\n            return NextResponse.json(errorResponse, { status: 404 });\n        }\n\n        // Check if project is linked\n        if (!project.syncMetadata) {\n            const errorResponse: ProjectApiError = {\n                success: false,\n                error: 'Project not linked',\n                message: 'This project is not linked to any repository',\n                code: 'PROJECT_NOT_LINKED'\n            };\n            return NextResponse.json(errorResponse, { status: 404 });\n        }\n\n        // Get the most recent synced commit for last sync info\n        const lastSyncedCommit = await db.syncedCommit.findFirst({\n            where: { projectId },\n            orderBy: { syncedAt: 'desc' },\n            select: {\n                commitHash: true,\n                syncedAt: true,\n                branch: true\n            }\n        });\n\n        // Get commits from the last 24 hours to estimate pending commits\n        const last24Hours = new Date();\n        last24Hours.setHours(last24Hours.getHours() - 24);\n\n        const recentCommitsCount = await db.syncedCommit.count({\n            where: {\n                projectId,\n                syncedAt: {\n                    gte: last24Hours\n                }\n            }\n        });\n\n        // Calculate sync interval (time between last two syncs)\n        const lastTwoSyncs = await db.syncedCommit.findMany({\n            where: { projectId },\n            orderBy: { syncedAt: 'desc' },\n            take: 2,\n            select: { syncedAt: true }\n        });\n\n        let lastSyncInterval = 0;\n        if (lastTwoSyncs.length === 2) {\n            const timeDiff = lastTwoSyncs[0]!.syncedAt.getTime() - lastTwoSyncs[1]!.syncedAt.getTime();\n            lastSyncInterval = Math.round(timeDiff / (1000 * 60)); // Convert to minutes\n        }\n\n        // Build response\n        const syncStatus: SyncStatusResponse = {\n            success: true,\n            lastSync: lastSyncedCommit ? {\n                syncHash: lastSyncedCommit.commitHash,\n                syncedAt: lastSyncedCommit.syncedAt.toISOString(),\n                commitCount: project._count.syncedCommits,\n                branch: lastSyncedCommit.branch\n            } : undefined,\n            pendingCommits: recentCommitsCount, // This is an approximation\n            totalCommits: project._count.syncedCommits,\n            repositoryInfo: {\n                url: project.syncMetadata.repositoryUrl || undefined,\n                branch: project.syncMetadata.branch,\n                lastCommitHash: project.syncMetadata.lastSyncHash || undefined,\n                linkedAt: project.syncMetadata.createdAt.toISOString()\n            },\n            syncSettings: {\n                autoSync: true, // Default assumption for CLI users\n                lastSyncInterval,\n                maxCommitsPerSync: 100 // Reasonable default limit\n            }\n        };\n\n        const successResponse: ProjectApiSuccess<SyncStatusResponse> = {\n            success: true,\n            data: syncStatus,\n            message: 'Sync status retrieved successfully'\n        };\n\n        return NextResponse.json(successResponse);\n\n    } catch (error) {\n        console.error('Sync status error:', error);\n\n        if (error instanceof Error && error.message.includes('token')) {\n            const errorResponse: ProjectApiError = {\n                success: false,\n                error: 'Unauthorized',\n                message: 'Authentication required',\n                code: 'AUTH_REQUIRED'\n            };\n            return NextResponse.json(errorResponse, { status: 401 });\n        }\n\n        const errorResponse: ProjectApiError = {\n            success: false,\n            error: 'Internal server error',\n            message: 'An unexpected error occurred while fetching sync status',\n            code: 'SYNC_STATUS_ERROR'\n        };\n        return NextResponse.json(errorResponse, { status: 500 });\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/cli/unlink/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server';\nimport {z} from 'zod';\nimport {db} from '@/lib/db';\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog';\nimport {\n    ProjectUnlinkRequest,\n    ProjectUnlinkResponse,\n    ProjectApiError,\n    ProjectApiSuccess\n} from '@/lib/types/cli/project-api';\nimport {createAuditLog} from \"@/lib/utils/auditLog\";\n\nconst unlinkRequestSchema = z.object({\n    reason: z.string().optional(),\n    preserveData: z.boolean().default(true),\n});\n\n/**\n * @method POST\n * @description Unlink a project from its Git repository\n * @body {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"reason\": {\n *       \"type\": \"string\",\n *       \"description\": \"Reason for unlinking\"\n *     },\n *     \"preserveData\": {\n *       \"type\": \"boolean\",\n *       \"description\": \"Whether to preserve synced commit data\",\n *       \"default\": true\n *     }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\" },\n *     \"data\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"success\": { \"type\": \"boolean\" },\n *         \"message\": { \"type\": \"string\" },\n *         \"unlinkedAt\": { \"type\": \"string\" }\n *       }\n *     }\n *   }\n * }\n * @error 400 Invalid request data\n * @error 401 Unauthorized\n * @error 404 Project not found or not linked\n * @secure bearerAuth\n */\nexport async function POST(\n    request: NextRequest,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        // Validate authentication\n        const user = await validateAuthAndGetUser();\n        const {projectId} = await context.params;\n\n        // Parse and validate request body\n        const body = await request.json();\n        const validatedData = unlinkRequestSchema.parse(body) as ProjectUnlinkRequest;\n\n        // Check if project exists and user has access\n        const project = await db.project.findFirst({\n            where: {\n                id: projectId,\n            },\n            include: {\n                syncMetadata: true,\n                syncedCommits: true\n            }\n        });\n\n        if (!project) {\n            const errorResponse: ProjectApiError = {\n                success: false,\n                error: 'Project not found',\n                message: 'Project not found or you do not have access to it',\n                code: 'PROJECT_NOT_FOUND'\n            };\n            return NextResponse.json(errorResponse, {status: 404});\n        }\n\n        const unlinkedAt = new Date();\n\n        // Force delete all sync metadata for this project (in case there are orphaned records)\n        await db.projectSyncMetadata.deleteMany({\n            where: {projectId: project.id}\n        });\n\n        // Handle data preservation or deletion\n        if (!validatedData.preserveData) {\n            // Delete all synced commits\n            await db.syncedCommit.deleteMany({\n                where: {projectId: project.id}\n            });\n        }\n\n        // Update project\n        await db.project.update({\n            where: {id: project.id},\n            data: {\n                updatedAt: unlinkedAt\n            }\n        });\n\n        // Create audit log entry\n        try {\n            await createAuditLog(\n                'PROJECT_UNLINKED',\n                user.id,\n                user.id,\n                {\n                    reason: validatedData.reason,\n                    preserveData: validatedData.preserveData,\n                    commitCount: project.syncedCommits.length,\n                    repositoryUrl: project.syncMetadata?.repositoryUrl || 'Unknown',\n                    unlinkedAt: unlinkedAt.toISOString()\n                }\n            );\n        } catch (auditError) {\n            console.error('Failed to create audit log:', auditError);\n            // Continue execution - audit logging is not critical\n        }\n\n        // Create response\n        const unlinkResponse: ProjectUnlinkResponse = {\n            success: true,\n            message: !project.syncMetadata\n                ? 'Project was not linked, but any orphaned sync data has been cleaned up'\n                : validatedData.preserveData\n                    ? 'Project unlinked successfully (data preserved)'\n                    : 'Project unlinked successfully (data removed)',\n            unlinkedAt: unlinkedAt.toISOString()\n        };\n\n        const successResponse: ProjectApiSuccess<ProjectUnlinkResponse> = {\n            success: true,\n            data: unlinkResponse,\n            message: 'Project unlinked from repository'\n        };\n\n        return NextResponse.json(successResponse);\n\n    } catch (error) {\n        console.error('Project unlink error:', error);\n\n        if (error instanceof z.ZodError) {\n            const errorResponse: ProjectApiError = {\n                success: false,\n                error: 'Validation failed',\n                message: 'Invalid request data',\n                code: 'VALIDATION_ERROR',\n                details: {errors: error.errors}\n            };\n            return NextResponse.json(errorResponse, {status: 400});\n        }\n\n        if (error instanceof Error && error.message.includes('token')) {\n            const errorResponse: ProjectApiError = {\n                success: false,\n                error: 'Unauthorized',\n                message: 'Authentication required',\n                code: 'AUTH_REQUIRED'\n            };\n            return NextResponse.json(errorResponse, {status: 401});\n        }\n\n        const errorResponse: ProjectApiError = {\n            success: false,\n            error: 'Internal server error',\n            message: 'An unexpected error occurred',\n            code: 'INTERNAL_ERROR'\n        };\n        return NextResponse.json(errorResponse, {status: 500});\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/integrations/email/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { db } from '@/lib/db';\nimport { z } from 'zod';\n\n// Validation schema for email config\nconst emailConfigSchema = z.object({\n    enabled: z.boolean(),\n    smtpHost: z.string().min(1, 'SMTP host is required'),\n    smtpPort: z.coerce.number().int().min(1).max(65535),\n    smtpUser: z.string().optional().nullable(),\n    smtpPassword: z.string().optional().nullable(),\n    smtpSecure: z.boolean().default(true),\n    fromEmail: z.string().email('Invalid email address'),\n    fromName: z.string().optional().nullable(),\n    replyToEmail: z.string().email('Invalid email address').optional().nullable().or(z.literal('')),\n    defaultSubject: z.string().optional().nullable(),\n});\n\n/**\n * @method GET\n * @description Retrieves the email configuration for a project\n */\nexport async function GET(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        await validateAuthAndGetUser();\n        const { projectId } = await context.params;\n\n        // Verify project access\n        const project = await db.project.findUnique({\n            where: { id: projectId },\n            include: { emailConfig: true }\n        });\n\n        if (!project) {\n            return NextResponse.json({ error: 'Project not found' }, { status: 404 });\n        }\n\n        // If no config exists yet, return default values\n        if (!project.emailConfig) {\n            return NextResponse.json({\n                enabled: false,\n                smtpHost: '',\n                smtpPort: 587,\n                smtpUser: '',\n                smtpPassword: '',\n                smtpSecure: true,\n                fromEmail: '',\n                fromName: '',\n                replyToEmail: '',\n                defaultSubject: 'New Changelog Update'\n            });\n        }\n\n        // Strip the SMTP password — never send it to the client\n        // eslint-disable-next-line @typescript-eslint/no-unused-vars\n        const { smtpPassword: _, ...safeConfig } = project.emailConfig;\n\n        return NextResponse.json({\n            ...safeConfig,\n            hasPassword: !!project.emailConfig.smtpPassword,\n        });\n    } catch (error) {\n        console.error('Failed to fetch email configuration:', error);\n        return NextResponse.json(\n            { error: 'Failed to fetch email configuration', message: (error as Error).message },\n            { status: 500 }\n        );\n    }\n}\n\n/**\n * @method POST\n * @description Creates or updates email configuration for a project\n */\nexport async function POST(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        await validateAuthAndGetUser();\n        const { projectId } = await context.params;\n\n        // Verify project access\n        const project = await db.project.findUnique({\n            where: { id: projectId },\n        });\n\n        if (!project) {\n            return NextResponse.json({ error: 'Project not found' }, { status: 404 });\n        }\n\n        // Parse and validate request body\n        const body = await request.json();\n        const validatedData = emailConfigSchema.parse(body);\n\n        // Check if existing config to handle password properly\n        const existingConfig = await db.emailConfig.findUnique({\n            where: { projectId }\n        });\n\n        // Prepare data for update or create\n        const data = {\n            ...validatedData,\n            // Handle null/empty values for optional fields\n            smtpUser: validatedData.smtpUser || '',\n            fromName: validatedData.fromName || '',\n            replyToEmail: validatedData.replyToEmail || '',\n            defaultSubject: validatedData.defaultSubject || 'New Changelog Update',\n            // Handle password update logic:\n            // - If password is provided in the request, use it\n            // - If not and there's an existing password, keep it\n            // - Otherwise use empty string\n            smtpPassword: validatedData.smtpPassword\n                ? validatedData.smtpPassword\n                : (existingConfig?.smtpPassword || '')\n        };\n\n        // Update or create email config\n        const emailConfig = await db.emailConfig.upsert({\n            where: { projectId },\n            update: data,\n            create: {\n                ...data,\n                projectId,\n            },\n        });\n\n        // Strip SMTP password — never return secrets to the client\n        // eslint-disable-next-line @typescript-eslint/no-unused-vars\n        const { smtpPassword: _pw, ...safeConfig } = emailConfig;\n\n        return NextResponse.json({\n            ...safeConfig,\n            hasPassword: !!emailConfig.smtpPassword,\n        });\n    } catch (error) {\n        console.error('Failed to update email configuration:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: 'Validation failed', details: error.errors },\n                { status: 400 }\n            );\n        }\n\n        return NextResponse.json(\n            { error: 'Failed to update email configuration', message: (error as Error).message },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/integrations/email/send/route.ts",
    "content": "import {NextResponse} from 'next/server';\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog';\nimport {db} from '@/lib/db';\nimport {z} from 'zod';\nimport {sendChangelogEmail} from '@/lib/services/email/notification';\n\n// Validation schema for send email request\nconst sendEmailSchema = z.object({\n    // Manual recipients array (optional)\n    recipients: z.array(z.string().email('Invalid email address')).optional(),\n\n    // Email subject\n    subject: z.string().min(1, 'Subject is required'),\n\n    // Changelog entry ID (can be 'digest' for digest emails)\n    changelogEntryId: z.string().optional(),\n\n    // Flag for sending a digest instead of a single entry\n    isDigest: z.boolean().default(false),\n\n    // Subscription types to include (if sending to subscribers)\n    subscriptionTypes: z.array(\n        z.enum(['ALL_UPDATES', 'MAJOR_ONLY', 'DIGEST_ONLY'])\n    ).optional()\n});\n\n/**\n * @method POST\n * @description Sends a changelog email to specified recipients or subscribers\n */\nexport async function POST(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        await validateAuthAndGetUser();\n        const {projectId} = await context.params;\n\n        // Verify project access\n        const project = await db.project.findUnique({\n            where: {id: projectId},\n            include: {emailConfig: true}\n        });\n\n        if (!project) {\n            return NextResponse.json({error: 'Project not found'}, {status: 404});\n        }\n\n        if (!project.emailConfig || !project.emailConfig.enabled) {\n            return NextResponse.json({\n                error: 'Email notifications are not properly configured or enabled for this project'\n            }, {status: 400});\n        }\n\n        // Detect custom domain from request headers\n        const host = request.headers.get('host');\n        const appUrl = process.env.NEXT_PUBLIC_APP_URL || '';\n        let customDomain: string | undefined;\n\n        try {\n            const appDomain = new URL(appUrl).hostname;\n            if (host && host !== appDomain && !host.includes('localhost') && !host.includes('127.0.0.1')) {\n                customDomain = host;\n            }\n        } catch (error) {\n            console.error('Error parsing app URL:', error);\n        }\n\n        // Parse and validate request body\n        const body = await request.json();\n        const validatedData = sendEmailSchema.parse(body);\n\n        // Check if we have any recipient specification\n        const hasRecipients = validatedData.recipients && validatedData.recipients.length > 0;\n        const hasSubscriptionTypes = validatedData.subscriptionTypes && validatedData.subscriptionTypes.length > 0;\n\n        if (!hasRecipients && !hasSubscriptionTypes) {\n            return NextResponse.json({\n                error: 'No recipients specified. You must provide either direct recipients or subscription types for subscribers.'\n            }, {status: 400});\n        }\n\n        // If sending to subscribers, fetch subscribers based on subscription types\n        let subscriberIds: string[] = [];\n\n        if (hasSubscriptionTypes) {\n            const subscribers = await db.emailSubscriber.findMany({\n                where: {\n                    isActive: true,\n                    subscriptions: {\n                        some: {\n                            projectId,\n                            subscriptionType: {\n                                in: validatedData.subscriptionTypes\n                            }\n                        }\n                    }\n                },\n                select: {\n                    id: true\n                }\n            });\n\n            subscriberIds = subscribers.map(sub => sub.id);\n        }\n\n        // Send the email using our service\n        const result = await sendChangelogEmail({\n            projectId,\n            subject: validatedData.subject,\n            changelogEntryId: validatedData.isDigest ? undefined : validatedData.changelogEntryId,\n            recipients: validatedData.recipients,\n            isDigest: validatedData.isDigest,\n            subscriberIds: subscriberIds.length > 0 ? subscriberIds : undefined,\n            customDomain\n        });\n\n        return NextResponse.json({\n            success: true,\n            message: `Email sent successfully to ${result.recipientCount} recipients`,\n            recipientCount: result.recipientCount,\n            messageId: result.messageId\n        });\n    } catch (error) {\n        console.error('Failed to send changelog email:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {error: 'Validation failed', details: error.errors},\n                {status: 400}\n            );\n        }\n\n        return NextResponse.json(\n            {\n                error: 'Failed to send changelog email',\n                message: (error instanceof Error) ? error.message : 'Unknown error',\n                stack: process.env.NODE_ENV === 'development' && (error instanceof Error) ? error.stack : undefined\n            },\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/integrations/email/test/route.ts",
    "content": "// app/api/projects/[projectId]/integrations/email/test/route.ts\nimport { NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { db } from '@/lib/db';\nimport nodemailer from 'nodemailer';\nimport { z } from 'zod';\nimport SMTPTransport from 'nodemailer/lib/smtp-transport';\n\n// Validation schema for test request\nconst testSchema = z.object({\n    smtpHost: z.string().min(1, 'SMTP host is required'),\n    smtpPort: z.number().int().min(1).max(65535),\n    smtpUser: z.string().optional().nullable(),\n    smtpPassword: z.string().optional().nullable(),\n    smtpSecure: z.boolean().default(false), // Default to false for local testing\n    fromEmail: z.string().email('Invalid email address'),\n    testEmail: z.string().email('Invalid test email address'),\n});\n\n/**\n * @method POST\n * @description Tests SMTP connection and sends a test email\n */\nexport async function POST(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        await validateAuthAndGetUser();\n        const { projectId } = await context.params;\n\n        // Verify project access\n        const project = await db.project.findUnique({\n            where: { id: projectId },\n        });\n\n        if (!project) {\n            return NextResponse.json({ error: 'Project not found' }, { status: 404 });\n        }\n\n        // Parse and validate request body\n        const body = await request.json();\n        const validatedData = testSchema.parse(body);\n\n        // Log the input values for debugging\n        console.log('Email test configuration:', {\n            host: validatedData.smtpHost,\n            port: validatedData.smtpPort,\n            secure: validatedData.smtpSecure,\n            user: validatedData.smtpUser || '(none)',\n            password: validatedData.smtpPassword ? '(provided)' : '(empty)'\n        });\n\n        // Create a transporter with correct typing for local test servers\n        const transportConfig: SMTPTransport.Options = {\n            host: validatedData.smtpHost,\n            port: validatedData.smtpPort,\n            // Force secure to false for local testing regardless of the input\n            secure: false,\n            // Configure TLS options to avoid certificate validation issues\n            tls: {\n                // Disable certificate validation for testing\n                rejectUnauthorized: false\n            },\n            // Disable STARTTLS for localhost test servers that don't support it\n            ignoreTLS: true,\n            // Set connection timeout to a lower value to fail faster\n            connectionTimeout: 5000\n        };\n\n        // Only add authentication if username is provided\n        if (validatedData.smtpUser && validatedData.smtpUser.trim() !== '') {\n            transportConfig.auth = {\n                user: validatedData.smtpUser,\n                pass: validatedData.smtpPassword || ''\n            };\n        }\n\n        console.log('Final transport configuration:', JSON.stringify(transportConfig, null, 2));\n\n        const transporter = nodemailer.createTransport(transportConfig);\n\n        try {\n            // Add debug logging\n            console.log('Verifying connection...');\n\n            // Test the connection\n            await transporter.verify();\n            console.log('Connection verified successfully');\n\n            // Send a test email\n            console.log('Sending test email...');\n            const info = await transporter.sendMail({\n                from: `\"${project.name}\" <${validatedData.fromEmail}>`,\n                to: validatedData.testEmail,\n                subject: \"Test Email from Changerawr\",\n                text: `This is a test email from Changerawr for project ${project.name}.`,\n                html: `\n                  <div style=\"font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;\">\n                    <h1 style=\"color: #333;\">Test Email from Changerawr</h1>\n                    <p>This is a test email from the Changerawr changelog system.</p>\n                    <p>Project: <strong>${project.name}</strong></p>\n                    <hr style=\"border: 1px solid #eee; margin: 20px 0;\" />\n                    <p style=\"color: #666; font-size: 12px;\">This email was sent to verify your SMTP configuration.</p>\n                  </div>\n                `,\n            });\n\n            console.log('Test email sent:', info.messageId);\n\n            // Update the last tested timestamp and status\n            await db.emailConfig.upsert({\n                where: { projectId },\n                update: {\n                    lastTestedAt: new Date(),\n                    testStatus: 'success',\n                },\n                create: {\n                    projectId,\n                    enabled: false,\n                    smtpHost: validatedData.smtpHost,\n                    smtpPort: validatedData.smtpPort,\n                    smtpUser: validatedData.smtpUser || '',\n                    smtpPassword: validatedData.smtpPassword || '',\n                    smtpSecure: false, // Force save as false for local testing\n                    fromEmail: validatedData.fromEmail,\n                    lastTestedAt: new Date(),\n                    testStatus: 'success',\n                },\n            });\n\n            return NextResponse.json({\n                success: true,\n                message: 'Connection successful and test email sent',\n                messageId: info.messageId,\n            });\n\n        } catch (error) {\n            console.error('Email test error:', error);\n\n            // Update test status with error message\n            await db.emailConfig.upsert({\n                where: { projectId },\n                update: {\n                    lastTestedAt: new Date(),\n                    testStatus: error instanceof Error ? `failed: ${error.message}` : 'failed: unknown error',\n                },\n                create: {\n                    projectId,\n                    enabled: false,\n                    smtpHost: validatedData.smtpHost,\n                    smtpPort: validatedData.smtpPort,\n                    smtpUser: validatedData.smtpUser || '',\n                    smtpPassword: validatedData.smtpPassword || '',\n                    smtpSecure: false, // Force save as false\n                    fromEmail: validatedData.fromEmail,\n                    lastTestedAt: new Date(),\n                    testStatus: error instanceof Error ? `failed: ${error.message}` : 'failed: unknown error',\n                },\n            });\n\n            return NextResponse.json({\n                success: false,\n                message: 'Failed to connect or send test email',\n                error: (error as Error).message,\n                stack: process.env.NODE_ENV === 'development' ? (error as Error).stack : undefined,\n                // Add additional troubleshooting info\n                troubleshooting: [\n                    \"1. Make sure your mail server is running\",\n                    \"2. Try using port 25 instead of 2525 if your server runs on a different port\",\n                    \"3. Check if the mail server requires SSL/TLS\",\n                    \"4. Verify the username format is correct for your mail server\"\n                ]\n            }, { status: 400 });\n        }\n    } catch (error) {\n        console.error('Failed to test email connection:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: 'Validation failed', details: error.errors },\n                { status: 400 }\n            );\n        }\n\n        return NextResponse.json(\n            { error: 'Failed to test email connection', message: (error as Error).message },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/integrations/github/generate/route.ts",
    "content": "// app/api/projects/[projectId]/integrations/github/generate/route.ts\nimport { NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { createAuditLog } from '@/lib/utils/auditLog';\nimport { db } from '@/lib/db';\nimport { z } from 'zod';\nimport { createGitHubClient, GitHubError } from '@/lib/services/github/client';\nimport { createGitHubChangelogGenerator, ChangelogGenerationOptions } from '@/lib/services/github/changelog-generator';\nimport { decryptToken } from '@/lib/utils/encryption';\n\n// Enhanced validation schema for generation options\nconst generateSchema = z.object({\n    // Generation method\n    method: z.enum(['recent', 'between_tags', 'between_commits']),\n\n    // For recent method\n    daysBack: z.number().min(1).max(365).optional(),\n\n    // For between methods\n    fromRef: z.string().optional(),\n    toRef: z.string().optional(),\n\n    // AI options\n    useAI: z.boolean().default(false),\n    aiModel: z.string().optional(),\n\n    // Code analysis options\n    includeCodeAnalysis: z.boolean().default(false),\n    maxCommitsToAnalyze: z.number().min(1).max(100).default(50),\n\n    // Traditional generation options\n    groupByType: z.boolean().default(true),\n    includeCommitLinks: z.boolean().default(true),\n\n    // Override integration settings\n    includeBreakingChanges: z.boolean().optional(),\n    includeFixes: z.boolean().optional(),\n    includeFeatures: z.boolean().optional(),\n    includeChores: z.boolean().optional(),\n    customCommitTypes: z.array(z.string()).optional(),\n});\n\n/**\n * @method POST\n * @description Generate changelog content from GitHub commits with optional AI analysis\n */\nexport async function POST(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n        const { projectId } = await context.params;\n\n        console.log('Starting GitHub changelog generation for project:', projectId);\n\n        // Get project with GitHub integration\n        const project = await db.project.findUnique({\n            where: { id: projectId },\n            include: { gitHubIntegration: true }\n        });\n\n        if (!project) {\n            console.error('Project not found:', projectId);\n            return NextResponse.json({ error: 'Project not found' }, { status: 404 });\n        }\n\n        if (!project.gitHubIntegration) {\n            console.error('No GitHub integration found for project:', projectId);\n            return NextResponse.json(\n                { error: 'GitHub integration not configured for this project' },\n                { status: 400 }\n            );\n        }\n\n        if (!project.gitHubIntegration.enabled) {\n            console.error('GitHub integration disabled for project:', projectId);\n            return NextResponse.json(\n                { error: 'GitHub integration is disabled' },\n                { status: 400 }\n            );\n        }\n\n        // Parse and validate request body\n        const body = await request.json();\n        console.log('Request body:', body);\n\n        const validatedData = generateSchema.parse(body);\n        console.log('Validated data:', validatedData);\n\n        // Validate method-specific requirements\n        if (validatedData.method === 'recent' && !validatedData.daysBack) {\n            return NextResponse.json(\n                { error: 'daysBack is required for recent method' },\n                { status: 400 }\n            );\n        }\n\n        if (\n            (validatedData.method === 'between_tags' || validatedData.method === 'between_commits') &&\n            (!validatedData.fromRef || !validatedData.toRef)\n        ) {\n            return NextResponse.json(\n                { error: 'fromRef and toRef are required for between methods' },\n                { status: 400 }\n            );\n        }\n\n        // Decrypt access token\n        let accessToken: string;\n        try {\n            accessToken = decryptToken(project.gitHubIntegration.accessToken);\n            console.log('Access token decrypted successfully');\n        } catch (decryptError) {\n            console.error('Failed to decrypt access token:', decryptError);\n            return NextResponse.json(\n                { error: 'Invalid access token configuration' },\n                { status: 500 }\n            );\n        }\n\n        // Create GitHub client and generator\n        const githubClient = createGitHubClient({\n            accessToken,\n            repositoryUrl: project.gitHubIntegration.repositoryUrl,\n            defaultBranch: project.gitHubIntegration.defaultBranch\n        });\n\n        const generator = createGitHubChangelogGenerator(githubClient);\n\n        // Get AI settings if needed\n        let aiApiKey: string | undefined;\n        let aiModel: string | undefined;\n        if (validatedData.useAI) {\n            const systemConfig = await db.systemConfig.findFirst({\n                where: { id: 1 },\n                select: {\n                    aiApiKey: true,\n                    enableAIAssistant: true,\n                    aiDefaultModel: true\n                }\n            });\n\n            if (!systemConfig?.enableAIAssistant || !systemConfig.aiApiKey) {\n                return NextResponse.json(\n                    { error: 'AI assistant is not enabled or configured' },\n                    { status: 400 }\n                );\n            }\n\n            aiApiKey = systemConfig.aiApiKey;\n            aiModel = validatedData.aiModel || systemConfig.aiDefaultModel || 'copilot-zero';\n        }\n\n        // Prepare generation options\n        const generationOptions: ChangelogGenerationOptions = {\n            includeBreakingChanges: validatedData.includeBreakingChanges ?? project.gitHubIntegration.includeBreakingChanges,\n            includeFixes: validatedData.includeFixes ?? project.gitHubIntegration.includeFixes,\n            includeFeatures: validatedData.includeFeatures ?? project.gitHubIntegration.includeFeatures,\n            includeChores: validatedData.includeChores ?? project.gitHubIntegration.includeChores,\n            customCommitTypes: validatedData.customCommitTypes ?? project.gitHubIntegration.customCommitTypes,\n            useAI: validatedData.useAI,\n            aiApiKey,\n            aiModel,\n            groupByType: validatedData.groupByType,\n            includeCommitLinks: validatedData.includeCommitLinks,\n            repositoryUrl: project.gitHubIntegration.repositoryUrl,\n            includeCodeAnalysis: validatedData.includeCodeAnalysis,\n            maxCommitsToAnalyze: validatedData.maxCommitsToAnalyze\n        };\n\n        console.log('Generation options:', {\n            ...generationOptions,\n            aiApiKey: generationOptions.aiApiKey ? '[REDACTED]' : undefined\n        });\n\n        // Log generation attempt\n        await createAuditLog(\n            'GITHUB_CHANGELOG_GENERATION_ATTEMPT',\n            user.id,\n            user.id,\n            {\n                projectId,\n                projectName: project.name,\n                method: validatedData.method,\n                daysBack: validatedData.daysBack,\n                fromRef: validatedData.fromRef,\n                toRef: validatedData.toRef,\n                useAI: validatedData.useAI,\n                includeCodeAnalysis: validatedData.includeCodeAnalysis,\n                maxCommitsToAnalyze: validatedData.maxCommitsToAnalyze,\n                repositoryUrl: project.gitHubIntegration.repositoryUrl,\n                timestamp: new Date().toISOString()\n            }\n        );\n\n        // Generate changelog based on method\n        let changelog;\n        try {\n            console.log(`Generating changelog using method: ${validatedData.method}`);\n\n            switch (validatedData.method) {\n                case 'recent':\n                    console.log(`Fetching commits from last ${validatedData.daysBack} days`);\n                    changelog = await generator.generateChangelogFromRecent(\n                        project.gitHubIntegration.repositoryUrl,\n                        validatedData.daysBack!,\n                        generationOptions\n                    );\n                    break;\n\n                case 'between_tags':\n                case 'between_commits':\n                    console.log(`Fetching commits between ${validatedData.fromRef} and ${validatedData.toRef}`);\n                    changelog = await generator.generateChangelogBetweenRefs(\n                        project.gitHubIntegration.repositoryUrl,\n                        validatedData.fromRef!,\n                        validatedData.toRef!,\n                        generationOptions\n                    );\n                    break;\n\n                default:\n                    throw new Error('Invalid generation method');\n            }\n\n            console.log(`Successfully generated changelog with ${changelog.commits.length} commits`);\n\n        } catch (error) {\n            console.error('Changelog generation error:', error);\n\n            // Log generation failure with more details\n            await createAuditLog(\n                'GITHUB_CHANGELOG_GENERATION_FAILED',\n                user.id,\n                user.id,\n                {\n                    projectId,\n                    method: validatedData.method,\n                    repositoryUrl: project.gitHubIntegration.repositoryUrl,\n                    useAI: validatedData.useAI,\n                    includeCodeAnalysis: validatedData.includeCodeAnalysis,\n                    error: error instanceof Error ? error.message : 'Unknown error',\n                    stack: error instanceof Error ? error.stack : undefined,\n                    timestamp: new Date().toISOString()\n                }\n            );\n\n            // Provide better error messages based on error type\n            if (error instanceof GitHubError) {\n                if (error.statusCode === 404) {\n                    return NextResponse.json(\n                        {\n                            error: 'Repository, branch, or tag not found',\n                            details: 'Please check that the repository URL and references are correct'\n                        },\n                        { status: 404 }\n                    );\n                }\n\n                if (error.statusCode === 403 || error.statusCode === 401) {\n                    return NextResponse.json(\n                        {\n                            error: 'GitHub access denied',\n                            details: 'Your access token may be invalid or lack necessary permissions'\n                        },\n                        { status: 401 }\n                    );\n                }\n\n                if (error.message.includes('rate limit')) {\n                    return NextResponse.json(\n                        {\n                            error: 'GitHub API rate limit exceeded',\n                            details: 'Please try again later or use a different access token'\n                        },\n                        { status: 429 }\n                    );\n                }\n\n                return NextResponse.json(\n                    {\n                        error: 'GitHub API error',\n                        details: error.message\n                    },\n                    { status: error.statusCode }\n                );\n            }\n\n            if (error instanceof Error) {\n                return NextResponse.json(\n                    {\n                        error: 'Changelog generation failed',\n                        details: error.message\n                    },\n                    { status: 500 }\n                );\n            }\n\n            throw error;\n        }\n\n        // Update last sync time\n        await db.gitHubIntegration.update({\n            where: { projectId },\n            data: {\n                lastSyncAt: new Date(),\n                lastCommitSha: changelog.commits[0]?.sha\n            }\n        });\n\n        // Get current timestamp for logging\n        const currentTimestamp = new Date().toISOString();\n\n        // Log successful generation\n        await createAuditLog(\n            'GITHUB_CHANGELOG_GENERATION_SUCCESS',\n            user.id,\n            user.id,\n            {\n                projectId,\n                projectName: project.name,\n                method: validatedData.method,\n                commitsProcessed: changelog.commits.length,\n                entriesGenerated: changelog.entries.length,\n                contentLength: changelog.content.length,\n                inferredVersion: changelog.version,\n                useAI: validatedData.useAI,\n                aiGenerated: changelog.metadata.aiGenerated,\n                hasCodeAnalysis: changelog.metadata.hasCodeAnalysis,\n                model: changelog.metadata.model,\n                timestamp: currentTimestamp\n            }\n        );\n\n        // Prepare response\n        const response = {\n            success: true,\n            changelog: {\n                content: changelog.content,\n                version: changelog.version,\n                commitsCount: changelog.commits.length,\n                entriesCount: changelog.entries.length,\n                entries: changelog.entries.map(entry => ({\n                    category: entry.category,\n                    description: entry.description,\n                    impact: entry.impact,\n                    technicalDetails: entry.technicalDetails,\n                    files: entry.files,\n                    commit: entry.commit.substring(0, 7)\n                }))\n            },\n            metadata: {\n                method: validatedData.method,\n                generatedAt: currentTimestamp,\n                repositoryUrl: project.gitHubIntegration.repositoryUrl,\n                fromRef: validatedData.fromRef,\n                toRef: validatedData.toRef,\n                daysBack: validatedData.daysBack,\n                aiEnhanced: validatedData.useAI,\n                codeAnalysis: validatedData.includeCodeAnalysis,\n                totalCommits: changelog.metadata.totalCommits,\n                analyzedCommits: changelog.metadata.analyzedCommits,\n                aiGenerated: changelog.metadata.aiGenerated,\n                hasCodeAnalysis: changelog.metadata.hasCodeAnalysis,\n                model: changelog.metadata.model\n            }\n        };\n\n        return NextResponse.json(response);\n\n    } catch (error) {\n        console.error('Failed to generate changelog from GitHub:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: 'Validation failed', details: error.errors },\n                { status: 400 }\n            );\n        }\n\n        // Handle other errors\n        return NextResponse.json(\n            {\n                error: 'Failed to generate changelog from GitHub',\n                details: error instanceof Error ? error.message : 'Unknown error'\n            },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/integrations/github/route.ts",
    "content": "// app/api/projects/[projectId]/integrations/github/route.ts\nimport { NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { createAuditLog } from '@/lib/utils/auditLog';\nimport { db } from '@/lib/db';\nimport { z } from 'zod';\nimport { createGitHubClient } from '@/lib/services/github/client';\nimport { encryptToken, decryptToken } from '@/lib/utils/encryption';\n\n// Validation schema for GitHub integration\nconst githubIntegrationSchema = z.object({\n    repositoryUrl: z.string().url('Invalid repository URL'),\n    accessToken: z.string().min(1, 'Access token is required').optional(), // Make optional for updates\n    defaultBranch: z.string().default('main'),\n    includeBreakingChanges: z.boolean().default(true),\n    includeFixes: z.boolean().default(true),\n    includeFeatures: z.boolean().default(true),\n    includeChores: z.boolean().default(false),\n    customCommitTypes: z.array(z.string()).default([]),\n    enabled: z.boolean().default(true),\n});\n\n/**\n * @method GET\n * @description Get GitHub integration settings for a project\n */\nexport async function GET(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        await validateAuthAndGetUser();\n        const { projectId } = await context.params;\n\n        const project = await db.project.findUnique({\n            where: { id: projectId },\n            include: { gitHubIntegration: true }\n        });\n\n        if (!project) {\n            return NextResponse.json({ error: 'Project not found' }, { status: 404 });\n        }\n\n        // Return settings without sensitive data\n        if (project.gitHubIntegration) {\n            const { accessToken, ...safeSettings } = project.gitHubIntegration;\n            return NextResponse.json({\n                ...safeSettings,\n                hasAccessToken: !!accessToken\n            });\n        }\n\n        return NextResponse.json(null);\n\n    } catch (error) {\n        console.error('Failed to fetch GitHub integration:', error);\n        return NextResponse.json(\n            { error: 'Failed to fetch integration settings' },\n            { status: 500 }\n        );\n    }\n}\n\n/**\n * @method POST\n * @description Create or update GitHub integration\n */\nexport async function POST(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n        const { projectId } = await context.params;\n\n        // Verify project exists\n        const project = await db.project.findUnique({\n            where: { id: projectId }\n        });\n\n        if (!project) {\n            return NextResponse.json({ error: 'Project not found' }, { status: 404 });\n        }\n\n        // Parse and validate request body\n        const body = await request.json();\n        const validatedData = githubIntegrationSchema.parse(body);\n\n        // Check if we have an existing integration\n        const existingIntegration = await db.gitHubIntegration.findUnique({\n            where: { projectId }\n        });\n\n        // For updates without a new token, use the existing token\n        let accessTokenToTest = validatedData.accessToken;\n        let encryptedTokenToStore: string;\n\n        if (!validatedData.accessToken && existingIntegration) {\n            // No new token provided, use existing one for testing\n            accessTokenToTest = decryptToken(existingIntegration.accessToken);\n            encryptedTokenToStore = existingIntegration.accessToken; // Keep existing encrypted token\n            console.log('Using existing access token for update');\n        } else if (validatedData.accessToken) {\n            // New token provided, encrypt it\n            encryptedTokenToStore = encryptToken(validatedData.accessToken);\n            console.log('Using new access token');\n        } else {\n            // No token at all and no existing integration\n            return NextResponse.json(\n                { error: 'Access token is required for new integrations' },\n                { status: 400 }\n            );\n        }\n\n        // Test GitHub connection with the token we're going to use\n        const githubClient = createGitHubClient({\n            accessToken: accessTokenToTest as string,\n            repositoryUrl: validatedData.repositoryUrl,\n            defaultBranch: validatedData.defaultBranch\n        });\n\n        try {\n            const repoInfo = await githubClient.testConnection(validatedData.repositoryUrl);\n            console.log('GitHub connection test successful:', repoInfo.name);\n        } catch (githubError) {\n            console.error('GitHub connection test failed:', githubError);\n            return NextResponse.json(\n                {\n                    error: 'Failed to connect to GitHub repository',\n                    details: githubError instanceof Error ? githubError.message : 'Unknown error'\n                },\n                { status: 400 }\n            );\n        }\n\n        // Create or update integration\n        const integration = await db.gitHubIntegration.upsert({\n            where: { projectId },\n            create: {\n                projectId,\n                repositoryUrl: validatedData.repositoryUrl,\n                accessToken: encryptedTokenToStore,\n                defaultBranch: validatedData.defaultBranch,\n                includeBreakingChanges: validatedData.includeBreakingChanges,\n                includeFixes: validatedData.includeFixes,\n                includeFeatures: validatedData.includeFeatures,\n                includeChores: validatedData.includeChores,\n                customCommitTypes: validatedData.customCommitTypes,\n                enabled: validatedData.enabled,\n            },\n            update: {\n                repositoryUrl: validatedData.repositoryUrl,\n                accessToken: encryptedTokenToStore, // Only update if new token provided\n                defaultBranch: validatedData.defaultBranch,\n                includeBreakingChanges: validatedData.includeBreakingChanges,\n                includeFixes: validatedData.includeFixes,\n                includeFeatures: validatedData.includeFeatures,\n                includeChores: validatedData.includeChores,\n                customCommitTypes: validatedData.customCommitTypes,\n                enabled: validatedData.enabled,\n                updatedAt: new Date(),\n            }\n        });\n\n        // Log integration setup\n        await createAuditLog(\n            'GITHUB_INTEGRATION_CONFIGURED',\n            user.id,\n            user.id,\n            {\n                projectId,\n                projectName: project.name,\n                repositoryUrl: validatedData.repositoryUrl,\n                defaultBranch: validatedData.defaultBranch,\n                enabled: validatedData.enabled,\n                timestamp: new Date().toISOString()\n            }\n        );\n\n        // Return safe data\n        // eslint-disable-next-line @typescript-eslint/no-unused-vars\n        const { accessToken, ...safeIntegration } = integration;\n        return NextResponse.json({\n            ...safeIntegration,\n            hasAccessToken: true\n        });\n\n    } catch (error) {\n        console.error('Failed to configure GitHub integration:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: 'Validation failed', details: error.errors },\n                { status: 400 }\n            );\n        }\n\n        return NextResponse.json(\n            { error: 'Failed to configure GitHub integration' },\n            { status: 500 }\n        );\n    }\n}\n\n/**\n * @method DELETE\n * @description Remove GitHub integration\n */\nexport async function DELETE(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser();\n        const { projectId } = await context.params;\n\n        const deleted = await db.gitHubIntegration.delete({\n            where: { projectId }\n        });\n\n        await createAuditLog(\n            'GITHUB_INTEGRATION_REMOVED',\n            user.id,\n            user.id,\n            {\n                projectId,\n                repositoryUrl: deleted.repositoryUrl,\n                timestamp: new Date().toISOString()\n            }\n        );\n\n        return NextResponse.json({ success: true });\n\n    } catch (error) {\n        console.error('Failed to remove GitHub integration:', error);\n        return NextResponse.json(\n            { error: 'Failed to remove GitHub integration' },\n            { status: 500 }\n        );\n    }\n}\n"
  },
  {
    "path": "app/api/projects/[projectId]/integrations/github/tags/route.ts",
    "content": "// app/api/projects/[projectId]/integrations/github/tags/route.ts\nimport { NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { db } from '@/lib/db';\nimport { createGitHubClient } from '@/lib/services/github/client';\nimport { createGitHubChangelogGenerator } from '@/lib/services/github/changelog-generator';\nimport { decryptToken } from '@/lib/utils/encryption';\n\n/**\n * @method GET\n * @description Get available tags and releases from GitHub repository\n */\nexport async function GET(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        await validateAuthAndGetUser();\n        const { projectId } = await context.params;\n\n        console.log('Fetching GitHub tags for project:', projectId);\n\n        // Get project with GitHub integration\n        const project = await db.project.findUnique({\n            where: { id: projectId },\n            include: { gitHubIntegration: true }\n        });\n\n        if (!project) {\n            console.error('Project not found:', projectId);\n            return NextResponse.json({ error: 'Project not found' }, { status: 404 });\n        }\n\n        if (!project.gitHubIntegration) {\n            console.error('No GitHub integration found for project:', projectId);\n            return NextResponse.json(\n                { error: 'GitHub integration not configured' },\n                { status: 400 }\n            );\n        }\n\n        if (!project.gitHubIntegration.enabled) {\n            console.error('GitHub integration disabled for project:', projectId);\n            return NextResponse.json(\n                { error: 'GitHub integration is disabled' },\n                { status: 400 }\n            );\n        }\n\n        if (!project.gitHubIntegration.accessToken) {\n            console.error('No access token configured for project:', projectId);\n            return NextResponse.json(\n                { error: 'No access token configured' },\n                { status: 400 }\n            );\n        }\n\n        console.log('GitHub integration config:', {\n            repositoryUrl: project.gitHubIntegration.repositoryUrl,\n            defaultBranch: project.gitHubIntegration.defaultBranch,\n            enabled: project.gitHubIntegration.enabled\n        });\n\n        // Decrypt access token and create client\n        let accessToken: string;\n        try {\n            accessToken = decryptToken(project.gitHubIntegration.accessToken);\n            console.log('Access token decrypted successfully');\n        } catch (decryptError) {\n            console.error('Failed to decrypt access token:', decryptError);\n            return NextResponse.json(\n                { error: 'Invalid access token configuration' },\n                { status: 500 }\n            );\n        }\n\n        const githubClient = createGitHubClient({\n            accessToken,\n            repositoryUrl: project.gitHubIntegration.repositoryUrl,\n            defaultBranch: project.gitHubIntegration.defaultBranch\n        });\n\n        const generator = createGitHubChangelogGenerator(githubClient);\n\n        try {\n            console.log('Fetching tags and releases from GitHub...');\n\n            // Get tags and releases in parallel\n            const [tags, releases] = await Promise.all([\n                generator.getAvailableTags(project.gitHubIntegration.repositoryUrl).catch(error => {\n                    console.error('Failed to fetch tags:', error);\n                    return [];\n                }),\n                generator.getAvailableReleases(project.gitHubIntegration.repositoryUrl).catch(error => {\n                    console.error('Failed to fetch releases:', error);\n                    return [];\n                })\n            ]);\n\n            console.log('Successfully fetched:', {\n                tagsCount: tags.length,\n                releasesCount: releases.length\n            });\n\n            return NextResponse.json({\n                tags: tags || [],\n                releases: releases || []\n            });\n\n        } catch (githubError) {\n            console.error('GitHub API Error:', githubError);\n\n            // Provide more specific error messages\n            if (githubError instanceof Error) {\n                if (githubError.message.includes('404')) {\n                    return NextResponse.json(\n                        {\n                            error: 'Repository not found',\n                            details: 'The repository URL may be incorrect or the token may not have access'\n                        },\n                        { status: 404 }\n                    );\n                }\n\n                if (githubError.message.includes('401')) {\n                    return NextResponse.json(\n                        {\n                            error: 'Authentication failed',\n                            details: 'The access token is invalid or expired'\n                        },\n                        { status: 401 }\n                    );\n                }\n\n                if (githubError.message.includes('403')) {\n                    return NextResponse.json(\n                        {\n                            error: 'Access forbidden',\n                            details: 'The access token does not have permission to access this repository'\n                        },\n                        { status: 403 }\n                    );\n                }\n            }\n\n            return NextResponse.json(\n                {\n                    error: 'Failed to fetch repository data from GitHub',\n                    details: githubError instanceof Error ? githubError.message : 'Unknown error'\n                },\n                { status: 500 }\n            );\n        }\n\n    } catch (error) {\n        console.error('Failed to fetch GitHub tags/releases:', error);\n        return NextResponse.json(\n            {\n                error: 'Failed to fetch repository information',\n                details: error instanceof Error ? error.message : 'Unknown error'\n            },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/integrations/github/test/route.ts",
    "content": "import {NextResponse} from 'next/server';\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog';\nimport {z} from 'zod';\nimport {createGitHubClient} from '@/lib/services/github/client';\n\nconst testSchema = z.object({\n    repositoryUrl: z.string().url('Invalid repository URL'),\n    accessToken: z.string().min(1, 'Access token is required'),\n});\n\n/**\n * @method POST\n * @description Test GitHub connection with provided credentials\n */\nexport async function POST(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        await validateAuthAndGetUser();\n        // eslint-disable-next-line @typescript-eslint/no-unused-vars\n        const {projectId} = await context.params;\n\n        const body = await request.json();\n        // console.log('Test request body:', {\n        //     hasRepositoryUrl: !!body.repositoryUrl,\n        //     hasAccessToken: !!body.accessToken,\n        //     repositoryUrl: body.repositoryUrl,\n        //     projectId: projectId\n        // });\n\n        const validatedData = testSchema.parse(body);\n\n        // Create GitHub client\n        const githubClient = createGitHubClient({\n            accessToken: validatedData.accessToken,\n            repositoryUrl: validatedData.repositoryUrl,\n        });\n\n        // console.log('Testing GitHub connection...');\n        // console.log('Repository URL:', validatedData.repositoryUrl);\n        // console.log('Token starts with:', validatedData.accessToken.substring(0, 8) + '...');\n\n        try {\n            // Test 1: Check user authentication\n            // console.log('Step 1: Testing user authentication');\n            const user = await githubClient.getUser();\n            // console.log('Authenticated as:', user.login);\n\n            // Test 2: Check repository access\n            // console.log('Step 2: Testing repository access');\n            const repo = await githubClient.testConnection(validatedData.repositoryUrl);\n            // console.log('Repository accessed:', repo.full_name);\n\n            // Test 3: Try to fetch a few commits\n            // console.log('Step 3: Testing commit access');\n            const commits = await githubClient.getCommits(validatedData.repositoryUrl, {per_page: 5});\n            // console.log('Fetched commits:', commits.length);\n\n            return NextResponse.json({\n                success: true,\n                user: {\n                    login: user.login,\n                    id: user.id\n                },\n                repository: {\n                    name: repo.name,\n                    full_name: repo.full_name,\n                    private: repo.private,\n                    default_branch: repo.default_branch\n                },\n                commitsCount: commits.length,\n                message: 'GitHub connection successful'\n            });\n\n        } catch (githubError) {\n            console.error('GitHub API Error:', githubError);\n\n            let errorMessage = 'GitHub connection failed';\n            let statusCode = 400;\n\n            if (githubError instanceof Error) {\n                if (githubError.message.includes('Bad credentials')) {\n                    errorMessage = 'Invalid access token. Please check your GitHub personal access token.';\n                    statusCode = 401;\n                } else if (githubError.message.includes('Not Found')) {\n                    errorMessage = 'Repository not found or access denied. Check the repository URL and token permissions.';\n                    statusCode = 404;\n                } else if (githubError.message.includes('rate limit')) {\n                    errorMessage = 'GitHub API rate limit exceeded. Please try again later.';\n                    statusCode = 429;\n                } else {\n                    errorMessage = githubError.message;\n                }\n            }\n\n            return NextResponse.json(\n                {\n                    success: false,\n                    error: errorMessage,\n                    details: githubError instanceof Error ? githubError.message : 'Unknown error'\n                },\n                {status: statusCode}\n            );\n        }\n\n    } catch (error) {\n        console.error('Test route error:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {\n                    success: false,\n                    error: 'Validation failed',\n                    details: error.errors.map(e => `${e.path.join('.')}: ${e.message}`)\n                },\n                {status: 400}\n            );\n        }\n\n        return NextResponse.json(\n            {\n                success: false,\n                error: 'Failed to test GitHub connection',\n                details: error instanceof Error ? error.message : 'Unknown error'\n            },\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/integrations/slack/channels/route.ts",
    "content": "import {NextResponse} from 'next/server'\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog'\nimport {db} from '@/lib/db'\nimport {decryptToken} from '@/lib/utils/encryption'\n\n/**\n * GET /api/projects/[projectId]/integrations/slack/channels\n * Fetch available channels from connected Slack workspace\n */\nexport async function GET(\n    req: Request,\n    {params}: {params: Promise<{projectId: string}>}\n) {\n    try {\n        await validateAuthAndGetUser()\n\n        const {projectId} = await params\n\n        // Get the Slack integration for this project\n        const integration = await db.slackIntegration.findUnique({\n            where: {projectId},\n            select: {\n                accessToken: true,\n            },\n        })\n\n        if (!integration || !integration.accessToken) {\n            return NextResponse.json(\n                {error: 'Slack integration not connected'},\n                {status: 400}\n            )\n        }\n\n        // Decrypt the access token\n        let decryptedToken: string\n        try {\n            decryptedToken = decryptToken(integration.accessToken)\n        } catch (error) {\n            console.error('Failed to decrypt Slack access token:', error)\n            return NextResponse.json(\n                {error: 'Failed to decrypt access token'},\n                {status: 500}\n            )\n        }\n\n        // Fetch channels from Slack API\n        const channelsResponse = await fetch('https://slack.com/api/conversations.list', {\n            method: 'POST',\n            headers: {\n                'Authorization': `Bearer ${decryptedToken}`,\n                'Content-Type': 'application/x-www-form-urlencoded',\n            },\n            body: new URLSearchParams({\n                types: 'public_channel,private_channel',\n                exclude_archived: 'true',\n                limit: '100',\n            }).toString(),\n        })\n\n        if (!channelsResponse.ok) {\n            return NextResponse.json(\n                {error: 'Failed to fetch channels from Slack'},\n                {status: 500}\n            )\n        }\n\n        const channelsData = await channelsResponse.json()\n\n        if (!channelsData.ok) {\n            return NextResponse.json(\n                {error: channelsData.error || 'Failed to fetch channels'},\n                {status: 400}\n            )\n        }\n\n        // Format channels for the frontend\n        const channels = (channelsData.channels || []).map((channel: any) => ({\n            id: channel.id,\n            name: channel.name,\n            isPrivate: channel.is_private,\n            isMember: channel.is_member,\n            topic: channel.topic?.value || '',\n        }))\n\n        return NextResponse.json({channels})\n    } catch (error) {\n        console.error('Error fetching Slack channels:', error)\n        return NextResponse.json(\n            {error: 'Internal server error'},\n            {status: 500}\n        )\n    }\n}\n"
  },
  {
    "path": "app/api/projects/[projectId]/integrations/slack/route.ts",
    "content": "import {NextResponse} from 'next/server'\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog'\nimport {db} from '@/lib/db'\n\n/**\n * GET /api/projects/[projectId]/integrations/slack\n * Fetch Slack integration for a project\n */\nexport async function GET(\n    req: Request,\n    {params}: {params: Promise<{projectId: string}>}\n) {\n    try {\n        await validateAuthAndGetUser()\n\n        const {projectId} = await params\n\n        const integration = await db.slackIntegration.findUnique({\n            where: {projectId},\n            select: {\n                id: true,\n                projectId: true,\n                teamId: true,\n                teamName: true,\n                botUserId: true,\n                botUsername: true,\n                channelId: true,\n                channelName: true,\n                autoSend: true,\n                enabled: true,\n                lastSyncAt: true,\n                lastErrorMessage: true,\n                postCount: true,\n                createdAt: true,\n                updatedAt: true,\n            },\n        })\n\n        if (!integration) {\n            return NextResponse.json(\n                {error: 'Slack integration not found'},\n                {status: 404}\n            )\n        }\n\n        return NextResponse.json(integration)\n    } catch (error) {\n        console.error('Error fetching Slack integration:', error)\n        return NextResponse.json(\n            {error: 'Internal server error'},\n            {status: 500}\n        )\n    }\n}\n\n/**\n * PUT /api/projects/[projectId]/integrations/slack\n * Update Slack integration settings\n */\nexport async function PUT(\n    req: Request,\n    {params}: {params: Promise<{projectId: string}>}\n) {\n    try {\n        await validateAuthAndGetUser()\n\n        const {projectId} = await params\n        const body = await req.json()\n        const {channelId, channelName, autoSend, enabled} = body\n\n        // Validate channel ID\n        if (!channelId || typeof channelId !== 'string') {\n            return NextResponse.json(\n                {error: 'Channel ID is required'},\n                {status: 400}\n            )\n        }\n\n        const integration = await db.slackIntegration.update({\n            where: {projectId},\n            data: {\n                channelId,\n                channelName: channelName || null,\n                autoSend: autoSend ?? true,\n                enabled: enabled ?? true,\n            },\n            select: {\n                id: true,\n                projectId: true,\n                teamId: true,\n                teamName: true,\n                botUserId: true,\n                botUsername: true,\n                channelId: true,\n                channelName: true,\n                autoSend: true,\n                enabled: true,\n                lastSyncAt: true,\n                lastErrorMessage: true,\n                postCount: true,\n                createdAt: true,\n                updatedAt: true,\n            },\n        })\n\n        return NextResponse.json(integration)\n    } catch (error) {\n        console.error('Error updating Slack integration:', error)\n        return NextResponse.json(\n            {error: 'Internal server error'},\n            {status: 500}\n        )\n    }\n}\n\n/**\n * DELETE /api/projects/[projectId]/integrations/slack\n * Disconnect Slack integration\n */\nexport async function DELETE(\n    req: Request,\n    {params}: {params: Promise<{projectId: string}>}\n) {\n    try {\n        await validateAuthAndGetUser()\n\n        const {projectId} = await params\n\n        await db.slackIntegration.delete({\n            where: {projectId},\n        })\n\n        return NextResponse.json({success: true})\n    } catch (error) {\n        console.error('Error deleting Slack integration:', error)\n        return NextResponse.json(\n            {error: 'Internal server error'},\n            {status: 500}\n        )\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/route.ts",
    "content": "import { db } from '@/lib/db'\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog'\nimport { z } from 'zod'\n\nconst projectUpdateSchema = z.object({\n    name: z.string().min(1).max(200).optional(),\n    isPublic: z.boolean().optional(),\n    allowAutoPublish: z.boolean().optional(),\n    requireApproval: z.boolean().optional(),\n    defaultTags: z.array(z.string()).optional(),\n})\n\n/**\n * @method GET\n * @description Fetches a specific project, including its changelog and tags\n * @path {projectId}\n * @query {}\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"name\": { \"type\": \"string\" },\n *     \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"changelog\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"version\": { \"type\": \"string\" },\n *           \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *           \"tags\": {\n *             \"type\": \"array\",\n *             \"items\": {\n *               \"type\": \"string\"\n *             }\n *           }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 404 Project not found\n * @error 500 An unexpected error occurred while fetching the project\n */\nexport async function GET(\n    request: Request,\n    { params }: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        await validateAuthAndGetUser()\n\n        const project = await db.project.findUnique({\n            where: { id: (await params).projectId },\n            include: {\n                changelog: {\n                    include: {\n                        entries: {\n                            orderBy: {\n                                createdAt: 'desc'\n                            },\n                            include: {\n                                tags: true\n                            }\n                        }\n                    }\n                }\n            }\n        })\n\n        if (!project) {\n            return Response.json({ error: 'Project not found' }, { status: 404 })\n        }\n\n        return Response.json(project)\n    } catch (error) {\n        console.error('Failed to fetch project:', error)\n        return Response.json({ error: 'Failed to fetch project' }, { status: 500 })\n    }\n}\n\n/**\n * @method PATCH\n * @description Updates a specific project\n * @path {projectId}\n * @request {json}\n * @query {}\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"name\": { \"type\": \"string\" },\n *     \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"changelog\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"version\": { \"type\": \"string\" },\n *           \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *           \"tags\": {\n *             \"type\": \"array\",\n *             \"items\": {\n *               \"type\": \"string\"\n *             }\n *           }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 404 Project not found\n * @error 500 An unexpected error occurred while updating the project\n */\nexport async function PATCH(\n    request: Request,\n    { params }: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser()\n\n        if (user.role === 'VIEWER') {\n            return Response.json({ error: 'Insufficient permissions' }, { status: 403 })\n        }\n\n        const json = await request.json()\n        const validatedData = projectUpdateSchema.parse(json)\n\n        const updated = await db.project.update({\n            where: { id: (await params).projectId },\n            data: validatedData,\n            include: {\n                changelog: true\n            }\n        })\n\n        return Response.json(updated)\n    } catch (error) {\n        if (error instanceof z.ZodError) {\n            return Response.json({ error: 'Invalid fields', details: error.errors }, { status: 400 })\n        }\n        console.error('Failed to update project:', error)\n        return Response.json({ error: 'Failed to update project' }, { status: 500 })\n    }\n}\n\n/**\n * @method DELETE\n * @description Deletes a specific project\n * @path {projectId}\n * @query {}\n * @response 204\n * @error 404 Project not found\n * @error 500 An unexpected error occurred while deleting the project\n */\nexport async function DELETE(\n    request: Request,\n    { params }: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        const user = await validateAuthAndGetUser()\n\n        // Only admins can delete projects\n        if (user.role !== 'ADMIN') {\n            return Response.json({ error: 'Admin access required to delete projects' }, { status: 403 })\n        }\n\n        await db.project.delete({\n            where: { id: (await params).projectId }\n        })\n\n        return new Response(null, { status: 204 })\n    } catch (error) {\n        console.error('Failed to delete project:', error)\n        return Response.json({ error: 'Failed to delete project' }, { status: 500 })\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/settings/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { db } from '@/lib/db'\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog'\nimport { createAuditLog } from '@/lib/utils/auditLog' // Add this import\n\n// Validation schema for project settings\nconst projectSettingsSchema = z.object({\n    name: z.string().min(1).optional(),\n    isPublic: z.boolean().optional(),\n    allowAutoPublish: z.boolean().optional(),\n    requireApproval: z.boolean().optional(),\n    defaultTags: z.array(z.string()).optional(),\n})\n\n/**\n * @method GET\n * @description Retrieves the project settings for a given project\n * @query {\n *   projectId: String, required\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"name\": { \"type\": \"string\" },\n *     \"isPublic\": { \"type\": \"boolean\" },\n *     \"allowAutoPublish\": { \"type\": \"boolean\" },\n *     \"requireApproval\": { \"type\": \"boolean\" },\n *     \"defaultTags\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n *     \"updatedAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *   }\n * }\n * @error 400 Invalid request data\n * @error 403 Unauthorized - User does not have 'ADMIN' role\n * @error 404 Project not found\n * @error 500 An unexpected error occurred while fetching the project settings\n */\nexport async function GET(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        const { projectId } = await (async () => context.params)();\n        const user = await validateAuthAndGetUser()\n\n        const project = await db.project.findUnique({\n            where: {\n                id: projectId\n            },\n            select: {\n                id: true,\n                name: true,\n                isPublic: true,\n                allowAutoPublish: true,\n                requireApproval: true,\n                defaultTags: true,\n                updatedAt: true,\n            }\n        })\n\n        if (!project) {\n            // Log attempt to view non-existent project\n            try {\n                await createAuditLog(\n                    'PROJECT_NOT_FOUND',\n                    user.id,\n                    user.id, // Use user's own ID to avoid foreign key issues\n                    {\n                        action: 'VIEW_PROJECT_SETTINGS',\n                        requestedProjectId: projectId,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create audit log:', auditLogError);\n            }\n\n            return new NextResponse(\n                JSON.stringify({ error: 'Project not found' }),\n                { status: 404 }\n            )\n        }\n\n        // Log project settings view\n        try {\n            await createAuditLog(\n                'VIEW_PROJECT_SETTINGS',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    projectId: project.id,\n                    projectName: project.name,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n        }\n\n        return new NextResponse(JSON.stringify(project), {\n            status: 200,\n            headers: { 'Content-Type': 'application/json' }\n        })\n    } catch (error) {\n        console.error('Failed to fetch project settings:', error)\n\n        // Log error\n        try {\n            const user = await validateAuthAndGetUser();\n            await createAuditLog(\n                'PROJECT_SETTINGS_ERROR',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    action: 'VIEW_PROJECT_SETTINGS',\n                    projectId: (await context.params).projectId,\n                    error: error instanceof Error ? error.message : 'Unknown error',\n                    stack: error instanceof Error ? error.stack : undefined,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create audit log:', auditLogError);\n        }\n\n        return new NextResponse(\n            JSON.stringify({ error: 'Failed to fetch project settings' }),\n            {\n                status: 500,\n                headers: { 'Content-Type': 'application/json' }\n            }\n        )\n    }\n}\n\n/**\n * @method PATCH\n * @description Updates the project settings for a given project\n * @query {\n *   projectId: String, required\n * }\n * @body {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"name\": { \"type\": \"string\" },\n *     \"isPublic\": { \"type\": \"boolean\" },\n *     \"allowAutoPublish\": { \"type\": \"boolean\" },\n *     \"requireApproval\": { \"type\": \"boolean\" },\n *     \"defaultTags\": {\n *       \"type\": \"array\",\n *       \"items\": { \"type\": \"string\" }\n *     }\n *   },\n *   \"required\": [\n *     \"name\"\n *   ]\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"name\": { \"type\": \"string\" },\n *     \"isPublic\": { \"type\": \"boolean\" },\n *     \"allowAutoPublish\": { \"type\": \"boolean\" },\n *     \"requireApproval\": { \"type\": \"boolean\" },\n *     \"defaultTags\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n *     \"updatedAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *   }\n * }\n * @error 400 Invalid request data\n * @error 403 Unauthorized - User does not have 'ADMIN' role\n * @error 404 Project not found\n * @error 500 An unexpected error occurred while updating the project settings\n */\nexport async function PATCH(\n    request: Request,\n    context: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        const { projectId } = await (async () => context.params)();\n        const user = await validateAuthAndGetUser()\n\n        // Log update attempt\n        try {\n            await createAuditLog(\n                'PROJECT_SETTINGS_UPDATE_ATTEMPT',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    projectId,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create update attempt audit log:', auditLogError);\n        }\n\n        // Get and validate request body\n        const body = await request.json()\n        const validatedData = projectSettingsSchema.parse(body)\n\n        // Access settings (isPublic, allowAutoPublish, requireApproval) are admin-only\n        const accessFields = ['isPublic', 'allowAutoPublish', 'requireApproval'] as const\n        if (user.role !== 'ADMIN') {\n            const attemptedAccessChange = accessFields.find(field => validatedData[field] !== undefined)\n            if (attemptedAccessChange) {\n                return new NextResponse(\n                    JSON.stringify({ error: 'Only administrators can modify access settings' }),\n                    { status: 403, headers: { 'Content-Type': 'application/json' } }\n                )\n            }\n        }\n\n        // Fetch current project to ensure it exists\n        const existingProject = await db.project.findUnique({\n            where: {\n                id: projectId\n            }\n        })\n\n        if (!existingProject) {\n            // Log attempt to update non-existent project\n            try {\n                await createAuditLog(\n                    'PROJECT_NOT_FOUND',\n                    user.id,\n                    user.id, // Use user's own ID to avoid foreign key issues\n                    {\n                        action: 'UPDATE_PROJECT_SETTINGS',\n                        requestedProjectId: projectId,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create project not found audit log:', auditLogError);\n            }\n\n            return new NextResponse(\n                JSON.stringify({ error: 'Project not found' }),\n                {\n                    status: 404,\n                    headers: { 'Content-Type': 'application/json' }\n                }\n            )\n        }\n\n        // Track changes for audit log\n        const changes: Record<string, { from: unknown; to: unknown }> = {};\n\n        if (validatedData.name !== undefined && validatedData.name !== existingProject.name) {\n            changes.name = {\n                from: existingProject.name,\n                to: validatedData.name\n            };\n        }\n\n        if (validatedData.isPublic !== undefined && validatedData.isPublic !== existingProject.isPublic) {\n            changes.isPublic = {\n                from: existingProject.isPublic,\n                to: validatedData.isPublic\n            };\n        }\n\n        if (validatedData.allowAutoPublish !== undefined && validatedData.allowAutoPublish !== existingProject.allowAutoPublish) {\n            changes.allowAutoPublish = {\n                from: existingProject.allowAutoPublish,\n                to: validatedData.allowAutoPublish\n            };\n        }\n\n        if (validatedData.requireApproval !== undefined && validatedData.requireApproval !== existingProject.requireApproval) {\n            changes.requireApproval = {\n                from: existingProject.requireApproval,\n                to: validatedData.requireApproval\n            };\n        }\n\n        if (validatedData.defaultTags !== undefined) {\n            const currentTags = existingProject.defaultTags || [];\n            const newTags = validatedData.defaultTags;\n\n            // Check if tags have changed\n            if (JSON.stringify(currentTags.sort()) !== JSON.stringify(newTags.sort())) {\n                changes.defaultTags = {\n                    from: currentTags,\n                    to: newTags\n                };\n            }\n        }\n\n        // Update project settings\n        const updatedProject = await db.project.update({\n            where: {\n                id: projectId\n            },\n            data: {\n                ...validatedData,\n                updatedAt: new Date()\n            },\n            select: {\n                id: true,\n                name: true,\n                isPublic: true,\n                allowAutoPublish: true,\n                requireApproval: true,\n                defaultTags: true,\n                updatedAt: true,\n            }\n        })\n\n        // Create specific audit log action based on what changed\n        let auditAction = 'UPDATE_PROJECT_SETTINGS';\n\n        // If only one thing changed, use a more specific audit action\n        if (Object.keys(changes).length === 1) {\n            const changedField = Object.keys(changes)[0];\n\n            if (changedField === 'name') {\n                auditAction = 'RENAME_PROJECT';\n            } else if (changedField === 'isPublic') {\n                auditAction = validatedData.isPublic ? 'MAKE_PROJECT_PUBLIC' : 'MAKE_PROJECT_PRIVATE';\n            } else if (changedField === 'allowAutoPublish') {\n                auditAction = validatedData.allowAutoPublish ? 'ENABLE_AUTO_PUBLISH' : 'DISABLE_AUTO_PUBLISH';\n            } else if (changedField === 'requireApproval') {\n                auditAction = validatedData.requireApproval ? 'ENABLE_APPROVAL_REQUIREMENT' : 'DISABLE_APPROVAL_REQUIREMENT';\n            }\n        }\n\n        // Log project settings update\n        try {\n            await createAuditLog(\n                auditAction,\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    projectId: updatedProject.id,\n                    projectName: updatedProject.name,\n                    changes,\n                    changeCount: Object.keys(changes).length,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create update audit log:', auditLogError);\n        }\n\n        return new NextResponse(JSON.stringify(updatedProject), {\n            status: 200,\n            headers: { 'Content-Type': 'application/json' }\n        })\n    } catch (error) {\n        console.error('Failed to update project settings:', error)\n\n        if (error instanceof z.ZodError) {\n            // Log validation error\n            try {\n                const user = await validateAuthAndGetUser();\n                await createAuditLog(\n                    'PROJECT_SETTINGS_VALIDATION_ERROR',\n                    user.id,\n                    user.id, // Use user's own ID to avoid foreign key issues\n                    {\n                        projectId: (await context.params).projectId,\n                        validationErrors: error.errors,\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditLogError) {\n                console.error('Failed to create validation error audit log:', auditLogError);\n            }\n\n            return new NextResponse(\n                JSON.stringify({\n                    error: 'Invalid request data',\n                    details: error.errors\n                }),\n                {\n                    status: 400,\n                    headers: { 'Content-Type': 'application/json' }\n                }\n            )\n        }\n\n        // Log general error\n        try {\n            const user = await validateAuthAndGetUser();\n            await createAuditLog(\n                'PROJECT_SETTINGS_ERROR',\n                user.id,\n                user.id, // Use user's own ID to avoid foreign key issues\n                {\n                    action: 'UPDATE_PROJECT_SETTINGS',\n                    projectId: (await context.params).projectId,\n                    error: error instanceof Error ? error.message : 'Unknown error',\n                    stack: error instanceof Error ? error.stack : undefined,\n                    timestamp: new Date().toISOString()\n                }\n            );\n        } catch (auditLogError) {\n            console.error('Failed to create error audit log:', auditLogError);\n        }\n\n        return new NextResponse(\n            JSON.stringify({ error: 'Failed to update project settings' }),\n            {\n                status: 500,\n                headers: { 'Content-Type': 'application/json' }\n            }\n        )\n    }\n}"
  },
  {
    "path": "app/api/projects/[projectId]/versions/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog'\nimport { db } from '@/lib/db'\n\n/**\n * @method GET\n * @description Fetches the versions of a given project\n * @query {\n *   projectId: String, required\n * }\n * @response 200 {\n *   \"type\": \"array\",\n *   \"items\": {\n *     \"type\": \"number\"\n *   }\n * }\n * @error 400 Invalid request data\n * @error 403 Unauthorized - User does not have 'ADMIN' role\n * @error 500 An unexpected error occurred while fetching the versions\n */\nexport async function GET(request: Request, context: { params: Promise<{ projectId: string }> }) {\n    try {\n        await validateAuthAndGetUser();\n        const { projectId } = await (async () => context.params)();\n\n        // Fetch all changelog entries with versions, sorted by publishedAt\n        const entries = await db.changelogEntry.findMany({\n            where: {\n                changelog: {\n                    projectId\n                },\n                version: {\n                    not: null\n                }\n            },\n            select: {\n                version: true,\n                publishedAt: true\n            },\n            orderBy: {\n                publishedAt: 'desc'\n            }\n        });\n\n        // Get unique versions\n        const versions = [...new Set(entries.map(entry => entry.version))].filter(Boolean);\n\n        return NextResponse.json({ versions });\n    } catch (error) {\n        console.error('Error fetching versions:', error);\n        return NextResponse.json(\n            { error: 'Failed to fetch versions' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/import/canny/fetch/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server';\nimport {CannyService} from '@/lib/services/projects/importing/integrations/canny.service';\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog';\nimport {CannyImportOptions} from '@/lib/types/projects/importing/canny';\n\nexport async function POST(request: NextRequest) {\n    try {\n        // Validate authentication\n        await validateAuthAndGetUser();\n\n        const body = await request.json();\n\n        console.log('Canny fetch request body:', body);\n\n        const options: CannyImportOptions = {\n            apiKey: body.apiKey,\n            includeLabels: body.includeLabels ?? true,\n            includePostTags: body.includePostTags ?? false,\n            statusFilter: body.statusFilter ?? 'published',\n            maxEntries: body.maxEntries ?? 50\n        };\n\n        // Validate options\n        if (!options.apiKey) {\n            return NextResponse.json(\n                {success: false, error: 'API key is required'},\n                {status: 400}\n            );\n        }\n\n        if (options.maxEntries > 500) {\n            return NextResponse.json(\n                {success: false, error: 'Maximum entries cannot exceed 500'},\n                {status: 400}\n            );\n        }\n\n        console.log('Fetching entries from Canny with options:', options);\n\n        // Fetch entries from Canny\n        const {entries, error} = await CannyService.fetchEntries(options);\n\n        if (error) {\n            console.error('Canny fetch error:', error);\n            return NextResponse.json(\n                {success: false, error},\n                {status: 400}\n            );\n        }\n\n        console.log('Raw Canny entries:', entries.length);\n\n        // Convert to validated entries\n        const convertedEntries = CannyService.convertEntries(entries, options);\n\n        console.log('Converted entries:', convertedEntries.length);\n        console.log('Sample converted entry:', convertedEntries[0]);\n\n        return NextResponse.json({\n            success: true,\n            entries: convertedEntries,\n            count: convertedEntries.length\n        });\n\n    } catch (error) {\n        console.error('Canny fetch error:', error);\n        return NextResponse.json(\n            {success: false, error: 'Failed to fetch from Canny'},\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/import/canny/validate/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server';\nimport { CannyService } from '@/lib/services/projects/importing/integrations/canny.service';\n\nexport async function POST(request: NextRequest) {\n    try {\n        const { apiKey } = await request.json();\n\n        if (!apiKey || typeof apiKey !== 'string') {\n            return NextResponse.json(\n                { valid: false, error: 'API key is required' },\n                { status: 400 }\n            );\n        }\n\n        const result = await CannyService.validateApiKey(apiKey);\n        return NextResponse.json(result);\n\n    } catch (error) {\n        console.error('Canny validation error:', error);\n        return NextResponse.json(\n            { valid: false, error: 'Validation failed' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/import/parse/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server';\nimport { MarkdownParserService } from '@/lib/services/projects/importing/markdown-parser.service';\nimport { ImportValidationService } from '@/lib/services/projects/importing/validation.service';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\n\nexport async function POST(request: NextRequest) {\n    try {\n        // Validate authentication\n        await validateAuthAndGetUser();\n\n        // Parse request body\n        const { content, projectId } = await request.json();\n\n        if (!content || typeof content !== 'string') {\n            return NextResponse.json(\n                { error: 'Content is required and must be a string' },\n                { status: 400 }\n            );\n        }\n\n        if (!projectId || typeof projectId !== 'string') {\n            return NextResponse.json(\n                { error: 'Project ID is required' },\n                { status: 400 }\n            );\n        }\n\n        // Validate user has access to project\n        // This would typically check project permissions\n        // For now, we'll assume they have access as we don't have any sort of access-control that is extensive in place yet\n\n        // Parse the markdown content\n        console.log('Parsing markdown content...');\n        const parsedChangelog = MarkdownParserService.parseChangelog(content);\n\n        if (parsedChangelog.entries.length === 0) {\n            return NextResponse.json(\n                { error: 'No valid changelog entries found in the content' },\n                { status: 422 }\n            );\n        }\n\n        // Validate the parsed entries\n        console.log('Validating parsed entries...');\n        const { validatedEntries, preview } = ImportValidationService.validateEntries(\n            parsedChangelog.entries\n        );\n\n        // Return the parsed and validated data\n        return NextResponse.json({\n            success: true,\n            parsed: parsedChangelog,\n            validatedEntries,\n            preview,\n            metadata: {\n                parseTime: new Date().toISOString(),\n                originalFormat: parsedChangelog.metadata.originalFormat,\n                totalSections: parsedChangelog.metadata.totalSections\n            }\n        });\n\n    } catch (error) {\n        console.error('Error parsing changelog content:', error);\n\n        return NextResponse.json(\n            {\n                error: 'Failed to parse changelog content',\n                details: error instanceof Error ? error.message : 'Unknown error'\n            },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/import/process/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server';\nimport {ImportProcessorService} from '@/lib/services/projects/importing/processor.service';\nimport {ImportValidationService} from '@/lib/services/projects/importing/validation.service';\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog';\nimport {createAuditLog} from '@/lib/utils/auditLog';\nimport {\n    ValidatedEntry,\n    ImportOptions,\n    ImportResult\n} from '@/lib/types/projects/importing';\n\ninterface AuthenticatedUser {\n    id: string;\n    email: string;\n    name?: string;\n    role: string;\n}\n\nexport async function POST(request: NextRequest) {\n    const startTime = new Date();\n    let user: AuthenticatedUser | null = null;\n    let projectId: string = '';\n\n    try {\n        // Validate authentication\n        user = await validateAuthAndGetUser() as AuthenticatedUser;\n\n        // Parse request body\n        const {projectId: reqProjectId, entries, options} = await request.json();\n        projectId = reqProjectId;\n\n        // Validate required fields\n        if (!projectId || typeof projectId !== 'string') {\n            return NextResponse.json(\n                {error: 'Project ID is required'},\n                {status: 400}\n            );\n        }\n\n        if (!Array.isArray(entries) || entries.length === 0) {\n            return NextResponse.json(\n                {error: 'Entries array is required and must not be empty'},\n                {status: 400}\n            );\n        }\n\n        // Validate import options\n        const optionValidation = ImportValidationService.validateImportOptions(options);\n        if (!optionValidation.isValid) {\n            return NextResponse.json(\n                {\n                    error: 'Invalid import options',\n                    details: optionValidation.errors\n                },\n                {status: 400}\n            );\n        }\n\n        // Validate user permissions\n        const permissionCheck = await ImportProcessorService.validateImportPermissions(\n            user.id,\n            projectId\n        );\n\n        if (!permissionCheck.canImport) {\n            return NextResponse.json(\n                {error: permissionCheck.reason || 'Insufficient permissions'},\n                {status: 403}\n            );\n        }\n\n        // Type the entries and options properly\n        const validatedEntries: ValidatedEntry[] = entries;\n        const importOptions: ImportOptions = {\n            strategy: options.strategy || 'merge',\n            preserveExistingEntries: options.preserveExistingEntries ?? true,\n            autoGenerateVersions: options.autoGenerateVersions ?? false,\n            defaultTags: options.defaultTags || [],\n            publishImportedEntries: options.publishImportedEntries ?? false,\n            dateHandling: options.dateHandling || 'preserve',\n            conflictResolution: options.conflictResolution || 'skip'\n        };\n\n        // Log the import attempt\n        await createAuditLog(\n            'CHANGELOG_IMPORT_STARTED',\n            user.id,\n            user.id,\n            {\n                projectId,\n                entryCount: validatedEntries.length,\n                strategy: importOptions.strategy,\n                conflictResolution: importOptions.conflictResolution,\n                timestamp: startTime.toISOString()\n            }\n        );\n\n        // Process the import\n        console.log(`Starting import for project ${projectId} with ${validatedEntries.length} entries...`);\n\n        const result: ImportResult = await ImportProcessorService.processImport(\n            projectId,\n            validatedEntries,\n            importOptions,\n            user.id\n        );\n\n        // Log the import completion\n        await createAuditLog(\n            result.success ? 'CHANGELOG_IMPORT_SUCCESS' : 'CHANGELOG_IMPORT_PARTIAL',\n            user.id,\n            user.id,\n            {\n                projectId,\n                importedCount: result.importedCount,\n                skippedCount: result.skippedCount,\n                errorCount: result.errorCount,\n                processingTime: result.processingTime,\n                success: result.success,\n                timestamp: new Date().toISOString()\n            }\n        );\n\n        // Return the result\n        return NextResponse.json(result);\n\n    } catch (error) {\n        console.error('Error processing changelog import:', error);\n\n        // Log the error\n        if (user && projectId) {\n            try {\n                await createAuditLog(\n                    'CHANGELOG_IMPORT_ERROR',\n                    user.id,\n                    user.id,\n                    {\n                        projectId,\n                        error: error instanceof Error ? error.message : 'Unknown error',\n                        processingTime: Date.now() - startTime.getTime(),\n                        timestamp: new Date().toISOString()\n                    }\n                );\n            } catch (auditError) {\n                console.error('Failed to create audit log:', auditError);\n            }\n        }\n\n        return NextResponse.json(\n            {\n                success: false,\n                error: 'Failed to process import',\n                details: error instanceof Error ? error.message : 'Unknown error occurred',\n                importedCount: 0,\n                skippedCount: 0,\n                errorCount: 0,\n                createdEntries: [],\n                warnings: [],\n                errors: [],\n                processingTime: Date.now() - startTime.getTime()\n            },\n            {status: 500}\n        );\n    }\n}\n\n// GET endpoint to check import history/stats\nexport async function GET(request: NextRequest) {\n    try {\n        const user = await validateAuthAndGetUser();\n        const {searchParams} = new URL(request.url);\n        const projectId = searchParams.get('projectId');\n\n        if (!projectId) {\n            return NextResponse.json(\n                {error: 'Project ID is required'},\n                {status: 400}\n            );\n        }\n\n        // Validate permissions\n        const permissionCheck = await ImportProcessorService.validateImportPermissions(\n            user.id,\n            projectId\n        );\n\n        if (!permissionCheck.canImport) {\n            return NextResponse.json(\n                {error: permissionCheck.reason || 'Insufficient permissions'},\n                {status: 403}\n            );\n        }\n\n        // Get import history\n        const history = await ImportProcessorService.getImportHistory(projectId);\n\n        return NextResponse.json({\n            success: true,\n            history\n        });\n\n    } catch (error) {\n        console.error('Error fetching import history:', error);\n\n        return NextResponse.json(\n            {\n                error: 'Failed to fetch import history',\n                details: error instanceof Error ? error.message : 'Unknown error'\n            },\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/projects/route.ts",
    "content": "import { db } from '@/lib/db'\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog'\nimport { z } from 'zod'\n\n/**\n * Schema for validating project request body.\n */\nconst projectSchema = z.object({\n    name: z.string().min(1, 'Project name is required')\n})\n\n/**\n * @method GET\n * @description Fetches a list of projects, including their latest changelog entry and entry count\n * @response 200 {\n *   \"type\": \"array\",\n *   \"items\": {\n *     \"type\": \"object\",\n *     \"properties\": {\n *       \"id\": { \"type\": \"string\" },\n *       \"name\": { \"type\": \"string\" },\n *       \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *       \"entryCount\": { \"type\": \"number\" },\n *       \"latestEntry\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"version\": { \"type\": \"string\" },\n *           \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 500 An unexpected error occurred while fetching projects\n */\nexport async function GET() {\n    try {\n        await validateAuthAndGetUser()\n\n        const projects = await db.project.findMany({\n            include: {\n                changelog: {\n                    include: {\n                        entries: {\n                            orderBy: {\n                                createdAt: 'desc'\n                            },\n                            take: 1,\n                            select: {\n                                id: true,\n                                version: true,\n                                createdAt: true\n                            }\n                        },\n                        _count: {\n                            select: {\n                                entries: true\n                            }\n                        }\n                    }\n                }\n            },\n            orderBy: {\n                createdAt: 'desc'\n            }\n        })\n\n        // Transform the response to include entry count and latest entry\n        const transformedProjects = projects.map(project => ({\n            ...project,\n            entryCount: project.changelog?._count?.entries || 0,\n            latestEntry: project.changelog?.entries?.[0] || null\n        }))\n\n        return Response.json(transformedProjects)\n    } catch (error) {\n        console.error('Failed to fetch projects:', error)\n        return Response.json({ error: 'Failed to fetch projects' }, { status: 500 })\n    }\n}\n\n/**\n * @method POST\n * @description Creates a new project and its associated changelog\n * @body {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"name\": {\n *       \"type\": \"string\",\n *       \"minLength\": 1,\n *       \"description\": \"Project name\"\n *     }\n *   }\n * }\n * @response 201 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"name\": { \"type\": \"string\" },\n *     \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"changelog\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" }\n *       }\n *     }\n *   }\n * }\n * @error 400 Invalid input - Project name is required\n * @error 500 An unexpected error occurred while creating the project\n */\nexport async function POST(request: Request) {\n    try {\n        await validateAuthAndGetUser()\n\n        const json = await request.json()\n        const { name } = projectSchema.parse(json)\n\n        const project = await db.project.create({\n            data: {\n                name,\n                changelog: {\n                    create: {} // Create an empty changelog automatically\n                }\n            },\n            include: {\n                changelog: true\n            }\n        })\n\n        return Response.json(project, { status: 201 })\n    } catch (error) {\n        if (error instanceof z.ZodError) {\n            return Response.json({ error: error.errors[0].message }, { status: 400 })\n        }\n\n        console.error('Failed to create project:', error)\n        return Response.json({ error: 'Failed to create project' }, { status: 500 })\n    }\n}"
  },
  {
    "path": "app/api/requests/route.ts",
    "content": "import {NextResponse} from 'next/server';\nimport {db} from '@/lib/db';\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog';\n\n/**\n * @method GET\n * @description Retrieves changelog requests for the current user\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"requests\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"type\": { \"type\": \"string\" },\n *           \"status\": { \"type\": \"string\" },\n *           \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *           \"reviewedAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *           \"project\": {\n *             \"type\": \"object\",\n *             \"properties\": {\n *               \"id\": { \"type\": \"string\" },\n *               \"name\": { \"type\": \"string\" }\n *             }\n *           },\n *           \"entry\": {\n *             \"type\": \"object\",\n *             \"properties\": {\n *               \"id\": { \"type\": \"string\" },\n *               \"title\": { \"type\": \"string\" }\n *             }\n *           },\n *           \"tag\": {\n *             \"type\": \"object\",\n *             \"properties\": {\n *               \"id\": { \"type\": \"string\" },\n *               \"name\": { \"type\": \"string\" }\n *             }\n *           }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 401 Unauthorized - Please log in\n * @error 500 Internal Server Error\n * @secure cookieAuth\n */\nexport async function GET() {\n    try {\n        // Validate authentication\n        const user = await validateAuthAndGetUser();\n\n        if (!user) {\n            return NextResponse.json(\n                {error: 'Unauthorized'},\n                {status: 401}\n            );\n        }\n\n        // Fetch requests for the current user\n        const requests = await db.changelogRequest.findMany({\n            where: {\n                staffId: user.id\n            },\n            include: {\n                project: {\n                    select: {\n                        id: true,\n                        name: true\n                    }\n                },\n                ChangelogEntry: {\n                    select: {\n                        id: true,\n                        title: true\n                    }\n                },\n                ChangelogTag: {\n                    select: {\n                        id: true,\n                        name: true\n                    }\n                },\n                admin: {\n                    select: {\n                        id: true,\n                        name: true,\n                        email: true\n                    }\n                }\n            },\n            orderBy: {\n                createdAt: 'desc'\n            }\n        });\n\n        return NextResponse.json({requests});\n    } catch (error) {\n        console.error('Error fetching requests:', error);\n        return NextResponse.json(\n            {error: 'Failed to fetch requests'},\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/search/route.ts",
    "content": "// app/api/search/route.ts\nimport {NextRequest, NextResponse} from 'next/server';\nimport {verifyAccessToken} from '@/lib/auth/tokens';\nimport {db} from '@/lib/db';\nimport {z} from 'zod';\nimport {searchService} from \"@/lib/services/search/service\";\n\n// Feature flag for tags\nconst ENABLE_TAGS = false;\n\n// Request validation schema\nconst searchSchema = z.object({\n    query: z.string().min(1).max(200),\n    filters: z.object({\n        tags: z.array(z.string()).optional(),\n        dateRange: z.object({\n            start: z.string().datetime(),\n            end: z.string().datetime()\n        }).optional(),\n        hasVersion: z.boolean().optional(),\n        projectIds: z.array(z.string()).optional()\n    }).optional(),\n    limit: z.number().min(1).max(50).default(20),\n    offset: z.number().min(0).default(0),\n    types: z.array(z.string()).optional()\n});\n\n// Helper function to get user from request\nasync function getUserFromRequest(request: NextRequest) {\n    // Try to get access token from cookies first\n    let accessToken = request.cookies.get('accessToken')?.value;\n\n    // If not in cookies, check Authorization header\n    if (!accessToken) {\n        const authHeader = request.headers.get('authorization');\n        if (authHeader?.startsWith('Bearer ')) {\n            accessToken = authHeader.substring(7);\n        }\n    }\n\n    if (!accessToken) {\n        return null;\n    }\n\n    try {\n        // Verify the token and get user ID\n        const userId = await verifyAccessToken(accessToken);\n        if (!userId) {\n            return null;\n        }\n\n        // Fetch full user data from database\n        return await db.user.findUnique({\n            where: {id: userId},\n            select: {\n                id: true,\n                email: true,\n                name: true,\n                role: true,\n                // Add any other fields you need for access control\n            }\n        });\n    } catch (error) {\n        console.error('Token verification failed:', error);\n        return null;\n    }\n}\n\nexport async function POST(request: NextRequest) {\n    try {\n        // Get authenticated user\n        const user = await getUserFromRequest(request);\n        if (!user) {\n            return NextResponse.json(\n                {error: 'Authentication required'},\n                {status: 401}\n            );\n        }\n\n        // Parse and validate request body\n        const body = await request.json();\n        const validation = searchSchema.safeParse(body);\n\n        if (!validation.success) {\n            return NextResponse.json(\n                {\n                    error: 'Invalid request parameters',\n                    details: validation.error.errors\n                },\n                {status: 400}\n            );\n        }\n\n        const {query, limit, offset} = validation.data;\n        let {filters} = validation.data;\n\n        // Remove tags from filters if tags are disabled\n        if (!ENABLE_TAGS && filters?.tags) {\n            // eslint-disable-next-line @typescript-eslint/no-unused-vars\n            const {tags, ...filtersWithoutTags} = filters;\n            filters = filtersWithoutTags;\n        }\n\n        // Convert date strings to Date objects if provided\n        const processedFilters = filters ? {\n            ...filters,\n            dateRange: filters.dateRange ? {\n                start: new Date(filters.dateRange.start),\n                end: new Date(filters.dateRange.end)\n            } : undefined\n        } : undefined;\n\n        // Perform search\n        const searchResults = await searchService.searchAll(\n            user,\n            query,\n            processedFilters,\n            limit,\n            offset\n        );\n\n        // Filter out any tag results if tags are disabled\n        if (!ENABLE_TAGS && searchResults.results) {\n            searchResults.results = searchResults.results.filter(\n                (result) => result.type !== 'tag'\n            );\n        }\n\n        // Log search for development\n        if (process.env.NODE_ENV === 'development') {\n            console.log(`Search performed: \"${query}\" by user ${user.id} (${user.role}), found ${searchResults.total} results in ${searchResults.executionTime}ms`);\n        }\n\n        return NextResponse.json({\n            success: true,\n            ...searchResults,\n            query\n        });\n\n    } catch (error) {\n        console.error('Search API error:', error);\n\n        return NextResponse.json(\n            {\n                error: 'Internal server error',\n                message: process.env.NODE_ENV === 'development'\n                    ? (error as Error).message\n                    : 'Search failed'\n            },\n            {status: 500}\n        );\n    }\n}\n\n// Optional: GET endpoint for simple searches (useful for testing)\nexport async function GET(request: NextRequest) {\n    const {searchParams} = new URL(request.url);\n    const query = searchParams.get('q');\n\n    if (!query) {\n        return NextResponse.json(\n            {error: 'Query parameter \"q\" is required'},\n            {status: 400}\n        );\n    }\n\n    // Get authenticated user\n    const user = await getUserFromRequest(request);\n    if (!user) {\n        return NextResponse.json(\n            {error: 'Authentication required'},\n            {status: 401}\n        );\n    }\n\n    try {\n        // Perform search with default parameters\n        const searchResults = await searchService.searchAll(\n            user,\n            query,\n            undefined, // No filters\n            10,         // Default limit\n            0           // No offset\n        );\n\n        // Filter out tag results if tags are disabled\n        if (!ENABLE_TAGS && searchResults.results) {\n            searchResults.results = searchResults.results.filter(\n                (result) => result.type !== 'tag'\n            );\n        }\n\n        return NextResponse.json({\n            success: true,\n            ...searchResults,\n            query\n        });\n\n    } catch (error) {\n        console.error('Search GET error:', error);\n\n        return NextResponse.json(\n            {\n                error: 'Internal server error',\n                message: process.env.NODE_ENV === 'development'\n                    ? (error as Error).message\n                    : 'Search failed'\n            },\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/setup/admin/route.ts",
    "content": "import {NextResponse} from 'next/server'\nimport {z} from 'zod'\nimport {db} from '@/lib/db'\nimport {hashPassword} from '@/lib/auth/password'\nimport {Role} from '@prisma/client'\n\n/**\n * Schema for validating admin user request body.\n */\nconst adminSchema = z.object({\n    name: z.string().min(2, 'Name must be at least 2 characters'),\n    email: z.string().email('Please enter a valid email'),\n    password: z.string().min(8, 'Password must be at least 8 characters'),\n})\n\n/**\n * @method POST\n * @description Creates a new admin user for the application\n * @body {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"name\": {\n *       \"type\": \"string\",\n *       \"minLength\": 2,\n *       \"description\": \"Admin's full name\"\n *     },\n *     \"email\": {\n *       \"type\": \"string\",\n *       \"format\": \"email\",\n *       \"description\": \"Admin's email address\"\n *     },\n *     \"password\": {\n *       \"type\": \"string\",\n *       \"minLength\": 8,\n *       \"description\": \"Admin's password\"\n *     }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"message\": {\n *       \"type\": \"string\",\n *       \"example\": \"Admin account created successfully\"\n *     },\n *     \"user\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"email\": { \"type\": \"string\" },\n *         \"name\": { \"type\": \"string\" },\n *         \"role\": { \"type\": \"string\" }\n *       }\n *     }\n *   }\n * }\n * @error 400 Validation failed - Invalid input data\n * @error 403 Setup already completed - Cannot run setup more than once\n * @error 500 An unexpected error occurred during setup\n */\nexport async function POST(request: Request) {\n    try {\n        // Check if setup is already complete\n        const userCount = await db.user.count({\n            where: {\n                email: {\n                    not: {\n                        endsWith: '@changerawr.sys'\n                    }\n                }\n            }\n        });\n        if (userCount > 0) {\n            return NextResponse.json(\n                {error: 'Setup has already been completed'},\n                {status: 403}\n            )\n        }\n\n        // Validate request data\n        const body = await request.json()\n        const validatedData = adminSchema.parse(body)\n\n        // Hash password\n        const hashedPassword = await hashPassword(validatedData.password)\n\n        // Create admin user\n        const user = await db.user.create({\n            data: {\n                name: validatedData.name,\n                email: validatedData.email,\n                password: hashedPassword,\n                role: Role.ADMIN,\n            },\n            select: {\n                id: true,\n                email: true,\n                name: true,\n                role: true,\n            },\n        })\n\n        return NextResponse.json({\n            message: 'Admin account created successfully',\n            user,\n        })\n    } catch (error) {\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {\n                    error: 'Invalid input',\n                    details: error.errors,\n                },\n                {status: 400}\n            )\n        }\n\n        console.error('Admin setup error:', error)\n        return NextResponse.json(\n            {error: 'Failed to create admin account'},\n            {status: 500}\n        )\n    }\n}\n\n/**\n * @method GET\n * @description Method not allowed - Admin user setup endpoint only accepts POST requests\n * @response 405 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": {\n *       \"type\": \"string\",\n *       \"example\": \"Method not allowed\"\n *     }\n *   }\n * }\n */\nexport async function GET() {\n    return NextResponse.json(\n        {error: 'Method not allowed'},\n        {status: 405}\n    )\n}"
  },
  {
    "path": "app/api/setup/invitations/route.ts",
    "content": "// app/api/invitations/route.ts\nimport { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { db } from '@/lib/db'\nimport { Role } from '@prisma/client'\nimport { nanoid } from 'nanoid'\n\n/**\n * Schema for validating invitation creation request body.\n */\nconst createInvitationSchema = z.object({\n    email: z.string().email('Please enter a valid email'),\n    name: z.string().optional(),\n    role: z.nativeEnum(Role).default(Role.STAFF),\n    expiresInDays: z.number().min(1).max(30).default(7),\n})\n\n/**\n * @method POST\n * @description Create a new invitation link for team setup\n * @body {\n *   \"type\": \"object\",\n *   \"required\": [\"email\"],\n *   \"properties\": {\n *     \"email\": {\n *       \"type\": \"string\",\n *       \"format\": \"email\",\n *       \"description\": \"Email address to invite\"\n *     },\n *     \"name\": {\n *       \"type\": \"string\",\n *       \"description\": \"Optional name for the invitee\"\n *     },\n *     \"role\": {\n *       \"type\": \"string\",\n *       \"enum\": [\"ADMIN\", \"STAFF\", \"VIEWER\"],\n *       \"default\": \"STAFF\",\n *       \"description\": \"Role to assign to the invited user\"\n *     },\n *     \"expiresInDays\": {\n *       \"type\": \"integer\",\n *       \"minimum\": 1,\n *       \"maximum\": 30,\n *       \"default\": 7,\n *       \"description\": \"Number of days until the invitation expires\"\n *     }\n *   }\n * }\n * @response 201 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"id\": { \"type\": \"string\" },\n *     \"token\": { \"type\": \"string\" },\n *     \"email\": { \"type\": \"string\" },\n *     \"role\": { \"type\": \"string\" },\n *     \"expiresAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *     \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n *   }\n * }\n * @error 400 Validation failed - Invalid input data\n * @error 409 Invitation already exists for this email\n * @error 500 An unexpected error occurred\n */\nexport async function POST(request: Request) {\n    try {\n        // Block access once setup is complete\n        const userCount = await db.user.count({\n            where: { email: { not: { endsWith: '@changerawr.sys' } } }\n        })\n        if (userCount > 0) {\n            return NextResponse.json(\n                { error: 'Setup already completed. Use the admin panel to manage invitations.' },\n                { status: 403 }\n            )\n        }\n\n        // Validate request data\n        const body = await request.json()\n        const validatedData = createInvitationSchema.parse(body)\n\n        // Check if invitation already exists for this email\n        const existingInvitation = await db.invitationLink.findFirst({\n            where: {\n                email: validatedData.email,\n                usedAt: null, // Only unused invitations\n                expiresAt: {\n                    gt: new Date() // Not expired\n                }\n            }\n        })\n\n        if (existingInvitation) {\n            return NextResponse.json(\n                { error: 'An active invitation already exists for this email' },\n                { status: 409 }\n            )\n        }\n\n        // Calculate expiration date\n        const expiresAt = new Date()\n        expiresAt.setDate(expiresAt.getDate() + validatedData.expiresInDays)\n\n        // Generate unique token\n        const token = nanoid(32)\n\n        // For setup phase, we'll use a system user ID or the first admin\n        // This is a temporary solution for the setup phase\n        let createdBy = 'system'\n\n        // Try to find the first admin user\n        const firstAdmin = await db.user.findFirst({\n            where: { role: Role.ADMIN },\n            select: { id: true }\n        })\n\n        if (firstAdmin) {\n            createdBy = firstAdmin.id\n        }\n\n        // Create invitation\n        const invitation = await db.invitationLink.create({\n            data: {\n                token,\n                email: validatedData.email,\n                role: validatedData.role,\n                createdBy,\n                expiresAt\n            },\n            select: {\n                id: true,\n                token: true,\n                email: true,\n                role: true,\n                expiresAt: true,\n                createdAt: true\n            }\n        })\n\n        return NextResponse.json(invitation, { status: 201 })\n    } catch (error) {\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {\n                    error: 'Validation failed',\n                    details: error.errors.map(e => ({\n                        path: e.path.join('.'),\n                        message: e.message\n                    }))\n                },\n                { status: 400 }\n            )\n        }\n\n        console.error('Invitation creation error:', error)\n        return NextResponse.json(\n            { error: 'An unexpected error occurred while creating invitation' },\n            { status: 500 }\n        )\n    }\n}\n\n/**\n * @method GET\n * @description List all active invitations (for admin use)\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"invitations\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"id\": { \"type\": \"string\" },\n *           \"email\": { \"type\": \"string\" },\n *           \"role\": { \"type\": \"string\" },\n *           \"expiresAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *           \"createdAt\": { \"type\": \"string\", \"format\": \"date-time\" },\n *           \"usedAt\": { \"type\": \"string\", \"format\": \"date-time\", \"nullable\": true }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 500 An unexpected error occurred\n */\nexport async function GET() {\n    try {\n        // Block access once setup is complete\n        const userCount = await db.user.count({\n            where: { email: { not: { endsWith: '@changerawr.sys' } } }\n        })\n        if (userCount > 0) {\n            return NextResponse.json(\n                { error: 'Setup already completed. Use the admin panel to manage invitations.' },\n                { status: 403 }\n            )\n        }\n\n        const invitations = await db.invitationLink.findMany({\n            select: {\n                id: true,\n                email: true,\n                role: true,\n                expiresAt: true,\n                createdAt: true,\n                usedAt: true\n            },\n            orderBy: {\n                createdAt: 'desc'\n            }\n        })\n\n        return NextResponse.json({ invitations })\n    } catch (error) {\n        console.error('Error fetching invitations:', error)\n        return NextResponse.json(\n            { error: 'An unexpected error occurred while fetching invitations' },\n            { status: 500 }\n        )\n    }\n}"
  },
  {
    "path": "app/api/setup/oauth/auto/route.ts",
    "content": "// app/api/setup/oauth/auto/route.ts\nimport { NextResponse } from 'next/server';\nimport { z } from 'zod';\nimport {\n    performAutoOAuthSetup,\n    verifyAutoSetupConfiguration,\n    getOAuthServerInfo\n} from '@/lib/auth/providers/easypanel/auto-setup';\nimport { db } from '@/lib/db';\n\n/**\n * Schema for validating auto OAuth setup request body.\n */\nconst autoSetupSchema = z.object({\n    appName: z.string().optional(),\n    redirectUri: z.string().url().optional(),\n    persistent: z.boolean().default(true),\n});\n\n/**\n * @method POST\n * @description Automatically create and configure OAuth client with remote server\n * @body {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"appName\": {\n *       \"type\": \"string\",\n *       \"description\": \"Optional custom name for the OAuth client\"\n *     },\n *     \"persistent\": {\n *       \"type\": \"boolean\",\n *       \"default\": true,\n *       \"description\": \"Whether the client should persist server restarts\"\n *     }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\" },\n *     \"client\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"id\": { \"type\": \"string\" },\n *         \"name\": { \"type\": \"string\" },\n *         \"clientId\": { \"type\": \"string\" },\n *         \"redirectUri\": { \"type\": \"string\" }\n *       }\n *     },\n *     \"message\": { \"type\": \"string\" }\n *   }\n * }\n * @response 400 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\", \"example\": false },\n *     \"error\": { \"type\": \"string\" },\n *     \"details\": { \"type\": \"string\" }\n *   }\n * }\n * @response 503 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\", \"example\": false },\n *     \"error\": { \"type\": \"string\", \"example\": \"Auto OAuth setup not available\" },\n *     \"details\": { \"type\": \"string\" }\n *   }\n * }\n * @error 400 Validation failed or setup error\n * @error 503 Auto OAuth setup not configured\n * @error 500 Internal server error\n */\nexport async function POST(request: Request) {\n    try {\n        // Block access once setup is complete\n        const userCount = await db.user.count({\n            where: { email: { not: { endsWith: '@changerawr.sys' } } }\n        })\n        if (userCount > 0) {\n            return NextResponse.json(\n                { success: false, error: 'Setup already completed. Use the admin panel to manage OAuth providers.' },\n                { status: 403 }\n            )\n        }\n\n        console.log('🦖 Auto OAuth setup request received');\n\n        // Check if environment variables are set first\n        const serverUrl = process.env.CHR_EPOA2_SERV_URL;\n        const apiKey = process.env.CHR_EPOA2_SERV_API_KEY;\n\n        console.log('🦖 Environment check:', {\n            hasServerUrl: !!serverUrl,\n            hasApiKey: !!apiKey,\n            serverUrl: serverUrl ? serverUrl.substring(0, 20) + '...' : 'not set'\n        });\n\n        if (!serverUrl || !apiKey) {\n            console.log('🦖 Missing environment variables');\n            return NextResponse.json(\n                {\n                    success: false,\n                    error: 'Auto OAuth setup not available',\n                    details: 'Missing CHR_EPOA2_SERV_URL or CHR_EPOA2_SERV_API_KEY environment variables'\n                },\n                { status: 503 }\n            );\n        }\n\n        // Parse and validate request body\n        let body;\n        try {\n            body = await request.json();\n            console.log('🦖 Request body received:', body);\n        } catch (parseError) {\n            console.error('🦖 Failed to parse request body:', parseError);\n            return NextResponse.json(\n                {\n                    success: false,\n                    error: 'Invalid JSON in request body',\n                    details: parseError instanceof Error ? parseError.message : 'JSON parse error'\n                },\n                { status: 400 }\n            );\n        }\n\n        // Validate request data with better error handling\n        let validatedData;\n        try {\n            validatedData = autoSetupSchema.parse(body);\n            console.log('🦖 Validated data:', validatedData);\n        } catch (validationError) {\n            console.error('🦖 Validation failed:', validationError);\n            if (validationError instanceof z.ZodError) {\n                return NextResponse.json(\n                    {\n                        success: false,\n                        error: 'Validation failed',\n                        details: validationError.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', '),\n                        received: body\n                    },\n                    { status: 400 }\n                );\n            }\n            throw validationError;\n        }\n\n        // Perform auto setup\n        console.log('🦖 Starting auto setup with options:', validatedData);\n        const result = await performAutoOAuthSetup({\n            appName: validatedData.appName,\n            persistent: validatedData.persistent\n        });\n\n        console.log('🦖 Auto setup result:', { success: result.success, error: result.error });\n\n        if (!result.success) {\n            return NextResponse.json(\n                {\n                    success: false,\n                    error: result.error,\n                    details: result.details\n                },\n                { status: result.error?.includes('not available') ? 503 : 400 }\n            );\n        }\n\n        console.log('🦖 Auto setup successful, client created:', result.client?.name);\n\n        return NextResponse.json({\n            success: true,\n            client: {\n                id: result.client!.id,\n                name: result.client!.name,\n                clientId: result.client!.clientId,\n                redirectUri: result.client!.redirectUri\n            },\n            message: 'OAuth client created and configured successfully'\n        });\n\n    } catch (error) {\n        console.error('🦖 Auto OAuth setup error:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {\n                    success: false,\n                    error: 'Validation failed',\n                    details: error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')\n                },\n                { status: 400 }\n            );\n        }\n\n        return NextResponse.json(\n            {\n                success: false,\n                error: 'Internal server error',\n                details: error instanceof Error ? error.message : 'Unknown error'\n            },\n            { status: 500 }\n        );\n    }\n}\n\n/**\n * @method GET\n * @description Check auto OAuth setup availability and status\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"available\": { \"type\": \"boolean\" },\n *     \"connected\": { \"type\": \"boolean\" },\n *     \"serverInfo\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"serverUrl\": { \"type\": \"string\" },\n *         \"hasApiKey\": { \"type\": \"boolean\" },\n *         \"isConfigured\": { \"type\": \"boolean\" }\n *       }\n *     },\n *     \"error\": { \"type\": \"string\" }\n *   }\n * }\n * @error 500 Internal server error\n */\nexport async function GET() {\n    try {\n        // Block access once setup is complete\n        const userCount = await db.user.count({\n            where: { email: { not: { endsWith: '@changerawr.sys' } } }\n        })\n        if (userCount > 0) {\n            return NextResponse.json({ error: 'Setup already completed' }, { status: 403 })\n        }\n\n        // Get server configuration info\n        const serverInfo = getOAuthServerInfo();\n\n        // Verify configuration if available\n        const verification = await verifyAutoSetupConfiguration();\n\n        return NextResponse.json({\n            available: verification.available,\n            connected: verification.connected,\n            serverInfo,\n            error: verification.error\n        });\n\n    } catch (error) {\n        console.error('Auto OAuth status check error:', error);\n        return NextResponse.json(\n            {\n                available: false,\n                connected: false,\n                serverInfo: getOAuthServerInfo(),\n                error: error instanceof Error ? error.message : 'Unknown error'\n            },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/setup/oauth/debug/route.ts",
    "content": "// app/api/setup/oauth/debug/route.ts\nimport { NextResponse } from 'next/server';\nimport {\n    getOAuthServerInfo\n} from '@/lib/auth/providers/easypanel/auto-setup';\nimport {isAutoOAuthAvailable} from \"@/lib/auth/providers/easypanel/client\";\nimport { db } from '@/lib/db';\n\n/**\n * @method GET\n * @description Debug endpoint to check OAuth auto setup configuration\n * @response 200 Debug information about OAuth setup\n */\nexport async function GET() {\n    try {\n        // Only accessible during initial setup\n        const userCount = await db.user.count({\n            where: { email: { not: { endsWith: '@changerawr.sys' } } }\n        })\n        if (userCount > 0) {\n            return NextResponse.json({ error: 'Setup already completed' }, { status: 403 })\n        }\n\n        const serverInfo = getOAuthServerInfo();\n        const isAvailable = isAutoOAuthAvailable();\n\n        // Get environment variables (safely)\n        const envVars = {\n            CHR_EPOA2_SERV_URL: process.env.CHR_EPOA2_SERV_URL ?\n                `${process.env.CHR_EPOA2_SERV_URL.substring(0, 20)}...` : 'not set',\n            CHR_EPOA2_SERV_API_KEY: process.env.CHR_EPOA2_SERV_API_KEY ?\n                `${process.env.CHR_EPOA2_SERV_API_KEY.substring(0, 8)}...` : 'not set',\n            NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL || 'not set'\n        };\n\n        return NextResponse.json({\n            timestamp: new Date().toISOString(),\n            autoSetupAvailable: isAvailable,\n            serverInfo,\n            environmentVariables: envVars,\n            callbackUrl: `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/auth/oauth/callback/easypanel`,\n            diagnostics: {\n                hasServerUrl: !!process.env.CHR_EPOA2_SERV_URL,\n                hasApiKey: !!process.env.CHR_EPOA2_SERV_API_KEY,\n                hasAppUrl: !!process.env.NEXT_PUBLIC_APP_URL\n            }\n        });\n\n    } catch (error) {\n        return NextResponse.json({\n            error: 'Debug endpoint failed',\n            message: error instanceof Error ? error.message : 'Unknown error',\n            timestamp: new Date().toISOString()\n        }, { status: 500 });\n    }\n}\n\n/**\n * @method POST\n * @description Test the auto setup schema validation\n */\nexport async function POST(request: Request) {\n    try {\n        // Only accessible during initial setup\n        const userCount = await db.user.count({\n            where: { email: { not: { endsWith: '@changerawr.sys' } } }\n        })\n        if (userCount > 0) {\n            return NextResponse.json({ success: false, error: 'Setup already completed' }, { status: 403 })\n        }\n\n        const body = await request.json();\n\n        // Import the schema from the main route\n        const { z } = await import('zod');\n\n        const testSchema = z.object({\n            appName: z.string().optional().or(z.literal('')).transform(val => val || undefined),\n            redirectUri: z.string().url().optional().or(z.literal('')).transform(val => val || undefined),\n            persistent: z.boolean().optional().default(true),\n        }).transform(data => ({\n            appName: data.appName,\n            redirectUri: data.redirectUri,\n            persistent: data.persistent ?? true\n        }));\n\n        const validated = testSchema.parse(body);\n\n        return NextResponse.json({\n            success: true,\n            receivedBody: body,\n            validatedData: validated,\n            timestamp: new Date().toISOString()\n        });\n\n    } catch (error) {\n        return NextResponse.json({\n            success: false,\n            error: error instanceof Error ? error.message : 'Unknown validation error',\n            receivedBody: await request.json().catch(() => 'Failed to parse body'),\n            timestamp: new Date().toISOString()\n        }, { status: 400 });\n    }\n}"
  },
  {
    "path": "app/api/setup/oauth/route.ts",
    "content": "import {NextResponse} from 'next/server';\nimport {z} from 'zod';\nimport {setupEasypanelProvider} from '@/lib/auth/providers/easypanel';\nimport {setupPocketIDProvider} from '@/lib/auth/providers/pocketid';\nimport {db} from '@/lib/db';\n\n/**\n * Schema for validating OAuth provider setup request.\n */\nconst oauthSetupSchema = z.object({\n    provider: z.string(),\n    baseUrl: z.string().url('Base URL must be a valid URL'),\n    clientId: z.string().min(1, 'Client ID is required'),\n    clientSecret: z.string().min(1, 'Client Secret is required')\n});\n\n/**\n * @method POST\n * @description Sets up an OAuth provider during initial system setup\n */\nexport async function POST(request: Request) {\n    try {\n        // Block access once setup is complete\n        const userCount = await db.user.count({\n            where: { email: { not: { endsWith: '@changerawr.sys' } } }\n        })\n        if (userCount > 0) {\n            return NextResponse.json(\n                { error: 'Setup already completed. Use the admin panel to manage OAuth providers.' },\n                { status: 403 }\n            )\n        }\n\n        // Validate request data\n        const body = await request.json();\n        const {provider, baseUrl, clientId, clientSecret} = oauthSetupSchema.parse(body);\n\n        let result;\n\n        // Set up appropriate provider\n        switch (provider.toLowerCase()) {\n            case 'easypanel':\n                result = await setupEasypanelProvider({\n                    baseUrl,\n                    clientId,\n                    clientSecret\n                });\n                break;\n            case 'pocketid':\n                result = await setupPocketIDProvider({\n                    baseUrl,\n                    clientId,\n                    clientSecret\n                });\n                break;\n            default:\n                return NextResponse.json(\n                    {error: `Unsupported provider: ${provider}`},\n                    {status: 400}\n                );\n        }\n\n        return NextResponse.json({\n            success: true,\n            provider: {\n                id: result.id,\n                name: result.name\n            }\n        });\n    } catch (error) {\n        console.error('OAuth setup error:', (error as Error).stack);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {error: 'Validation failed', details: error.errors},\n                {status: 400}\n            );\n        }\n\n        return NextResponse.json(\n            {error: 'Failed to set up OAuth provider'},\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/setup/route.ts",
    "content": "// app/api/setup/route.ts\nimport {NextResponse} from 'next/server';\nimport {db} from '@/lib/db';\nimport {SetupProgress} from '@/app/api/setup/types';\nimport {SetupStep} from '@/components/setup/setup-context';\n\n/**\n * @method GET\n * @description Get current setup progress including team invitations\n */\nexport async function GET() {\n    try {\n        // Check database state to determine progress\n        const userCount = await db.user.count({\n            where: {\n                email: {\n                    not: {\n                        endsWith: '@changerawr.sys'\n                    }\n                }\n            }\n        });\n        const systemConfig = await db.systemConfig.findFirst();\n        const oauthProviders = await db.oAuthProvider.count();\n        const invitationCount = await db.invitationLink.count();\n\n        console.log('🔍 Setup API: Database state check:', {\n            userCount,\n            hasSystemConfig: !!systemConfig,\n            oauthProviders,\n            invitationCount\n        });\n\n        const progress: SetupProgress = {\n            currentStep: 'welcome', // Always start with welcome\n            completedSteps: [],\n            adminCreated: userCount > 0,\n            settingsConfigured: !!systemConfig,\n            oauthConfigured: oauthProviders > 0,\n            teamInvitesSent: invitationCount > 0,\n            isComplete: userCount > 0 && !!systemConfig\n        };\n\n        // ENFORCE PROPER STEP ORDER - each step requires previous steps\n        // Step 1: Admin must be created first\n        if (progress.adminCreated) {\n            progress.completedSteps.push('admin');\n            console.log('✅ Setup API: Admin step completed');\n\n            // Step 2: Settings can only be completed if admin exists\n            if (progress.settingsConfigured) {\n                progress.completedSteps.push('settings');\n                console.log('✅ Setup API: Settings step completed');\n\n                // Step 3: OAuth can only be completed if settings exist\n                if (progress.oauthConfigured) {\n                    progress.completedSteps.push('oauth');\n                    console.log('✅ Setup API: OAuth step completed');\n\n                    // Step 4: Team invites can only be sent if OAuth is configured\n                    if (progress.teamInvitesSent) {\n                        progress.completedSteps.push('team');\n                        console.log('✅ Setup API: Team step completed');\n                    }\n                }\n            }\n        }\n\n        // Determine current step based on completed steps\n        if (progress.completedSteps.length === 0) {\n            progress.currentStep = 'welcome';\n            console.log('🎬 Setup API: Starting with welcome (no steps completed)');\n        } else {\n            const lastCompleted = progress.completedSteps[progress.completedSteps.length - 1];\n            const stepOrder: SetupStep[] = ['welcome', 'admin', 'settings', 'oauth', 'team', 'complete'];\n            const nextStepIndex = stepOrder.indexOf(lastCompleted as SetupStep) + 1;\n            progress.currentStep = stepOrder[nextStepIndex] || 'complete';\n            console.log(`🔄 Setup API: Last completed: ${lastCompleted}, next step: ${progress.currentStep}`);\n        }\n\n        // Mark as complete only when all required steps are done\n        if (progress.completedSteps.includes('admin') && progress.completedSteps.includes('settings')) {\n            if (progress.completedSteps.includes('team') || progress.completedSteps.includes('oauth')) {\n                progress.currentStep = 'complete';\n                progress.completedSteps.push('complete');\n                progress.isComplete = true;\n                console.log('🎉 Setup API: Setup is complete!');\n            }\n        }\n\n        console.log('📊 Setup API: Final progress:', progress);\n        return NextResponse.json(progress);\n    } catch (error) {\n        console.error('❌ Setup API: Error occurred:', error);\n        return NextResponse.json(\n            {error: 'Failed to get setup progress'},\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/setup/settings/route.ts",
    "content": "import {NextResponse} from 'next/server'\nimport {z} from 'zod'\nimport {db} from '@/lib/db'\n\n/**\n * Schema for validating system settings request body.\n */\nconst settingsSchema = z.object({\n    defaultInvitationExpiry: z.number().min(1).max(30).default(7),\n    requireApprovalForChangelogs: z.boolean().default(true),\n    maxChangelogEntriesPerProject: z.number().min(10).max(10000).default(100),\n    enableAnalytics: z.boolean().default(true),\n    enableNotifications: z.boolean().default(true),\n    timezone: z.string().min(1).max(100).default('UTC'),\n})\n\n/**\n * @method POST\n * @summary Initialize System Settings\n * @description Sets up the initial system configuration. This endpoint can only be called once,\n * before any system settings are configured.\n * @body {\n *   \"type\": \"object\",\n *   \"required\": [\"defaultInvitationExpiry\", \"requireApprovalForChangelogs\", \"maxChangelogEntriesPerProject\", \"enableAnalytics\", \"enableNotifications\"],\n *   \"properties\": {\n *     \"defaultInvitationExpiry\": {\n *       \"type\": \"integer\",\n *       \"minimum\": 1,\n *       \"maximum\": 30,\n *       \"default\": 7,\n *       \"description\": \"Default expiry duration (in days) for invitations\"\n *     },\n *     \"requireApprovalForChangelogs\": {\n *       \"type\": \"boolean\",\n *       \"default\": true,\n *       \"description\": \"Whether changelog entries require approval before publishing\"\n *     },\n *     \"maxChangelogEntriesPerProject\": {\n *       \"type\": \"integer\",\n *       \"minimum\": 10,\n *       \"maximum\": 1000,\n *       \"default\": 100,\n *       \"description\": \"Maximum number of changelog entries allowed per project\"\n *     },\n *     \"enableAnalytics\": {\n *       \"type\": \"boolean\",\n *       \"default\": true,\n *       \"description\": \"Whether analytics collection is enabled\"\n *     },\n *     \"enableNotifications\": {\n *       \"type\": \"boolean\",\n *       \"default\": true,\n *       \"description\": \"Whether system notifications are enabled\"\n *     }\n *   }\n * }\n * @response 201 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"message\": {\n *       \"type\": \"string\",\n *       \"example\": \"System settings configured successfully\"\n *     },\n *     \"config\": {\n *       \"type\": \"object\",\n *       \"required\": [\"id\", \"defaultInvitationExpiry\", \"requireApprovalForChangelogs\", \"maxChangelogEntriesPerProject\", \"enableAnalytics\", \"enableNotifications\"],\n *       \"properties\": {\n *         \"id\": {\n *           \"type\": \"integer\",\n *           \"example\": 1,\n *           \"description\": \"System configuration ID\"\n *         },\n *         \"defaultInvitationExpiry\": {\n *           \"type\": \"integer\",\n *           \"example\": 7,\n *           \"description\": \"Configured invitation expiry in days\"\n *         },\n *         \"requireApprovalForChangelogs\": {\n *           \"type\": \"boolean\",\n *           \"example\": true,\n *           \"description\": \"Whether changelog approval is required\"\n *         },\n *         \"maxChangelogEntriesPerProject\": {\n *           \"type\": \"integer\",\n *           \"example\": 100,\n *           \"description\": \"Maximum changelog entries per project\"\n *         },\n *         \"enableAnalytics\": {\n *           \"type\": \"boolean\",\n *           \"example\": true,\n *           \"description\": \"Analytics status\"\n *         },\n *         \"enableNotifications\": {\n *           \"type\": \"boolean\",\n *           \"example\": true,\n *           \"description\": \"Notifications status\"\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 400 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": {\n *       \"type\": \"string\",\n *       \"example\": \"Validation failed\"\n *     },\n *     \"details\": {\n *       \"type\": \"array\",\n *       \"items\": {\n *         \"type\": \"object\",\n *         \"properties\": {\n *           \"path\": {\n *             \"type\": \"string\",\n *             \"example\": \"defaultInvitationExpiry\"\n *           },\n *           \"message\": {\n *             \"type\": \"string\",\n *             \"example\": \"Number must be between 1 and 30\"\n *           }\n *         }\n *       }\n *     }\n *   }\n * }\n * @error 500 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": {\n *       \"type\": \"string\",\n *       \"example\": \"An unexpected error occurred during configuration\"\n *     }\n *   }\n * }\n */\nexport async function POST(request: Request) {\n    try {\n        // Block access once setup is complete (admin user exists)\n        const userCount = await db.user.count({\n            where: { email: { not: { endsWith: '@changerawr.sys' } } }\n        })\n        if (userCount > 0) {\n            return NextResponse.json(\n                { error: 'Setup already completed. Use the admin panel to manage system settings.' },\n                { status: 403 }\n            )\n        }\n\n        // Validate request data first\n        const body = await request.json()\n        const validatedData = settingsSchema.parse(body)\n\n        // Check if settings already exist\n        const existingConfig = await db.systemConfig.findFirst()\n\n        let config;\n        if (existingConfig) {\n            // Update existing settings\n            config = await db.systemConfig.update({\n                where: {id: existingConfig.id},\n                data: {\n                    defaultInvitationExpiry: validatedData.defaultInvitationExpiry,\n                    requireApprovalForChangelogs: validatedData.requireApprovalForChangelogs,\n                    maxChangelogEntriesPerProject: validatedData.maxChangelogEntriesPerProject,\n                    enableAnalytics: validatedData.enableAnalytics,\n                    enableNotifications: validatedData.enableNotifications,\n                    timezone: validatedData.timezone,\n                }\n            })\n        } else {\n            // Create new settings\n            config = await db.systemConfig.create({\n                data: {\n                    id: 1,\n                    defaultInvitationExpiry: validatedData.defaultInvitationExpiry,\n                    requireApprovalForChangelogs: validatedData.requireApprovalForChangelogs,\n                    maxChangelogEntriesPerProject: validatedData.maxChangelogEntriesPerProject,\n                    enableAnalytics: validatedData.enableAnalytics,\n                    enableNotifications: validatedData.enableNotifications,\n                    timezone: validatedData.timezone,\n                }\n            })\n        }\n\n        return NextResponse.json({\n            message: existingConfig ? 'System settings updated successfully' : 'System settings configured successfully',\n            config\n        }, {status: existingConfig ? 200 : 201})\n    } catch (error) {\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                {\n                    error: 'Validation failed',\n                    details: error.errors.map(e => ({\n                        path: e.path.join('.'),\n                        message: e.message\n                    }))\n                },\n                {status: 400}\n            )\n        }\n\n        console.error('Settings setup error:', error)\n        return NextResponse.json(\n            {error: 'An unexpected error occurred during configuration'},\n            {status: 500}\n        )\n    }\n}\n\n/**\n * @method GET\n * @summary Method Not Allowed\n * @description This endpoint only accepts POST requests for system configuration\n * @response 405 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"error\": {\n *       \"type\": \"string\",\n *       \"example\": \"Method not allowed\"\n *     }\n *   }\n * }\n */\nexport async function GET() {\n    return NextResponse.json(\n        {error: 'Method not allowed'},\n        {status: 405}\n    )\n}"
  },
  {
    "path": "app/api/setup/setupProgressSchema.ts",
    "content": "// Schema for tracking setup progress\nimport {z} from \"zod\";\n\nexport const setupProgressSchema = z.object({\n    currentStep: z.string(),\n    completedSteps: z.array(z.string()),\n    adminCreated: z.boolean().default(false),\n    settingsConfigured: z.boolean().default(false),\n    oauthConfigured: z.boolean().optional(),\n    teamInvitesSent: z.boolean().default(false),\n    isComplete: z.boolean().default(false)\n});"
  },
  {
    "path": "app/api/setup/status/route.ts",
    "content": "import { NextResponse } from \"next/server\"\nimport { db } from \"@/lib/db\"\n\n/**\n * @method GET\n * @description Checks if the initial setup has been completed and provides setup state information\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"isComplete\": {\n *       \"type\": \"boolean\",\n *       \"description\": \"Indicates whether the setup has been completed\"\n *     },\n *     \"setupState\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"adminCreated\": { \"type\": \"boolean\" },\n *         \"systemConfigured\": { \"type\": \"boolean\" },\n *         \"oauthConfigured\": { \"type\": \"boolean\" }\n *       }\n *     }\n *   }\n * }\n * @error 500 An unexpected error occurred while checking setup status\n */\nexport async function GET() {\n    try {\n        // Add cache headers to prevent frequent checks\n        const responseHeaders = new Headers();\n        responseHeaders.set('Cache-Control', 'max-age=5');\n\n        // Check if any *non-system* user exists\n        const userCount = await db.user.count({\n            where: {\n                email: {\n                    not: {\n                        endsWith: '@changerawr.sys'\n                    }\n                }\n            }\n        });\n\n        // Check if system configuration exists\n        const systemConfig = await db.systemConfig.findFirst();\n\n        // Check if any OAuth providers are configured\n        const oauthProviders = await db.oAuthProvider.count();\n\n        // Determine if setup is complete (minimum requirements)\n        const isComplete = userCount > 0 && !!systemConfig;\n\n        return NextResponse.json({\n            isComplete,\n            setupState: {\n                adminCreated: userCount > 0,\n                systemConfigured: !!systemConfig,\n                oauthConfigured: oauthProviders > 0\n            }\n        }, {\n            status: 200,\n            headers: responseHeaders\n        });\n    } catch (error) {\n        console.error('Setup status check error:', error);\n\n        // Even on error, set cache headers to prevent thundering herd\n        const responseHeaders = new Headers();\n        responseHeaders.set('Cache-Control', 'max-age=1');\n\n        return NextResponse.json(\n            {\n                error: 'Failed to check setup status',\n                isComplete: false,\n                setupState: {\n                    adminCreated: false,\n                    systemConfigured: false,\n                    oauthConfigured: false\n                }\n            },\n            {\n                status: 500,\n                headers: responseHeaders\n            }\n        );\n    }\n}"
  },
  {
    "path": "app/api/setup/types.ts",
    "content": "import { z } from 'zod';\nimport { setupProgressSchema } from './setupProgressSchema';\n\nexport const setupSteps = [\n    'welcome',  // Added welcome step\n    'admin',\n    'settings',\n    'oauth',\n    'team',\n    'complete'\n] as const;\n\nexport type SetupProgress = z.infer<typeof setupProgressSchema>;"
  },
  {
    "path": "app/api/subscribers/[subscriberId]/route.ts",
    "content": "// app/api/subscribers/[subscriberId]/route.ts\n\nimport { NextResponse } from 'next/server';\nimport { db } from '@/lib/db';\nimport { z } from 'zod';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\n\n// Validation schema for subscriber updates\nconst updateSubscriberSchema = z.object({\n    name: z.string().optional(),\n    isActive: z.boolean().optional(),\n    subscriptionType: z.enum(['ALL_UPDATES', 'MAJOR_ONLY', 'DIGEST_ONLY']).optional(),\n});\n\n/**\n * @method PATCH\n * @description Updates a subscriber's information or subscription preferences\n */\nexport async function PATCH(\n    request: Request,\n    context: { params: Promise<{ subscriberId: string }> }\n): Promise<Response> {\n    try {\n        // Authentication check\n        await validateAuthAndGetUser();\n\n        const { subscriberId } = await context.params;\n        const { searchParams } = new URL(request.url);\n        const projectId = searchParams.get('projectId');\n        const body = await request.json();\n\n        // Validate input\n        const validatedData = updateSubscriberSchema.parse(body);\n\n        if (!subscriberId) {\n            return NextResponse.json(\n                { error: 'Subscriber ID is required' },\n                { status: 400 }\n            );\n        }\n\n        // Check if subscriber exists\n        const subscriber = await db.emailSubscriber.findUnique({\n            where: { id: subscriberId },\n            include: {\n                subscriptions: projectId\n                    ? { where: { projectId } }\n                    : undefined,\n            },\n        });\n\n        if (!subscriber) {\n            return NextResponse.json(\n                { error: 'Subscriber not found' },\n                { status: 404 }\n            );\n        }\n\n        // Start transaction for related updates\n        return await db.$transaction(async (tx) => {\n            // Update subscriber info\n            if (validatedData.name !== undefined || validatedData.isActive !== undefined) {\n                await tx.emailSubscriber.update({\n                    where: { id: subscriberId },\n                    data: {\n                        ...(validatedData.name !== undefined && { name: validatedData.name }),\n                        ...(validatedData.isActive !== undefined && { isActive: validatedData.isActive }),\n                    },\n                });\n            }\n\n            // Update subscription type if both projectId and subscriptionType are provided\n            if (projectId && validatedData.subscriptionType) {\n                const subscription = subscriber.subscriptions?.[0];\n\n                if (subscription) {\n                    await tx.projectSubscription.update({\n                        where: { id: subscription.id },\n                        data: { subscriptionType: validatedData.subscriptionType },\n                    });\n                } else {\n                    await tx.projectSubscription.create({\n                        data: {\n                            subscriberId,\n                            projectId,\n                            subscriptionType: validatedData.subscriptionType,\n                        },\n                    });\n                }\n            }\n\n            return NextResponse.json({\n                success: true,\n                message: 'Subscriber updated successfully',\n            });\n        });\n    } catch (error) {\n        console.error('Error updating subscriber:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: 'Validation failed', details: error.errors },\n                { status: 400 }\n            );\n        }\n\n        return NextResponse.json(\n            { error: 'Failed to update subscriber' },\n            { status: 500 }\n        );\n    }\n}\n\n/**\n * @method DELETE\n * @description Removes a subscriber from a project or deletes them entirely\n */\nexport async function DELETE(\n    request: Request,\n    context: { params: Promise<{ subscriberId: string }> }\n): Promise<Response> {\n    try {\n        // Authentication check\n        await validateAuthAndGetUser();\n\n        const { subscriberId } = await context.params;\n        const { searchParams } = new URL(request.url);\n        const projectId = searchParams.get('projectId');\n\n        if (!subscriberId) {\n            return NextResponse.json(\n                { error: 'Subscriber ID is required' },\n                { status: 400 }\n            );\n        }\n\n        // Check if subscriber exists\n        const subscriber = await db.emailSubscriber.findUnique({\n            where: { id: subscriberId },\n            include: {\n                subscriptions: true,\n            },\n        });\n\n        if (!subscriber) {\n            return NextResponse.json(\n                { error: 'Subscriber not found' },\n                { status: 404 }\n            );\n        }\n\n        if (projectId) {\n            // Remove from specific project\n            const subscription = subscriber.subscriptions.find(\n                (sub) => sub.projectId === projectId\n            );\n\n            if (subscription) {\n                await db.projectSubscription.delete({\n                    where: { id: subscription.id },\n                });\n            } else {\n                return NextResponse.json(\n                    { error: 'Subscription not found for this project' },\n                    { status: 404 }\n                );\n            }\n        } else {\n            // Remove subscriber entirely (including all subscriptions)\n            await db.emailSubscriber.delete({\n                where: { id: subscriberId },\n            });\n        }\n\n        return NextResponse.json({\n            success: true,\n            message: 'Subscriber removed successfully',\n        });\n    } catch (error) {\n        console.error('Error removing subscriber:', error);\n        return NextResponse.json(\n            { error: 'Failed to remove subscriber' },\n            { status: 500 }\n        );\n    }\n}\n"
  },
  {
    "path": "app/api/subscribers/generate-mock/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { db } from '@/lib/db';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { nanoid } from 'nanoid';\n\n/**\n * @method POST\n * @description Development-only endpoint to generate mock subscribers for testing\n * @query projectId - Project ID to create subscribers for\n * @query count - Number of subscribers to generate (default: 20, max: 100)\n */\nexport async function POST(request: Request) {\n    // Only allow in development\n    if (process.env.NODE_ENV !== 'development') {\n        return NextResponse.json(\n            { error: 'This endpoint is only available in development mode' },\n            { status: 403 }\n        );\n    }\n\n    try {\n        // Authentication check\n        const user = await validateAuthAndGetUser();\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                { error: 'Only admins can generate mock data' },\n                { status: 403 }\n            );\n        }\n\n        const { searchParams } = new URL(request.url);\n        const projectId = searchParams.get('projectId');\n        let count = parseInt(searchParams.get('count') || '20');\n\n        // Validate count and limit it to a reasonable number\n        count = Math.min(Math.max(1, count), 100);\n\n        if (!projectId) {\n            return NextResponse.json(\n                { error: 'Project ID is required' },\n                { status: 400 }\n            );\n        }\n\n        // Verify project exists\n        const project = await db.project.findUnique({\n            where: { id: projectId }\n        });\n\n        if (!project) {\n            return NextResponse.json(\n                { error: 'Project not found' },\n                { status: 404 }\n            );\n        }\n\n        // Generate names for mock data\n        const firstNames = [\n            'John', 'Jane', 'Alex', 'Emma', 'Michael', 'Sarah', 'David', 'Lisa',\n            'Robert', 'Maria', 'James', 'Jennifer', 'William', 'Linda', 'Richard',\n            'Emily', 'Thomas', 'Jessica', 'Daniel', 'Susan'\n        ];\n\n        const lastNames = [\n            'Smith', 'Johnson', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis',\n            'Rodriguez', 'Martinez', 'Wilson', 'Anderson', 'Taylor', 'Thomas',\n            'Moore', 'Jackson', 'Martin', 'Lee', 'Thompson', 'White', 'Harris'\n        ];\n\n        // Generate domains for mock emails\n        const domains = [\n            'gmail.com', 'outlook.com', 'yahoo.com', 'hotmail.com',\n            'protonmail.com', 'icloud.com', 'aol.com', 'mail.com',\n            'company.com', 'example.org', 'test.co', 'dev.io'\n        ];\n\n        // Subscription types\n        const subscriptionTypes = ['ALL_UPDATES', 'MAJOR_ONLY', 'DIGEST_ONLY'];\n\n        // Create batch of subscribers\n        const createdSubscribers = [];\n\n        for (let i = 0; i < count; i++) {\n            // Generate random mock data\n            const firstName = firstNames[Math.floor(Math.random() * firstNames.length)];\n            const lastName = lastNames[Math.floor(Math.random() * lastNames.length)];\n            const domain = domains[Math.floor(Math.random() * domains.length)];\n            const subscriptionType = subscriptionTypes[\n                Math.floor(Math.random() * subscriptionTypes.length)\n                ] as 'ALL_UPDATES' | 'MAJOR_ONLY' | 'DIGEST_ONLY';\n\n            // Random timestamp between 1 and 365 days ago\n            const daysAgo = Math.floor(Math.random() * 365) + 1;\n            const createdAt = new Date();\n            createdAt.setDate(createdAt.getDate() - daysAgo);\n\n            // Random decision if this subscriber has received an email\n            const hasReceivedEmail = Math.random() > 0.3;\n            const lastEmailDaysAgo = hasReceivedEmail ? Math.floor(Math.random() * daysAgo) : null;\n            const lastEmailSentAt = lastEmailDaysAgo\n                ? new Date(Date.now() - lastEmailDaysAgo * 24 * 60 * 60 * 1000)\n                : null;\n\n            // Create a unique email (avoid duplicates)\n            const uniqueSuffix = Math.floor(Math.random() * 10000);\n            const email = `${firstName.toLowerCase()}.${lastName.toLowerCase()}${uniqueSuffix}@${domain}`;\n\n            try {\n                // Check if email already exists\n                const existingSubscriber = await db.emailSubscriber.findUnique({\n                    where: { email }\n                });\n\n                if (existingSubscriber) {\n                    // Skip creating this subscriber\n                    continue;\n                }\n\n                // Create new subscriber\n                const subscriber = await db.emailSubscriber.create({\n                    data: {\n                        email,\n                        name: `${firstName} ${lastName}`,\n                        unsubscribeToken: nanoid(32),\n                        isActive: true,\n                        createdAt,\n                        lastEmailSentAt,\n                        subscriptions: {\n                            create: {\n                                projectId,\n                                subscriptionType\n                            }\n                        }\n                    }\n                });\n\n                createdSubscribers.push(subscriber);\n            } catch (err) {\n                console.error(`Failed to create mock subscriber ${email}:`, err);\n                // Continue with the next subscriber\n            }\n        }\n\n        return NextResponse.json({\n            success: true,\n            message: `Created ${createdSubscribers.length} mock subscribers for project`,\n            count: createdSubscribers.length\n        });\n    } catch (error) {\n        console.error('Error generating mock subscribers:', error);\n        return NextResponse.json(\n            { error: 'Failed to generate mock subscribers' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/subscribers/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { z } from 'zod';\nimport { db } from '@/lib/db';\nimport { nanoid } from 'nanoid';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\n\n/**\n * @method POST\n * @description Creates or updates a subscription for a user to receive email notifications for a project\n */\nexport async function POST(request: Request) {\n    try {\n        const subscriberSchema = z.object({\n            email: z.string().email('Invalid email format'),\n            projectId: z.string().min(1, 'Project ID is required'),\n            name: z.string().optional(),\n            subscriptionType: z.enum(['ALL_UPDATES', 'MAJOR_ONLY', 'DIGEST_ONLY']).default('ALL_UPDATES')\n        });\n\n        // Parse and validate request body\n        const body = await request.json();\n        const { email, projectId, name, subscriptionType } = subscriberSchema.parse(body);\n\n        // Verify project exists and is public\n        const project = await db.project.findUnique({\n            where: { id: projectId }\n        });\n\n        if (!project) {\n            return NextResponse.json(\n                { error: 'Project not found' },\n                { status: 404 }\n            );\n        }\n\n        // Find or create subscriber\n        let subscriber = await db.emailSubscriber.findUnique({\n            where: { email },\n            include: {\n                subscriptions: {\n                    where: { projectId }\n                }\n            }\n        });\n\n        if (!subscriber) {\n            // Create new subscriber\n            subscriber = await db.emailSubscriber.create({\n                data: {\n                    email,\n                    name,\n                    unsubscribeToken: nanoid(32),\n                    isActive: true,\n                },\n                include: {\n                    subscriptions: {\n                        where: { projectId }\n                    }\n                }\n            });\n        } else if (name && !subscriber.name) {\n            // Update subscriber name if provided and not set before\n            await db.emailSubscriber.update({\n                where: { id: subscriber.id },\n                data: { name }\n            });\n        }\n\n        // Create or update subscription\n        if (subscriber.subscriptions.length === 0) {\n            // Create new subscription\n            await db.projectSubscription.create({\n                data: {\n                    subscriberId: subscriber.id,\n                    projectId,\n                    subscriptionType\n                }\n            });\n        } else {\n            // Update existing subscription\n            await db.projectSubscription.update({\n                where: { id: subscriber.subscriptions[0].id },\n                data: { subscriptionType }\n            });\n        }\n\n        return NextResponse.json({\n            success: true,\n            message: 'Subscription created successfully'\n        });\n    } catch (error) {\n        console.error('Error creating subscription:', error);\n\n        if (error instanceof z.ZodError) {\n            return NextResponse.json(\n                { error: 'Validation failed', details: error.errors },\n                { status: 400 }\n            );\n        }\n\n        return NextResponse.json(\n            { error: 'Failed to create subscription' },\n            { status: 500 }\n        );\n    }\n}\n\n/**\n * @method GET\n * @description List subscribers for a project with pagination and search functionality\n * @query projectId - Project ID to fetch subscribers for\n * @query page - Page number (default: 1)\n * @query limit - Results per page (default: 10)\n * @query search - Optional search term for email or name\n */\nexport async function GET(request: Request) {\n    try {\n        // Authentication check\n        await validateAuthAndGetUser();\n\n        const { searchParams } = new URL(request.url);\n        const projectId = searchParams.get('projectId');\n        const page = parseInt(searchParams.get('page') || '1');\n        const limit = parseInt(searchParams.get('limit') || '10');\n        const search = searchParams.get('search') || '';\n\n        if (!projectId) {\n            return NextResponse.json(\n                { error: 'Project ID is required' },\n                { status: 400 }\n            );\n        }\n\n        // Make sure values are valid\n        const validPage = Math.max(1, page);\n        const validLimit = Math.min(Math.max(1, limit), 100); // Max 100 per page\n        const skip = (validPage - 1) * validLimit;\n\n        // Build search conditions\n        const searchCondition = search\n            ? {\n                OR: [\n                    { email: { contains: search, mode: 'insensitive' as const } },\n                    { name: { contains: search, mode: 'insensitive' as const } }\n                ]\n            }\n            : {};\n\n        // Query with pagination and search\n        const [subscribers, totalCount] = await Promise.all([\n            db.emailSubscriber.findMany({\n                where: {\n                    ...searchCondition,\n                    subscriptions: {\n                        some: { projectId }\n                    },\n                    isActive: true\n                },\n                include: {\n                    subscriptions: {\n                        where: { projectId }\n                    }\n                },\n                orderBy: {\n                    createdAt: 'desc'\n                },\n                skip,\n                take: validLimit\n            }),\n            db.emailSubscriber.count({\n                where: {\n                    ...searchCondition,\n                    subscriptions: {\n                        some: { projectId }\n                    },\n                    isActive: true\n                }\n            })\n        ]);\n\n        // Format response\n        return NextResponse.json({\n            subscribers: subscribers.map(s => ({\n                id: s.id,\n                email: s.email,\n                name: s.name,\n                subscriptionType: s.subscriptions[0]?.subscriptionType || 'ALL_UPDATES',\n                createdAt: s.createdAt,\n                lastEmailSentAt: s.lastEmailSentAt\n            })),\n            page: validPage,\n            limit: validLimit,\n            totalCount,\n            totalPages: Math.ceil(totalCount / validLimit)\n        });\n    } catch (error) {\n        console.error('Error fetching subscribers:', error);\n        return NextResponse.json(\n            { error: 'Failed to fetch subscribers' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/system/agent-version/route.ts",
    "content": "import { NextResponse } from 'next/server'\n\nexport const runtime = 'nodejs'\n\n/**\n * Public API to fetch nginx-agent version (only if SSL is enabled).\n * This wraps the internal API call with proper authentication.\n */\nexport async function GET() {\n    const sslEnabled = process.env.NEXT_PUBLIC_SSL_ENABLED === 'true'\n\n    if (!sslEnabled) {\n        return NextResponse.json(\n            { error: 'SSL not enabled' },\n            { status: 404 },\n        )\n    }\n\n    const agentUrl = process.env.NGINX_AGENT_URL\n    const internalSecret = process.env.INTERNAL_API_SECRET\n\n    if (!agentUrl || !internalSecret) {\n        return NextResponse.json(\n            { error: 'Agent not configured' },\n            { status: 503 },\n        )\n    }\n\n    try {\n        const response = await fetch(`${agentUrl}/version`, {\n            signal: AbortSignal.timeout(5000),\n        })\n\n        if (!response.ok) {\n            return NextResponse.json(\n                { error: 'Failed to fetch agent version' },\n                { status: 502 },\n            )\n        }\n\n        const data = await response.json()\n        return NextResponse.json(data)\n    } catch (error) {\n        console.error('[api/system/agent-version] Error:', error)\n        return NextResponse.json(\n            { error: 'Failed to connect to nginx-agent' },\n            { status: 503 },\n        )\n    }\n}\n"
  },
  {
    "path": "app/api/system/easypanel/status/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server';\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog';\nimport {EasypanelService} from '@/lib/services/easypanel';\n\ninterface EasypanelStatusResponse {\n    configured: boolean;\n    connected?: boolean;\n    error?: string;\n    config?: {\n        projectId: string;\n        serviceId: string;\n        panelUrl: string;\n    };\n}\n\n/**\n * Get Easypanel configuration and connection status\n * @method GET\n * @description Checks Easypanel configuration and tests connection\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"configured\": { \"type\": \"boolean\" },\n *     \"connected\": { \"type\": \"boolean\" },\n *     \"error\": { \"type\": \"string\" },\n *     \"config\": {\n *       \"type\": \"object\",\n *       \"properties\": {\n *         \"projectId\": { \"type\": \"string\" },\n *         \"serviceId\": { \"type\": \"string\" },\n *         \"panelUrl\": { \"type\": \"string\" }\n *       }\n *     }\n *   }\n * }\n * @error 401 Unauthorized - Authentication required\n * @error 403 Forbidden - Admin access required\n * @error 500 Internal server error\n * @secure cookieAuth\n */\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport async function GET(request: NextRequest) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                {error: 'Admin access required'},\n                {status: 403}\n            );\n        }\n\n        const status: EasypanelStatusResponse = {\n            configured: EasypanelService.isConfigured(),\n        };\n\n        if (status.configured) {\n            // Add configuration details (without API key)\n            status.config = {\n                projectId: process.env.EASYPANEL_PROJECT_ID!,\n                serviceId: process.env.EASYPANEL_SERVICE_ID!,\n                panelUrl: process.env.EASYPANEL_PANEL_URL!,\n            };\n\n            // Test connection\n            try {\n                const easypanel = EasypanelService.fromEnv();\n                if (easypanel) {\n                    status.connected = await easypanel.testConnection();\n                    if (!status.connected) {\n                        status.error = 'Connection test failed - check credentials and network access';\n                    }\n                } else {\n                    status.connected = false;\n                    status.error = 'Failed to initialize Easypanel service';\n                }\n            } catch (error) {\n                status.connected = false;\n                status.error = error instanceof Error ? error.message : 'Connection test failed';\n            }\n        } else {\n            status.error = 'Missing required environment variables: EASYPANEL_PROJECT_ID, EASYPANEL_SERVICE_ID, EASYPANEL_PANEL_URL, EASYPANEL_API_KEY';\n        }\n\n        return NextResponse.json(status);\n    } catch (error) {\n        console.error('Error checking Easypanel status:', error);\n        return NextResponse.json(\n            {error: 'Failed to check Easypanel status'},\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/system/perform-update/route.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { EasypanelService } from '@/lib/services/easypanel';\nimport { compareVersions } from 'compare-versions';\nimport { appInfo } from '@/lib/app-info';\nimport { generateDockerImage, validateDockerImage } from '@/lib/utils/docker';\n\ninterface PerformUpdateRequest {\n    targetVersion: string;\n    customImage?: string;\n}\n\n/**\n * Perform automatic update via Easypanel\n * @method POST\n * @description Automatically updates the application using Easypanel API\n * @body {\n *   \"type\": \"object\",\n *   \"required\": [\"targetVersion\"],\n *   \"properties\": {\n *     \"targetVersion\": {\n *       \"type\": \"string\",\n *       \"description\": \"Target version to update to\"\n *     },\n *     \"customImage\": {\n *       \"type\": \"string\",\n *       \"description\": \"Optional custom Docker image (overrides default generation)\"\n *     }\n *   }\n * }\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"success\": { \"type\": \"boolean\" },\n *     \"message\": { \"type\": \"string\" },\n *     \"fromVersion\": { \"type\": \"string\" },\n *     \"toVersion\": { \"type\": \"string\" },\n *     \"imageUsed\": { \"type\": \"string\" },\n *     \"estimatedRestartTime\": { \"type\": \"number\" }\n *   }\n * }\n * @error 400 Invalid request or version\n * @error 401 Unauthorized - Authentication required\n * @error 403 Forbidden - Admin access required\n * @error 500 Internal server error\n * @error 503 Easypanel not configured\n * @secure cookieAuth\n */\nexport async function POST(request: NextRequest) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                { error: 'Admin access required' },\n                { status: 403 }\n            );\n        }\n\n        // Check if Easypanel is configured\n        if (!EasypanelService.isConfigured()) {\n            return NextResponse.json(\n                {\n                    error: 'Easypanel not configured',\n                    details: 'EASYPANEL_PROJECT_ID, EASYPANEL_SERVICE_ID, EASYPANEL_PANEL_URL, and EASYPANEL_API_KEY must be set'\n                },\n                { status: 503 }\n            );\n        }\n\n        const body: PerformUpdateRequest = await request.json();\n        const { targetVersion, customImage } = body;\n\n        if (!targetVersion) {\n            return NextResponse.json(\n                { error: 'Target version is required' },\n                { status: 400 }\n            );\n        }\n\n        // Validate that target version is newer than current\n        if (compareVersions(targetVersion, appInfo.version) <= 0) {\n            return NextResponse.json(\n                {\n                    error: 'Invalid target version',\n                    details: `Target version ${targetVersion} is not newer than current version ${appInfo.version}`\n                },\n                { status: 400 }\n            );\n        }\n\n        // Create Easypanel service instance\n        const easypanel = EasypanelService.fromEnv();\n        if (!easypanel) {\n            return NextResponse.json(\n                { error: 'Failed to initialize Easypanel service' },\n                { status: 500 }\n            );\n        }\n\n        // Determine the Docker image to use\n        let dockerImage: string;\n        if (customImage) {\n            // Validate custom image\n            const validation = validateDockerImage(customImage);\n            if (!validation.valid) {\n                return NextResponse.json(\n                    {\n                        error: 'Invalid custom Docker image',\n                        details: validation.error\n                    },\n                    { status: 400 }\n                );\n            }\n            dockerImage = customImage;\n        } else {\n            // Generate standard image\n            dockerImage = generateDockerImage(targetVersion);\n        }\n\n        console.log(`Starting automatic update from ${appInfo.version} to ${targetVersion}`);\n        console.log(`Using Docker image: ${dockerImage}`);\n        console.log(`Easypanel config: ${JSON.stringify(easypanel.getConfig())}`);\n\n        try {\n            // Test connection first\n            const connectionTest = await easypanel.testConnection();\n            if (!connectionTest) {\n                throw new Error('Failed to connect to Easypanel API');\n            }\n\n            // Perform the update\n            await easypanel.performUpdate(targetVersion, customImage);\n\n            return NextResponse.json({\n                success: true,\n                message: 'Update completed successfully. The application is being redeployed.',\n                fromVersion: appInfo.version,\n                toVersion: targetVersion,\n                imageUsed: dockerImage,\n                estimatedRestartTime: 60, // seconds\n            });\n        } catch (updateError) {\n            console.error('Easypanel update failed:', updateError);\n\n            return NextResponse.json(\n                {\n                    error: 'Update failed',\n                    details: updateError instanceof Error ? updateError.message : 'Unknown error occurred'\n                },\n                { status: 500 }\n            );\n        }\n    } catch (error) {\n        console.error('Error performing update:', error);\n        return NextResponse.json(\n            { error: 'Failed to perform update' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/system/update-status/route.ts",
    "content": "// app/api/system/update-status/route.ts\n\nimport { NextRequest, NextResponse } from 'next/server';\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\nimport { appInfo } from '@/lib/app-info';\nimport { EasypanelService } from '@/lib/services/easypanel';\nimport { UpdateStatus } from '@/lib/types/easypanel';\nimport { compareVersions } from 'compare-versions';\n\n/**\n * Get update status including Easypanel configuration\n * @method GET\n * @description Checks for available updates and Easypanel configuration status\n * @response 200 {\n *   \"type\": \"object\",\n *   \"properties\": {\n *     \"available\": { \"type\": \"boolean\" },\n *     \"currentVersion\": { \"type\": \"string\" },\n *     \"latestVersion\": { \"type\": \"string\" },\n *     \"canAutoUpdate\": { \"type\": \"boolean\" },\n *     \"easypanelConfigured\": { \"type\": \"boolean\" }\n *   }\n * }\n * @error 401 Unauthorized - Authentication required\n * @error 403 Forbidden - Admin access required\n * @error 500 Internal server error\n * @secure cookieAuth\n */\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport async function GET(request: NextRequest) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                { error: 'Admin access required' },\n                { status: 403 }\n            );\n        }\n\n        // Check if Easypanel is configured\n        const easypanelConfigured = EasypanelService.isConfigured();\n\n        let latestVersion = appInfo.version;\n        let updateAvailable = false;\n\n        try {\n            // Fetch latest version from update server\n            const response = await fetch('https://dl.supers0ft.us/changerawr/', {\n                // options could go here\n            });\n\n            if (response.ok) {\n                const data = await response.json();\n                latestVersion = data.version || appInfo.version;\n                updateAvailable = compareVersions(latestVersion, appInfo.version) > 0;\n            }\n        } catch (error) {\n            console.warn('Failed to check for updates:', error);\n            // If we can't check for updates, just use current version\n        }\n\n        const updateStatus: UpdateStatus = {\n            available: updateAvailable,\n            currentVersion: appInfo.version,\n            latestVersion,\n            canAutoUpdate: easypanelConfigured && updateAvailable,\n            easypanelConfigured,\n        };\n\n        return NextResponse.json(updateStatus);\n    } catch (error) {\n        console.error('Error checking update status:', error);\n        return NextResponse.json(\n            { error: 'Failed to check update status' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/system/version/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { db } from '@/lib/db';\n\nexport async function GET(): Promise<NextResponse> {\n    try {\n        const result: { version: string }[] = await db.$queryRaw`SELECT version();`;\n        const databaseVersion = result[0]?.version ? result[0].version.split(' ')[1].replace(',', '') : 'Unknown';\n        return NextResponse.json({\n            status: 'success',\n            databaseVersion,\n        });\n    } catch (error) {\n        console.error('Error fetching database version:', error);\n        return NextResponse.json(\n            { status: 'error', message: 'Failed to retrieve database version' },\n            { status: 500 }\n        );\n    }\n}"
  },
  {
    "path": "app/api/telemetry/config/route.ts",
    "content": "// app/api/telemetry/config/route.ts\nimport {NextRequest, NextResponse} from 'next/server';\nimport {TelemetryService} from '@/lib/services/telemetry/service';\n\nexport async function GET() {\n    try {\n        const config = await TelemetryService.getTelemetryConfig();\n        return NextResponse.json(config);\n    } catch (error) {\n        console.error('Failed to get telemetry config:', error);\n        return NextResponse.json(\n            {error: 'Failed to get telemetry configuration'},\n            {status: 500}\n        );\n    }\n}\n\nexport async function POST(request: NextRequest) {\n    try {\n        const body = await request.json();\n        const {allowTelemetry} = body;\n\n        if (!['enabled', 'disabled'].includes(allowTelemetry)) {\n            return NextResponse.json(\n                {error: 'Invalid telemetry state'},\n                {status: 400}\n            );\n        }\n\n        const currentConfig = await TelemetryService.getTelemetryConfig();\n        const updatedConfig = {...currentConfig, allowTelemetry};\n\n        // Handle different telemetry state transitions\n        if (allowTelemetry === 'enabled') {\n            if (!currentConfig.instanceId) {\n                // First time enabling - register new instance\n                console.log('First time enabling telemetry - registering new instance...');\n                const instanceId = await TelemetryService.registerInstance();\n                updatedConfig.instanceId = instanceId;\n            } else if (currentConfig.allowTelemetry === 'disabled') {\n                // Re-enabling - reactivate existing instance\n                console.log('Re-enabling telemetry - reactivating instance:', currentConfig.instanceId);\n                try {\n                    await TelemetryService.reactivateInstance(currentConfig.instanceId);\n                } catch (error) {\n                    console.warn('Failed to reactivate instance:', error);\n                }\n            }\n            // If already enabled, no action needed\n        } else if (allowTelemetry === 'disabled' && currentConfig.instanceId) {\n            // Disabling - deactivate instance\n            console.log('Disabling telemetry - deactivating instance:', currentConfig.instanceId);\n            try {\n                await TelemetryService.deactivateInstance(currentConfig.instanceId);\n            } catch (error) {\n                console.error('Failed to deactivate instance:', error);\n            }\n        }\n\n        console.log('Updating telemetry config:', updatedConfig);\n        await TelemetryService.updateTelemetryConfig(updatedConfig);\n\n        return NextResponse.json({\n            success: true,\n            config: updatedConfig\n        });\n    } catch (error) {\n        console.error('Failed to update telemetry config:', error);\n        return NextResponse.json(\n            {\n                error: 'Failed to update telemetry configuration',\n                details: error instanceof Error ? error.message : 'Unknown error'\n            },\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api/telemetry/debug/route.ts",
    "content": "import {NextRequest, NextResponse} from 'next/server';\nimport {TelemetryService} from '@/lib/services/telemetry/service';\nimport {validateAuthAndGetUser} from '@/lib/utils/changelog';\n\nexport async function GET() {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                {error: 'Admin access required'},\n                {status: 403}\n            );\n        }\n\n        // Test telemetry connection\n        try {\n            await TelemetryService.testConnection();\n            return NextResponse.json({\n                success: true,\n                message: 'Telemetry connection test successful'\n            });\n        } catch (error) {\n            return NextResponse.json({\n                success: false,\n                error: error instanceof Error ? error.message : 'Unknown error',\n                details: 'Check server logs for more information'\n            });\n        }\n    } catch (error) {\n        console.error('Debug API error:', error);\n        return NextResponse.json(\n            {error: 'Debug test failed'},\n            {status: 500}\n        );\n    }\n}\n\nexport async function POST(request: NextRequest) {\n    try {\n        const user = await validateAuthAndGetUser();\n\n        if (user.role !== 'ADMIN') {\n            return NextResponse.json(\n                {error: 'Admin access required'},\n                {status: 403}\n            );\n        }\n\n        const body = await request.json();\n        const {action} = body;\n\n        switch (action) {\n            case 'test_connection':\n                try {\n                    await TelemetryService.testConnection();\n                    return NextResponse.json({\n                        success: true,\n                        message: 'Connection test successful'\n                    });\n                } catch (error) {\n                    return NextResponse.json({\n                        success: false,\n                        error: error instanceof Error ? error.message : 'Unknown error'\n                    });\n                }\n\n            case 'get_config':\n                const config = await TelemetryService.getTelemetryConfig();\n                return NextResponse.json({\n                    success: true,\n                    config\n                });\n\n            case 'force_register':\n                try {\n                    const instanceId = await TelemetryService.registerInstance();\n                    return NextResponse.json({\n                        success: true,\n                        instanceId,\n                        message: 'Instance registered successfully'\n                    });\n                } catch (error) {\n                    return NextResponse.json({\n                        success: false,\n                        error: error instanceof Error ? error.message : 'Unknown error'\n                    });\n                }\n\n            default:\n                return NextResponse.json(\n                    {error: 'Invalid action'},\n                    {status: 400}\n                );\n        }\n    } catch (error) {\n        console.error('Debug API error:', error);\n        return NextResponse.json(\n            {error: 'Debug action failed'},\n            {status: 500}\n        );\n    }\n}"
  },
  {
    "path": "app/api-docs/route.ts",
    "content": "import { ApiReference } from '@scalar/nextjs-api-reference'\n\nconst config = {\n    url: '/swagger.json',\n    theme: 'kepler',\n\n}\n\n// @ts-ignore\nexport const GET = ApiReference(config)"
  },
  {
    "path": "app/changelog/[projectId]/[entryId]/page.tsx",
    "content": "'use client';\n\nimport {useEffect, useState} from 'react';\nimport {useRouter} from 'next/navigation';\nimport {ArrowLeft, Calendar, Clock} from 'lucide-react';\nimport Link from 'next/link';\nimport {RenderMarkdown} from '@/components/markdown-editor/RenderMarkdown';\nimport {Badge} from '@/components/ui/badge';\nimport {Button} from '@/components/ui/button';\nimport {Skeleton} from '@/components/ui/skeleton';\nimport {formatDistanceToNow, format} from 'date-fns';\nimport ShareButton from '@/components/changelog/ShareButton';\nimport {useEntryViewTracking} from '@/app/changelog/[projectId]/changelog-view';\n\ninterface ChangelogEntry {\n    id: string;\n    title: string;\n    content: string;\n    excerpt?: string;\n    version?: string;\n    publishedAt: string;\n    createdAt: string;\n    updatedAt: string;\n    changelogId: string;\n    tags: Array<{\n        id: string;\n        name: string;\n        color: string | null;\n    }>;\n}\n\ninterface EntryResponse {\n    project: {\n        id: string;\n        name: string;\n        description?: string;\n    };\n    entry: ChangelogEntry;\n}\n\ntype EntryPageProps = {\n    params: Promise<{ projectId: string; entryId: string }>;\n};\n\nexport default function EntryPage({params}: EntryPageProps) {\n    const router = useRouter();\n    const [data, setData] = useState<EntryResponse | null>(null);\n    const [loading, setLoading] = useState(true);\n    const [error, setError] = useState(false);\n    const [projectId, setProjectId] = useState<string>('');\n    const [entryId, setEntryId] = useState<string>('');\n\n    // Call the tracking hook at the top level with safe defaults\n    // It will only track once data is loaded\n    const entryRef = useEntryViewTracking(\n        data?.entry?.id || '',\n        data?.project?.id || ''\n    );\n\n    useEffect(() => {\n        params.then(({projectId, entryId}) => {\n            setProjectId(projectId);\n            setEntryId(entryId);\n\n            // Fetch entry data\n            fetch(`/api/changelog/entries/${entryId}`)\n                .then(res => {\n                    if (!res.ok) {\n                        throw new Error('Failed to fetch entry');\n                    }\n                    return res.json();\n                })\n                .then((data: EntryResponse) => {\n                    setData(data);\n                })\n                .catch(() => {\n                    setError(true);\n                })\n                .finally(() => {\n                    setLoading(false);\n                });\n        });\n    }, [params]);\n\n    if (loading) {\n        return (\n            <div className=\"min-h-screen bg-background\">\n                <div className=\"max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12\">\n                    <Skeleton className=\"h-10 w-32 mb-8\"/>\n                    <div className=\"space-y-8\">\n                        <div className=\"space-y-4\">\n                            <Skeleton className=\"h-6 w-24\"/>\n                            <Skeleton className=\"h-12 w-3/4\"/>\n                            <div className=\"flex gap-4\">\n                                <Skeleton className=\"h-5 w-20\"/>\n                                <Skeleton className=\"h-5 w-32\"/>\n                                <Skeleton className=\"h-5 w-28\"/>\n                            </div>\n                        </div>\n                        <Skeleton className=\"h-px w-full\"/>\n                        <div className=\"space-y-4\">\n                            <Skeleton className=\"h-4 w-full\"/>\n                            <Skeleton className=\"h-4 w-full\"/>\n                            <Skeleton className=\"h-4 w-3/4\"/>\n                        </div>\n                    </div>\n                </div>\n            </div>\n        );\n    }\n\n    if (error || !data) {\n        return (\n            <div className=\"min-h-screen bg-background flex items-center justify-center\">\n                <div className=\"text-center\">\n                    <h1 className=\"text-4xl font-bold mb-4\">Entry Not Found</h1>\n                    <p className=\"text-muted-foreground mb-8\">\n                        The changelog entry you're looking for doesn't exist or has been removed.\n                    </p>\n                    <Link href={`/changelog/${projectId}`}>\n                        <Button>Back to Changelog</Button>\n                    </Link>\n                </div>\n            </div>\n        );\n    }\n\n    const {project, entry} = data;\n\n    return (\n        <div className=\"min-h-screen bg-background\">\n            <div className=\"max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12\">\n                {/* Back Button */}\n                <div className=\"mb-8\">\n                    <Link href={`/changelog/${projectId}`}>\n                        <Button variant=\"ghost\" size=\"sm\" className=\"gap-2\">\n                            <ArrowLeft className=\"w-4 h-4\"/>\n                            Back to {project.name}\n                        </Button>\n                    </Link>\n                </div>\n\n                {/* Entry Header */}\n                <article ref={entryRef} className=\"space-y-8\">\n                    <header className=\"space-y-4\">\n                        {/* Tags */}\n                        {entry.tags && entry.tags.length > 0 && (\n                            <div className=\"flex flex-wrap gap-2\">\n                                {entry.tags.map((tag) => (\n                                    <Badge\n                                        key={tag.id}\n                                        variant=\"secondary\"\n                                        style={{\n                                            backgroundColor: tag.color ? `${tag.color}20` : undefined,\n                                            color: tag.color || undefined,\n                                            borderColor: tag.color || undefined,\n                                        }}\n                                    >\n                                        {tag.name}\n                                    </Badge>\n                                ))}\n                            </div>\n                        )}\n\n                        {/* Title */}\n                        <h1 className=\"text-4xl font-bold tracking-tight\">\n                            {entry.title}\n                        </h1>\n\n                        {/* Metadata */}\n                        <div className=\"flex flex-wrap items-center gap-4 text-sm text-muted-foreground\">\n                            {entry.version && (\n                                <div className=\"flex items-center gap-1.5\">\n                                    <Badge variant=\"outline\" className=\"font-mono\">\n                                        v{entry.version}\n                                    </Badge>\n                                </div>\n                            )}\n\n                            <div className=\"flex items-center gap-1.5\">\n                                <Calendar className=\"w-4 h-4\"/>\n                                <time dateTime={entry.publishedAt}>\n                                    {format(new Date(entry.publishedAt), 'MMMM d, yyyy')}\n                                </time>\n                            </div>\n\n                            <div className=\"flex items-center gap-1.5\">\n                                <Clock className=\"w-4 h-4\"/>\n                                {formatDistanceToNow(new Date(entry.publishedAt), {addSuffix: true})}\n                            </div>\n\n                            <ShareButton\n                                url={`${window.location.origin}/changelog/${projectId}/${entryId}`}\n                                title={entry.title}\n                            />\n                        </div>\n                    </header>\n\n                    {/* Divider */}\n                    <hr className=\"border-border\"/>\n\n                    {/* Content */}\n                    <div className=\"prose prose-lg dark:prose-invert max-w-none\">\n                        <RenderMarkdown>\n                            {entry.content}\n                        </RenderMarkdown>\n                    </div>\n\n                    {/* Footer */}\n                    <footer className=\"pt-8 border-t border-border\">\n                        <div className=\"flex items-center justify-between\">\n                            <div className=\"text-sm text-muted-foreground\">\n                                Last updated: {format(new Date(entry.updatedAt), 'MMMM d, yyyy')}\n                            </div>\n\n                            <Link href={`/changelog/${projectId}`}>\n                                <Button variant=\"outline\" size=\"sm\">\n                                    View All Updates\n                                </Button>\n                            </Link>\n                        </div>\n                    </footer>\n                </article>\n            </div>\n        </div>\n    );\n}\n"
  },
  {
    "path": "app/changelog/[projectId]/changelog-view.tsx",
    "content": "'use client';\n\nimport { useEffect, useRef } from 'react';\nimport { motion } from 'framer-motion';\nimport { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport { Calendar, Rss } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport { useTimezone } from '@/hooks/use-timezone';\nimport Link from 'next/link';\n\ninterface ChangelogEntry {\n    id: string;\n    title: string;\n    content: string;\n    version?: string;\n    publishedAt: Date;\n    tags: Array<{ id: string; name: string }>;\n}\n\ninterface ChangelogData {\n    project: {\n        id: string;\n        name: string;\n    };\n    items: ChangelogEntry[];\n}\n\ninterface ChangelogViewProps {\n    data: ChangelogData;\n}\n\n// Track entry view when it becomes visible\nexport function useEntryViewTracking(entryId: string, projectId: string) {\n    const elementRef = useRef<HTMLDivElement>(null);\n    const hasTracked = useRef(false);\n\n    useEffect(() => {\n        const element = elementRef.current;\n        if (!element || hasTracked.current) return;\n\n        const observer = new IntersectionObserver(\n            (entries) => {\n                entries.forEach((entry) => {\n                    if (entry.isIntersecting && !hasTracked.current) {\n                        hasTracked.current = true;\n\n                        // Track the entry view\n                        fetch('/api/analytics/track', {\n                            method: 'POST',\n                            headers: {\n                                'Content-Type': 'application/json',\n                            },\n                            body: JSON.stringify({\n                                projectId,\n                                changelogEntryId: entryId,\n                            }),\n                        }).catch((error) => {\n                            console.error('Failed to track entry view:', error);\n                        });\n                    }\n                });\n            },\n            {\n                threshold: 0.5, // Track when 50% of the entry is visible\n                rootMargin: '0px 0px -100px 0px', // Trigger earlier\n            }\n        );\n\n        observer.observe(element);\n\n        return () => {\n            observer.disconnect();\n        };\n    }, [entryId, projectId]);\n\n    return elementRef;\n}\n\nfunction ChangelogEntry({ entry, projectId, timezone }: { entry: ChangelogEntry; projectId: string; timezone: string }) {\n    const entryRef = useEntryViewTracking(entry.id, projectId);\n\n    const fadeIn = {\n        initial: { opacity: 0, y: 20 },\n        animate: { opacity: 1, y: 0 },\n        transition: { duration: 0.5 }\n    };\n\n    return (\n        <motion.article\n            ref={entryRef}\n            variants={fadeIn}\n            initial=\"initial\"\n            whileInView=\"animate\"\n            viewport={{ once: true, margin: \"-100px\" }}\n            className=\"group relative\"\n        >\n            <div className={cn(\n                \"relative p-8 rounded-2xl border bg-card/50 backdrop-blur-sm\",\n                \"hover:shadow-lg hover:shadow-primary/5 transition-all duration-300\",\n                \"hover:border-primary/20\"\n            )}>\n                {/* Entry header */}\n                <div className=\"flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4 mb-6\">\n                    <div className=\"space-y-2\">\n                        <div className=\"flex items-center gap-3\">\n                            <h2 className=\"text-2xl font-semibold tracking-tight group-hover:text-primary transition-colors\">\n                                {entry.title}\n                            </h2>\n                            {entry.version && (\n                                <Badge variant=\"secondary\" className=\"font-mono text-xs\">\n                                    v{entry.version}\n                                </Badge>\n                            )}\n                        </div>\n\n                        <div className=\"flex items-center text-sm text-muted-foreground\">\n                            <Calendar className=\"h-4 w-4 mr-2\" />\n                            {new Date(entry.publishedAt).toLocaleDateString('en-US', {\n                                year: 'numeric',\n                                month: 'long',\n                                day: 'numeric',\n                                timeZone: timezone,\n                            })}\n                        </div>\n                    </div>\n\n                    {/* Tags */}\n                    {entry.tags.length > 0 && (\n                        <div className=\"flex flex-wrap gap-2\">\n                            {entry.tags.map((tag) => (\n                                <Badge key={tag.id} variant=\"outline\" className=\"text-xs\">\n                                    {tag.name}\n                                </Badge>\n                            ))}\n                        </div>\n                    )}\n                </div>\n\n                {/* Entry content */}\n                <div\n                    className=\"prose prose-neutral dark:prose-invert max-w-none\"\n                    dangerouslySetInnerHTML={{ __html: entry.content }}\n                />\n            </div>\n        </motion.article>\n    );\n}\n\nexport default function ChangelogView({ data }: ChangelogViewProps) {\n    const timezone = useTimezone();\n\n    const fadeIn = {\n        initial: { opacity: 0, y: 20 },\n        animate: { opacity: 1, y: 0 },\n        transition: { duration: 0.6 }\n    };\n\n    const staggerChildren = {\n        animate: {\n            transition: {\n                staggerChildren: 0.1\n            }\n        }\n    };\n\n    // Generate current page URL for sharing\n    const pageUrl = typeof window !== 'undefined'\n        ? `${window.location.origin}/changelog/${data.project.id}`\n        : '';\n\n    return (\n        <div className=\"min-h-screen bg-background\">\n            {/* Gradient background */}\n            <div className=\"absolute top-0 inset-x-0 h-96 overflow-hidden pointer-events-none\">\n                <div className=\"absolute inset-0 bg-gradient-to-b from-primary/10 via-primary/5 to-transparent\" />\n                <div className=\"absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(var(--primary-rgb),0.1),transparent)]\" />\n            </div>\n\n            <div className=\"relative pb-24\">\n                <motion.header\n                    className=\"relative py-16 md:py-24 px-4 max-w-6xl mx-auto\"\n                    variants={fadeIn}\n                    initial=\"initial\"\n                    animate=\"animate\"\n                >\n                    <div className=\"text-center space-y-6\">\n                        {/* Project name */}\n                        <h1\n                            className={cn(\n                                \"text-4xl md:text-6xl lg:text-7xl font-bold tracking-tight\",\n                                \"bg-gradient-to-b from-foreground via-foreground/95 to-foreground/80\",\n                                \"bg-clip-text text-transparent\"\n                            )}\n                        >\n                            {data.project.name}\n                        </h1>\n\n                        {/* Subtitle */}\n                        <p className=\"text-xl md:text-2xl text-muted-foreground font-medium\">\n                            Changelog &amp; Release Notes\n                        </p>\n\n                        {/* Action buttons */}\n                        <div className=\"flex items-center justify-center gap-4 pt-4\">\n                            <Button variant=\"outline\" size=\"sm\" asChild>\n                                <Link href={`${pageUrl}/rss.xml`} target=\"_blank\">\n                                    <Rss className=\"h-4 w-4 mr-2\" />\n                                    RSS Feed\n                                </Link>\n                            </Button>\n                        </div>\n                    </div>\n                </motion.header>\n\n                {/* Changelog entries */}\n                <main className=\"max-w-4xl mx-auto px-4\">\n                    {data.items.length === 0 ? (\n                        <motion.div\n                            className=\"text-center py-16\"\n                            variants={fadeIn}\n                            initial=\"initial\"\n                            animate=\"animate\"\n                        >\n                            <p className=\"text-muted-foreground text-lg\">\n                                No changelog entries yet. Check back soon!\n                            </p>\n                        </motion.div>\n                    ) : (\n                        <motion.div\n                            className=\"space-y-12\"\n                            variants={staggerChildren}\n                            initial=\"initial\"\n                            animate=\"animate\"\n                        >\n                            {data.items.map((entry) => (\n                                <ChangelogEntry\n                                    key={entry.id}\n                                    entry={entry}\n                                    projectId={data.project.id}\n                                    timezone={timezone}\n                                />\n                            ))}\n                        </motion.div>\n                    )}\n                </main>\n            </div>\n        </div>\n    );\n}"
  },
  {
    "path": "app/changelog/[projectId]/layout.tsx",
    "content": "import { Metadata } from 'next'\nimport React from \"react\";\nimport ScrollToTopButton from \"@/components/changelog/ScrollToTopButton\";\nimport { ThemeToggle } from \"@/components/changelog/ThemeToggle\";\nimport ButtonGroup from \"@/components/changelog/ButtonGroup\";\n\ninterface ChangelogLayoutProps {\n    params: Promise<{ projectId: string }>\n    children: React.ReactNode\n}\n\nexport async function generateMetadata(\n    { params }: ChangelogLayoutProps\n): Promise<Metadata> {\n    const { projectId } = await params\n\n    return {\n        metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'),\n        alternates: {\n            types: {\n                'application/rss+xml': `/api/changelog/${projectId}/rss.xml`,\n            },\n        },\n    }\n}\n\nexport default async function ChangelogLayout({ children, params }: ChangelogLayoutProps) {\n    const { projectId } = await params\n\n    return (\n        <div className=\"container mx-auto py-8\">\n            {projectId && <ButtonGroup projectId={projectId} />}\n            {children}\n        </div>\n    )\n}"
  },
  {
    "path": "app/changelog/[projectId]/loading.tsx",
    "content": "// app/changelog/[projectId]/loading.tsx\nimport { Skeleton } from '@/components/ui/skeleton'\nimport { Card, CardContent, CardHeader } from '@/components/ui/card'\n\nexport default function Loading() {\n    return (\n        <div className=\"min-h-screen bg-background\">\n            <div className=\"container max-w-4xl py-8 px-4 md:py-12\">\n                <div className=\"text-center mb-12\">\n                    <Skeleton className=\"h-12 w-[300px] mx-auto mb-4\" />\n                    <Skeleton className=\"h-6 w-[400px] mx-auto\" />\n                </div>\n\n                <div className=\"space-y-8\">\n                    {[...Array(5)].map((_, i) => (\n                        <Card key={i}>\n                            <CardHeader>\n                                <div className=\"flex items-start justify-between gap-4\">\n                                    <div className=\"space-y-2\">\n                                        <Skeleton className=\"h-6 w-[250px]\" />\n                                        <Skeleton className=\"h-5 w-[100px]\" />\n                                    </div>\n                                    <Skeleton className=\"h-5 w-[120px]\" />\n                                </div>\n                            </CardHeader>\n                            <CardContent className=\"space-y-2\">\n                                <Skeleton className=\"h-4 w-full\" />\n                                <Skeleton className=\"h-4 w-3/4\" />\n                                <Skeleton className=\"h-4 w-1/2\" />\n                            </CardContent>\n                        </Card>\n                    ))}\n                </div>\n            </div>\n        </div>\n    )\n}"
  },
  {
    "path": "app/changelog/[projectId]/not-found.tsx",
    "content": "// app/changelog/[projectId]/not-found.tsx\nimport Link from 'next/link'\nimport { Button } from '@/components/ui/button'\n\nexport default function NotFound() {\n    return (\n        <div className=\"min-h-screen bg-background flex items-center justify-center\">\n            <div className=\"text-center space-y-4 px-4\">\n                <h1 className=\"text-4xl font-bold tracking-tight\">Changelog Not Found</h1>\n                <p className=\"text-muted-foreground max-w-[500px] mx-auto\">\n                    The changelog you&apos;re looking for doesn&apos;t exist or isn&apos;t publicly available.\n                </p>\n                <Button asChild>\n                    <Link href=\"/\">Return Home</Link>\n                </Button>\n            </div>\n        </div>\n    )\n}"
  },
  {
    "path": "app/changelog/[projectId]/page.tsx",
    "content": "import { Suspense } from 'react';\nimport { notFound } from 'next/navigation';\nimport ChangelogEntries from '@/components/changelog/ChangelogEntries';\nimport ShareButton from '@/components/changelog/ShareButton';\nimport { Skeleton } from '@/components/ui/skeleton';\nimport { GitBranch, Clock, Rss } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport {\n    Tooltip,\n    TooltipContent,\n    TooltipProvider,\n    TooltipTrigger\n} from '@/components/ui/tooltip';\nimport Link from 'next/link';\nimport { Metadata } from 'next';\nimport SubscriptionForm from \"@/components/subscription-form\";\nimport {trackChangelogView} from \"@/lib/middleware/analytics\";\nimport {headers} from \"next/headers\";\n\ninterface ChangelogResponse {\n    project: {\n        id: string;\n        name: string;\n        description?: string;\n        emailNotificationsEnabled?: boolean;\n    };\n    items: Array<{\n        id: string;\n        publishedAt: string;\n        title?: string;\n    }>;\n    nextCursor?: string;\n}\n\ntype ChangelogPageProps = {\n    params: Promise<{ projectId: string }>;\n};\n\nasync function getInitialData(projectId: string): Promise<ChangelogResponse | null> {\n    const res = await fetch(\n        `${process.env.NEXT_PUBLIC_APP_URL}/api/changelog/${projectId}/entries/all`,\n        { next: { revalidate: 300 } }\n    );\n\n    if (!res.ok) {\n        if (res.status === 404) return null;\n        throw new Error('Failed to fetch changelog');\n    }\n\n    return res.json();\n}\n\nexport async function generateMetadata(\n    { params }: ChangelogPageProps\n): Promise<Metadata> {\n    const { projectId } = await params;\n    const data = await getInitialData(projectId);\n\n    if (!data) {\n        return {\n            title: 'Changelog Not Found',\n            description: 'The requested changelog could not be found.'\n        };\n    }\n\n    const { project, items } = data;\n    const latestUpdate = items[0]?.publishedAt\n        ? new Date(items[0].publishedAt).toLocaleDateString('en-US', {\n            year: 'numeric',\n            month: 'long',\n            day: 'numeric'\n        })\n        : null;\n\n    return {\n        title: `${project.name} Changelog & Release Notes`,\n        description: project.description ||\n            `View the latest updates and changes for ${project.name}${latestUpdate ? ` - last updated ${latestUpdate}` : ''}.`,\n        openGraph: {\n            title: `${project.name} Changelog`,\n            description: project.description ||\n                `Stay up to date with the latest improvements, features, and bug fixes for ${project.name}.`,\n            type: 'website',\n        },\n    };\n}\n\n// Loading component for Suspense\nfunction ChangelogSkeleton() {\n    return (\n        <div className=\"space-y-12\">\n            {[...Array(3)].map((_, i) => (\n                <div key={i} className=\"space-y-4 animate-pulse\">\n                    <div className=\"space-y-3\">\n                        <Skeleton className=\"h-8 w-[300px]\" />\n                        <Skeleton className=\"h-5 w-[200px]\" />\n                    </div>\n                    <Skeleton className=\"h-48 w-full rounded-lg\" />\n                    <div className=\"flex gap-2 pt-4\">\n                        <Skeleton className=\"h-6 w-16 rounded-full\" />\n                        <Skeleton className=\"h-6 w-16 rounded-full\" />\n                        <Skeleton className=\"h-6 w-16 rounded-full\" />\n                    </div>\n                </div>\n            ))}\n        </div>\n    );\n}\n\nexport default async function ChangelogPage({ params }: ChangelogPageProps) {\n    // Using the proper Next.js 15 pattern with await\n    const { projectId } = await params;\n    const data = await getInitialData(projectId);\n\n    if (!data) {\n        notFound();\n    }\n\n    // Track the changelog view asynchronously (don't block rendering)\n    try {\n        const headersList = await headers();\n        const request = new Request('http://localhost', {\n            headers: headersList\n        });\n\n        // Track the main changelog page view\n        await trackChangelogView(request, {\n            projectId: data.project.id,\n            // No specific entry ID for main page view\n        });\n    } catch (error) {\n        // Don't let analytics tracking errors break the page\n        console.error('Failed to track changelog view:', error);\n    }\n\n    const stats = {\n        totalEntries: data.items.length + (data.nextCursor ? '+' : ''),\n        lastUpdate: data.items[0]?.publishedAt,\n    };\n\n    // Generate current page URL for sharing\n    const pageUrl = `${process.env.NEXT_PUBLIC_APP_URL}/changelog/${projectId}`;\n\n    return (\n        <div className=\"min-h-screen bg-background\">\n            {/* Gradient background */}\n            <div className=\"absolute top-0 inset-x-0 h-96 overflow-hidden pointer-events-none\">\n                <div className=\"absolute inset-0 bg-gradient-to-b from-primary/10 via-primary/5 to-transparent\" />\n                <div className=\"absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(var(--primary-rgb),0.1),transparent)]\" />\n            </div>\n\n            <div className=\"relative pb-24\">\n                <header className=\"relative py-16 md:py-24 px-4 max-w-6xl mx-auto\">\n                    <div className=\"text-center space-y-6\">\n                        {/* Project name */}\n                        <h1\n                            className={cn(\n                                \"text-4xl md:text-6xl lg:text-7xl font-bold tracking-tight\",\n                                \"bg-gradient-to-b from-foreground via-foreground/95 to-foreground/80\",\n                                \"bg-clip-text text-transparent\"\n                            )}\n                        >\n                            {data.project.name}\n                        </h1>\n\n                        {/* Subtitle */}\n                        <p className=\"text-xl md:text-2xl text-muted-foreground font-medium\">\n                            Changelog &amp; Release Notes\n                        </p>\n\n                        {/* Description if available */}\n                        {data.project.description && (\n                            <p className=\"text-lg text-muted-foreground max-w-2xl mx-auto\">\n                                {data.project.description}\n                            </p>\n                        )}\n\n                        {/* Stats display with RSS and share links */}\n                        <div className=\"flex flex-col items-center gap-6\">\n                            <div className=\"inline-flex flex-wrap justify-center items-center gap-4 md:gap-8 px-6 md:px-8 py-4\n                            bg-background/60 backdrop-blur-sm\n                            border border-border/40\n                            rounded-full\n                            hover:bg-background/80 hover:border-border/60\n                            transition-all duration-300\">\n                                <div className=\"flex items-center gap-3\">\n                                    <GitBranch className=\"w-5 h-5 text-muted-foreground\" />\n                                    <span className=\"font-medium text-lg\">\n                    {stats.totalEntries} Updates\n                  </span>\n                                </div>\n\n                                {stats.lastUpdate && (\n                                    <>\n                                        <div className=\"hidden md:block w-1.5 h-1.5 rounded-full bg-border\" />\n                                        <div className=\"flex items-center gap-3\">\n                                            <Clock className=\"w-5 h-5 text-muted-foreground\" />\n                                            <time\n                                                dateTime={stats.lastUpdate}\n                                                className=\"font-medium text-lg tabular-nums\"\n                                            >\n                                                {new Intl.DateTimeFormat('en-US', {\n                                                    month: 'long',\n                                                    day: 'numeric',\n                                                    year: 'numeric',\n                                                }).format(new Date(stats.lastUpdate))}\n                                            </time>\n                                        </div>\n                                    </>\n                                )}\n\n                                <div className=\"hidden md:block w-1.5 h-1.5 rounded-full bg-border\" />\n\n                                <TooltipProvider>\n                                    <Tooltip>\n                                        <TooltipTrigger asChild>\n                                            <Link\n                                                href={`/changelog/${projectId}/rss.xml`}\n                                                className=\"flex items-center gap-2 text-muted-foreground hover:text-orange-500 transition-colors duration-200\"\n                                                aria-label=\"Subscribe to RSS feed\"\n                                            >\n                                                <Rss className=\"w-5 h-5\" />\n                                                <span className=\"font-medium text-lg\">RSS</span>\n                                            </Link>\n                                        </TooltipTrigger>\n                                        <TooltipContent>\n                                            Subscribe to updates via RSS\n                                        </TooltipContent>\n                                    </Tooltip>\n                                </TooltipProvider>\n\n                                <div className=\"hidden md:block w-1.5 h-1.5 rounded-full bg-border\" />\n\n                                {/* Client-side Share Button */}\n                                <ShareButton\n                                    url={pageUrl}\n                                    title={`${data.project.name} Changelog`}\n                                    text={data.project.description || `Check out the latest updates for ${data.project.name}`}\n                                />\n                            </div>\n                        </div>\n                        {/* Only show subscription form if email notifications are enabled */}\n                        {data.project.emailNotificationsEnabled && (\n                            <SubscriptionForm projectId={projectId} projectName={data.project.name} />\n                        )}\n                    </div>\n                </header>\n\n                <div className=\"relative max-w-7xl mx-auto px-4 md:px-6\">\n                    <Suspense fallback={<ChangelogSkeleton />}>\n                        <ChangelogEntries projectId={projectId} />\n                    </Suspense>\n                </div>\n\n                {/* Footer spacer */}\n                <div className=\"h-12\" />\n            </div>\n        </div>\n    );\n}"
  },
  {
    "path": "app/changelog/[projectId]/rss.xml/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\nimport { generateRSSFeed } from '@/lib/services/changelog/rss'\n\nexport async function GET(\n    request: Request,\n    { params }: { params: Promise<{ projectId: string }> }\n) {\n    try {\n        const { projectId } = await params\n\n        // Get project and changelog\n        const project = await db.project.findUnique({\n            where: {\n                id: projectId,\n                isPublic: true\n            },\n            select: {\n                id: true,\n                name: true,\n                changelog: {\n                    select: {\n                        id: true,\n                        entries: {\n                            where: {\n                                publishedAt: { not: null }\n                            },\n                            orderBy: [\n                                { publishedAt: 'desc' },\n                                { id: 'desc' }\n                            ],\n                            take: 10 // Changed from 50 to 10 entries\n                        }\n                    }\n                }\n            }\n        })\n\n        if (!project?.changelog) {\n            return NextResponse.json(\n                { error: 'Changelog not found or not public' },\n                { status: 404 }\n            )\n        }\n\n        const baseUrl = process.env.NEXT_PUBLIC_APP_URL || new URL(request.url).origin;\n        const feedUrl = `${baseUrl}/changelog/${project.id}`\n\n        const rss = generateRSSFeed(project.changelog.entries, {\n            title: `${project.name} Changelog`,\n            description: `Latest changes and updates for ${project.name}`,\n            link: feedUrl\n        })\n\n        // Return RSS XML with proper content type\n        return new NextResponse(rss, {\n            headers: {\n                'Content-Type': 'application/xml;charset=utf-8',\n                'Cache-Control': 'public, max-age=3600', // Cache for 1 hour\n            }\n        })\n    } catch (error) {\n        console.error('Error generating RSS feed:', error)\n        return NextResponse.json(\n            { error: 'Failed to generate RSS feed' },\n            { status: 500 }\n        )\n    }\n}"
  },
  {
    "path": "app/changelog/custom-domain/[domain]/[entryId]/EntryContent.tsx",
    "content": "'use client';\n\nimport { ArrowLeft, Calendar, Clock } from 'lucide-react';\nimport Link from 'next/link';\nimport { RenderMarkdown } from '@/components/markdown-editor/RenderMarkdown';\nimport { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport { formatDistanceToNow, format } from 'date-fns';\nimport ShareButton from '@/components/changelog/ShareButton';\nimport { useEntryViewTracking } from '@/app/changelog/[projectId]/changelog-view';\nimport ButtonGroup from '@/components/changelog/ButtonGroup';\n\ninterface ChangelogEntry {\n    id: string;\n    title: string;\n    content: string;\n    excerpt?: string;\n    version?: string;\n    publishedAt: string;\n    createdAt: string;\n    updatedAt: string;\n    tags: Array<{\n        id: string;\n        name: string;\n        color: string | null;\n    }>;\n}\n\ninterface EntryContentProps {\n    domain: string;\n    projectId: string;\n    projectName: string;\n    entry: ChangelogEntry;\n}\n\nexport function EntryContent({ domain, projectId, projectName, entry }: EntryContentProps) {\n    const entryRef = useEntryViewTracking(entry.id, projectId);\n\n    return (\n        <>\n            <ButtonGroup projectId={projectId} />\n            <div className=\"min-h-screen bg-background\">\n                <div className=\"max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12\">\n                {/* Back Button */}\n                <div className=\"mb-8\">\n                    <Link href=\"/\">\n                        <Button variant=\"ghost\" size=\"sm\" className=\"gap-2\">\n                            <ArrowLeft className=\"w-4 h-4\" />\n                            Back to {projectName}\n                        </Button>\n                    </Link>\n                </div>\n\n                {/* Entry Header */}\n                <article ref={entryRef} className=\"space-y-8\">\n                    <header className=\"space-y-4\">\n                        {/* Tags */}\n                        {entry.tags && entry.tags.length > 0 && (\n                            <div className=\"flex flex-wrap gap-2\">\n                                {entry.tags.map((tag) => (\n                                    <Badge\n                                        key={tag.id}\n                                        variant=\"secondary\"\n                                        style={{\n                                            backgroundColor: tag.color ? `${tag.color}20` : undefined,\n                                            color: tag.color || undefined,\n                                            borderColor: tag.color || undefined,\n                                        }}\n                                    >\n                                        {tag.name}\n                                    </Badge>\n                                ))}\n                            </div>\n                        )}\n\n                        {/* Title */}\n                        <h1 className=\"text-4xl font-bold tracking-tight\">\n                            {entry.title}\n                        </h1>\n\n                        {/* Metadata */}\n                        <div className=\"flex flex-wrap items-center gap-4 text-sm text-muted-foreground\">\n                            {entry.version && (\n                                <div className=\"flex items-center gap-1.5\">\n                                    <Badge variant=\"outline\" className=\"font-mono\">\n                                        {entry.version}\n                                    </Badge>\n                                </div>\n                            )}\n\n                            <div className=\"flex items-center gap-1.5\">\n                                <Calendar className=\"w-4 h-4\" />\n                                <time dateTime={entry.publishedAt}>\n                                    {format(new Date(entry.publishedAt), 'MMMM d, yyyy')}\n                                </time>\n                            </div>\n\n                            <div className=\"flex items-center gap-1.5\">\n                                <Clock className=\"w-4 h-4\" />\n                                {formatDistanceToNow(new Date(entry.publishedAt), { addSuffix: true })}\n                            </div>\n\n                            <ShareButton\n                                url={`https://${domain}/${entry.id}`}\n                                title={entry.title}\n                            />\n                        </div>\n                    </header>\n\n                    {/* Divider */}\n                    <hr className=\"border-border\" />\n\n                    {/* Content */}\n                    <div className=\"prose prose-lg dark:prose-invert max-w-none\">\n                        <RenderMarkdown>\n                            {entry.content}\n                        </RenderMarkdown>\n                    </div>\n\n                    {/* Footer */}\n                    <footer className=\"pt-8 border-t border-border\">\n                        <div className=\"flex items-center justify-between\">\n                            <div className=\"text-sm text-muted-foreground\">\n                                Last updated: {format(new Date(entry.updatedAt), 'MMMM d, yyyy')}\n                            </div>\n\n                            <Link href=\"/\">\n                                <Button variant=\"outline\" size=\"sm\">\n                                    View All Updates\n                                </Button>\n                            </Link>\n                        </div>\n                    </footer>\n                </article>\n                </div>\n            </div>\n        </>\n    );\n}\n"
  },
  {
    "path": "app/changelog/custom-domain/[domain]/[entryId]/page.tsx",
    "content": "import {notFound} from 'next/navigation';\nimport {Metadata} from 'next';\nimport {db} from '@/lib/db';\nimport {EntryContent} from './EntryContent';\n\ninterface ChangelogEntry {\n    id: string;\n    title: string;\n    content: string;\n    excerpt?: string;\n    version?: string;\n    publishedAt: string;\n    createdAt: string;\n    updatedAt: string;\n    tags: Array<{\n        id: string;\n        name: string;\n        color: string | null;\n    }>;\n}\n\ninterface EntryResponse {\n    project: {\n        id: string;\n        name: string;\n        description?: string;\n    };\n    entry: ChangelogEntry;\n}\n\ntype EntryPageProps = {\n    params: Promise<{ domain: string; entryId: string }>;\n};\n\nasync function getProjectFromDomain(domain: string) {\n    // First find the custom domain\n    const customDomain = await db.customDomain.findUnique({\n        where: {\n            domain: domain,\n            verified: true,\n        },\n        select: {\n            projectId: true,\n        },\n    });\n\n    if (!customDomain) {\n        return null;\n    }\n\n    // Then get the project with changelog\n    const project = await db.project.findUnique({\n        where: {\n            id: customDomain.projectId,\n            isPublic: true,\n        },\n        select: {\n            id: true,\n            name: true,\n            changelog: {\n                select: {\n                    id: true,\n                },\n            },\n        },\n    });\n\n    return project;\n}\n\nasync function getEntry(changelogId: string, entryId: string): Promise<ChangelogEntry | null> {\n    const entry = await db.changelogEntry.findFirst({\n        where: {\n            id: entryId,\n            changelogId: changelogId,\n            publishedAt: {not: null},\n        },\n        select: {\n            id: true,\n            title: true,\n            content: true,\n            excerpt: true,\n            version: true,\n            publishedAt: true,\n            createdAt: true,\n            updatedAt: true,\n            tags: {\n                select: {\n                    id: true,\n                    name: true,\n                    color: true,\n                },\n            },\n        },\n    });\n\n    if (!entry) return null;\n\n    return {\n        id: entry.id,\n        title: entry.title,\n        content: entry.content,\n        excerpt: entry.excerpt ?? undefined,\n        version: entry.version ?? undefined,\n        publishedAt: entry.publishedAt!.toISOString(),\n        createdAt: entry.createdAt.toISOString(),\n        updatedAt: entry.updatedAt.toISOString(),\n        tags: entry.tags,\n    };\n}\n\nexport async function generateMetadata({params}: EntryPageProps): Promise<Metadata> {\n    const {domain, entryId} = await params;\n    const project = await getProjectFromDomain(domain);\n\n    if (!project?.changelog) {\n        return {\n            title: 'Entry Not Found',\n        };\n    }\n\n    const entry = await getEntry(project.changelog.id, entryId);\n\n    if (!entry) {\n        return {\n            title: 'Entry Not Found',\n        };\n    }\n\n    return {\n        title: `${entry.title} - ${project.name}`,\n        description: entry.excerpt || entry.content.substring(0, 160),\n        openGraph: {\n            title: entry.title,\n            description: entry.excerpt || entry.content.substring(0, 160),\n            type: 'article',\n            publishedTime: entry.publishedAt,\n            modifiedTime: entry.updatedAt,\n        },\n    };\n}\n\nexport default async function CustomDomainEntryPage({params}: EntryPageProps) {\n    const {domain, entryId} = await params;\n    const project = await getProjectFromDomain(domain);\n\n    if (!project?.changelog) {\n        notFound();\n    }\n\n    const entry = await getEntry(project.changelog.id, entryId);\n\n    if (!entry) {\n        notFound();\n    }\n\n    return (\n        <EntryContent\n            domain={domain}\n            projectId={project.id}\n            projectName={project.name}\n            entry={entry}\n        />\n    );\n}\n"
  },
  {
    "path": "app/changelog/custom-domain/[domain]/layout.tsx",
    "content": "import {Metadata} from 'next'\nimport React from \"react\";\nimport ButtonGroup from \"@/components/changelog/ButtonGroup\";\nimport {getDomainByDomain} from '@/lib/custom-domains/service';\n\ninterface CustomDomainLayoutProps {\n    params: Promise<{\n        domain: string;\n        path?: string[];\n    }>\n    children: React.ReactNode\n}\n\nexport async function generateMetadata(\n    {params}: CustomDomainLayoutProps\n): Promise<Metadata> {\n    const {domain: encodedDomain} = await params;\n    const domain = decodeURIComponent(encodedDomain);\n\n    const domainConfig = await getDomainByDomain(domain);\n\n    if (!domainConfig || !domainConfig.verified) {\n        return {\n            metadataBase: new URL(`https://${domain}`),\n        };\n    }\n\n    return {\n        metadataBase: new URL(`https://${domain}`),\n        alternates: {\n            types: {\n                'application/rss+xml': `/rss.xml`,\n            },\n        },\n    };\n}\n\nexport default async function CustomDomainLayout({children, params}: CustomDomainLayoutProps) {\n    const {domain: encodedDomain} = await params;\n    const domain = decodeURIComponent(encodedDomain);\n\n    const domainConfig = await getDomainByDomain(domain);\n    const projectId = domainConfig?.projectId;\n    const isVerified = domainConfig?.verified;\n\n    return (\n        <div className=\"container mx-auto py-8\">\n            {isVerified && projectId && <ButtonGroup projectId={projectId} />}\n            {children}\n        </div>\n    );\n}"
  },
  {
    "path": "app/changelog/custom-domain/[domain]/page.tsx",
    "content": "// noinspection HtmlUnknownTarget\n\nimport {Suspense} from 'react';\nimport {notFound} from 'next/navigation';\nimport ChangelogEntries from '@/components/changelog/ChangelogEntries';\nimport ShareButton from '@/components/changelog/ShareButton';\nimport {Skeleton} from '@/components/ui/skeleton';\nimport {GitBranch, Clock, Rss} from 'lucide-react';\nimport {cn} from '@/lib/utils';\nimport {\n    Tooltip,\n    TooltipContent,\n    TooltipProvider,\n    TooltipTrigger\n} from '@/components/ui/tooltip';\nimport Link from 'next/link';\nimport {Metadata} from 'next';\nimport SubscriptionForm from \"@/components/subscription-form\";\nimport {trackChangelogView} from \"@/lib/middleware/analytics\";\nimport {headers} from \"next/headers\";\nimport {getDomainByDomain} from '@/lib/custom-domains/service';\n\ninterface ChangelogResponse {\n    project: {\n        id: string;\n        name: string;\n        description?: string;\n        emailNotificationsEnabled?: boolean;\n    };\n    items: Array<{\n        id: string;\n        publishedAt: string;\n        title?: string;\n    }>;\n    nextCursor?: string;\n}\n\ntype CustomDomainPageProps = {\n    params: Promise<{\n        domain: string;\n        path?: string[];\n    }>;\n};\n\nasync function getInitialData(projectId: string): Promise<ChangelogResponse | null> {\n    const res = await fetch(\n        `${process.env.NEXT_PUBLIC_APP_URL}/api/changelog/${projectId}/entries`,\n        {next: {revalidate: 300}}\n    );\n\n    if (!res.ok) {\n        if (res.status === 404) return null;\n        throw new Error('Failed to fetch changelog');\n    }\n\n    return res.json();\n}\n\nexport async function generateMetadata(\n    {params}: CustomDomainPageProps\n): Promise<Metadata> {\n    const {domain: encodedDomain} = await params;\n    const domain = decodeURIComponent(encodedDomain);\n\n    const domainConfig = await getDomainByDomain(domain);\n\n    if (!domainConfig) {\n        return {\n            title: 'Domain Not Found',\n            description: 'The requested domain configuration was not found.'\n        };\n    }\n\n    if (!domainConfig.verified) {\n        return {\n            title: `${domain} - Verification Required`,\n            description: 'Domain verification is required to access this changelog.'\n        };\n    }\n\n    const data = await getInitialData(domainConfig.projectId);\n\n    if (!data) {\n        return {\n            title: 'Changelog Not Found',\n            description: 'The requested changelog could not be found.'\n        };\n    }\n\n    const {project, items} = data;\n    const latestUpdate = items[0]?.publishedAt\n        ? new Date(items[0].publishedAt).toLocaleDateString('en-US', {\n            year: 'numeric',\n            month: 'long',\n            day: 'numeric'\n        })\n        : null;\n\n    return {\n        title: `${project.name} Changelog & Release Notes`,\n        description: project.description ||\n            `View the latest updates and changes for ${project.name}${latestUpdate ? ` - last updated ${latestUpdate}` : ''}.`,\n        openGraph: {\n            title: `${project.name} Changelog`,\n            description: project.description ||\n                `Stay up to date with the latest improvements, features, and bug fixes for ${project.name}.`,\n            type: 'website',\n            url: `https://${domain}`,\n        },\n        alternates: {\n            canonical: `https://${domain}`,\n            types: {\n                'application/rss+xml': `https://${domain}/rss.xml`,\n            },\n        },\n    };\n}\n\n// Loading component for Suspense\nfunction ChangelogSkeleton() {\n    return (\n        <div className=\"space-y-12\">\n            {[...Array(3)].map((_, i) => (\n                <div key={i} className=\"space-y-4 animate-pulse\">\n                    <div className=\"space-y-3\">\n                        <Skeleton className=\"h-8 w-[300px]\"/>\n                        <Skeleton className=\"h-5 w-[200px]\"/>\n                    </div>\n                    <Skeleton className=\"h-48 w-full rounded-lg\"/>\n                    <div className=\"flex gap-2 pt-4\">\n                        <Skeleton className=\"h-6 w-16 rounded-full\"/>\n                        <Skeleton className=\"h-6 w-16 rounded-full\"/>\n                        <Skeleton className=\"h-6 w-16 rounded-full\"/>\n                    </div>\n                </div>\n            ))}\n        </div>\n    );\n}\n\n// Domain verification page component\nfunction DomainVerificationPage({domain}: { domain: string }) {\n    return (\n        <div className=\"min-h-[calc(100vh-4rem)] bg-background flex items-center justify-center px-4\">\n            <div className=\"max-w-2xl w-full mx-auto\">\n                <div className=\"text-center\">\n                    {/* Large warning icon */}\n                    <div className=\"w-24 h-24 mx-auto mb-8\">\n                        <svg className=\"w-full h-full text-red-600\" fill=\"none\" stroke=\"currentColor\"\n                             viewBox=\"0 0 24 24\">\n                            <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1.5}\n                                  d=\"M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z\"/>\n                        </svg>\n                    </div>\n\n                    {/* Main heading */}\n                    <h1 className=\"text-4xl font-normal text-foreground mb-6\">\n                        Domain verification required\n                    </h1>\n\n                    {/* URL with proper handling for long domains */}\n                    <div className=\"bg-muted rounded-lg p-4 mb-8 border-l-4 border-red-600\">\n                        <p className=\"text-muted-foreground text-sm mb-2 uppercase tracking-wide font-medium\">\n                            DOMAIN\n                        </p>\n                        <p className=\"text-foreground font-mono text-lg break-all leading-relaxed\">\n                            {domain}\n                        </p>\n                    </div>\n\n                    {/* Error message */}\n                    <div\n                        className=\"bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-6 mb-8\">\n                        <h2 className=\"text-lg font-semibold text-red-800 dark:text-red-200 mb-3\">\n                            DNS verification pending\n                        </h2>\n                        <p className=\"text-red-700 dark:text-red-300 text-sm\">\n                            This domain is configured but has not completed the verification process.\n                            DNS records must be validated before content can be served.\n                        </p>\n                    </div>\n\n                    {/* Technical details */}\n                    <div className=\"text-left bg-muted rounded-lg p-6 mb-8\">\n                        <h3 className=\"font-semibold text-foreground mb-4\">Technical Details</h3>\n                        <div className=\"space-y-3 text-sm\">\n                            <div className=\"flex\">\n                                <span className=\"text-muted-foreground w-24 flex-shrink-0\">Status:</span>\n                                <span className=\"text-foreground\">Unverified</span>\n                            </div>\n                            <div className=\"flex\">\n                                <span className=\"text-muted-foreground w-24 flex-shrink-0\">Error:</span>\n                                <span\n                                    className=\"text-foreground font-mono\">DNS_VERIFICATION_REQUIRED</span>\n                            </div>\n                            <div className=\"flex\">\n                                <span className=\"text-muted-foreground w-24 flex-shrink-0\">Code:</span>\n                                <span className=\"text-foreground font-mono\">ERR_DOMAIN_NOT_VERIFIED</span>\n                            </div>\n                        </div>\n                    </div>\n\n                    {/* Footer note */}\n                    <p className=\"text-xs text-muted-foreground mt-8\">\n                        Contact your system administrator if you believe this is an error\n                    </p>\n                </div>\n            </div>\n        </div>\n    );\n}\n\nexport default async function CustomDomainPage({params}: CustomDomainPageProps) {\n    const {domain: encodedDomain, path} = await params;\n    const domain = decodeURIComponent(encodedDomain);\n    const pathSegments = path || [];\n\n    // Look up the domain configuration\n    const domainConfig = await getDomainByDomain(domain);\n\n    if (!domainConfig) {\n        notFound();\n    }\n\n    // If domain is not verified, show verification instructions\n    if (!domainConfig.verified) {\n        return <DomainVerificationPage domain={domain}/>;\n    }\n\n    // Get the project data using the domain's project ID\n    const projectId = domainConfig.projectId;\n    const data = await getInitialData(projectId);\n\n    if (!data) {\n        notFound();\n    }\n\n    // Track the changelog view asynchronously (don't block rendering)\n    try {\n        const headersList = await headers();\n        const request = new Request(`https://${domain}`, {\n            headers: headersList\n        });\n\n        // Track the main changelog page view\n        await trackChangelogView(request, {\n            projectId: data.project.id,\n            // No specific entry ID for main page view\n        });\n    } catch (error) {\n        // Don't let analytics tracking errors break the page\n        console.error('Failed to track changelog view:', error);\n    }\n\n    const stats = {\n        totalEntries: data.items.length + (data.nextCursor ? '+' : ''),\n        lastUpdate: data.items[0]?.publishedAt,\n    };\n\n    // Generate current page URL for sharing (use custom domain)\n    const pageUrl = `https://${domain}${pathSegments.length > 0 ? `/${pathSegments.join('/')}` : ''}`;\n\n    return (\n        <div className=\"min-h-screen bg-background\">\n            {/* Gradient background */}\n            <div className=\"absolute top-0 inset-x-0 h-96 overflow-hidden pointer-events-none\">\n                <div className=\"absolute inset-0 bg-gradient-to-b from-primary/10 via-primary/5 to-transparent\"/>\n                <div\n                    className=\"absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(var(--primary-rgb),0.1),transparent)]\"/>\n            </div>\n\n            <div className=\"relative pb-24\">\n                <header className=\"relative py-16 md:py-24 px-4 max-w-6xl mx-auto\">\n                    <div className=\"text-center space-y-6\">\n                        {/* Project name */}\n                        <h1\n                            className={cn(\n                                \"text-4xl md:text-6xl lg:text-7xl font-bold tracking-tight\",\n                                \"bg-gradient-to-b from-foreground via-foreground/95 to-foreground/80\",\n                                \"bg-clip-text text-transparent\"\n                            )}\n                        >\n                            {data.project.name}\n                        </h1>\n\n                        {/* Subtitle */}\n                        <p className=\"text-xl md:text-2xl text-muted-foreground font-medium\">\n                            Changelog &amp; Release Notes\n                        </p>\n\n                        {/* Description if available */}\n                        {data.project.description && (\n                            <p className=\"text-lg text-muted-foreground max-w-2xl mx-auto\">\n                                {data.project.description}\n                            </p>\n                        )}\n\n                        {/* Stats display with RSS and share links */}\n                        <div className=\"flex flex-col items-center gap-6\">\n                            <div className=\"inline-flex flex-wrap justify-center items-center gap-4 md:gap-8 px-6 md:px-8 py-4\n                            bg-background/60 backdrop-blur-sm\n                            border border-border/40\n                            rounded-full\n                            hover:bg-background/80 hover:border-border/60\n                            transition-all duration-300\">\n                                <div className=\"flex items-center gap-3\">\n                                    <GitBranch className=\"w-5 h-5 text-muted-foreground\"/>\n                                    <span className=\"font-medium text-lg\">\n                                        {stats.totalEntries} Updates\n                                    </span>\n                                </div>\n\n                                {stats.lastUpdate && (\n                                    <>\n                                        <div className=\"hidden md:block w-1.5 h-1.5 rounded-full bg-border\"/>\n                                        <div className=\"flex items-center gap-3\">\n                                            <Clock className=\"w-5 h-5 text-muted-foreground\"/>\n                                            <time\n                                                dateTime={stats.lastUpdate}\n                                                className=\"font-medium text-lg tabular-nums\"\n                                            >\n                                                {new Intl.DateTimeFormat('en-US', {\n                                                    month: 'long',\n                                                    day: 'numeric',\n                                                    year: 'numeric',\n                                                }).format(new Date(stats.lastUpdate))}\n                                            </time>\n                                        </div>\n                                    </>\n                                )}\n\n                                <div className=\"hidden md:block w-1.5 h-1.5 rounded-full bg-border\"/>\n\n                                <TooltipProvider>\n                                    <Tooltip>\n                                        <TooltipTrigger asChild>\n                                            <Link\n                                                href=\"/rss.xml\"\n                                                className=\"flex items-center gap-2 text-muted-foreground hover:text-orange-500 transition-colors duration-200\"\n                                                aria-label=\"Subscribe to RSS feed\"\n                                            >\n                                                <Rss className=\"w-5 h-5\"/>\n                                                <span className=\"font-medium text-lg\">RSS</span>\n                                            </Link>\n                                        </TooltipTrigger>\n                                        <TooltipContent>\n                                            Subscribe to updates via RSS\n                                        </TooltipContent>\n                                    </Tooltip>\n                                </TooltipProvider>\n\n                                <div className=\"hidden md:block w-1.5 h-1.5 rounded-full bg-border\"/>\n\n                                {/* Client-side Share Button */}\n                                <ShareButton\n                                    url={pageUrl}\n                                    title={`${data.project.name} Changelog`}\n                                    text={data.project.description || `Check out the latest updates for ${data.project.name}`}\n                                />\n                            </div>\n                        </div>\n                        {/* Only show subscription form if email notifications are enabled */}\n                        {data.project.emailNotificationsEnabled && (\n                            <SubscriptionForm projectId={projectId} projectName={data.project.name}/>\n                        )}\n                    </div>\n                </header>\n\n                <div className=\"relative max-w-7xl mx-auto px-4 md:px-6\">\n                    <Suspense fallback={<ChangelogSkeleton/>}>\n                        <ChangelogEntries projectId={projectId}/>\n                    </Suspense>\n                </div>\n\n                {/* Footer spacer */}\n                <div className=\"h-12\"/>\n            </div>\n        </div>\n    );\n}"
  },
  {
    "path": "app/changelog/custom-domain/[domain]/rss.xml/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport { db } from '@/lib/db'\nimport { generateRSSFeed } from '@/lib/services/changelog/rss'\nimport { getDomainByDomain } from '@/lib/custom-domains/service'\n\nexport async function GET(\n    request: Request,\n    { params }: { params: Promise<{ domain: string }> }\n) {\n    try {\n        const { domain: encodedDomain } = await params\n        const domain = decodeURIComponent(encodedDomain)\n\n        // Look up the domain configuration\n        const domainConfig = await getDomainByDomain(domain)\n\n        if (!domainConfig || !domainConfig.verified) {\n            return NextResponse.json(\n                { error: 'Domain not found or not verified' },\n                { status: 404 }\n            )\n        }\n\n        const projectId = domainConfig.projectId\n\n        // Get project and changelog entries\n        const project = await db.project.findUnique({\n            where: {\n                id: projectId,\n                isPublic: true\n            },\n            select: {\n                id: true,\n                name: true,\n                changelog: {\n                    select: {\n                        id: true,\n                        entries: {\n                            where: {\n                                publishedAt: { not: null }\n                            },\n                            orderBy: [\n                                { publishedAt: 'desc' },\n                                { id: 'desc' }\n                            ],\n                            take: 10\n                        }\n                    }\n                }\n            }\n        })\n\n        if (!project?.changelog) {\n            return NextResponse.json(\n                { error: 'Changelog not found or not public' },\n                { status: 404 }\n            )\n        }\n\n        // Use the custom domain as the base URL\n        const feedUrl = `https://${domain}`\n\n        const rss = generateRSSFeed(project.changelog.entries, {\n            title: `${project.name} Changelog`,\n            description: `Latest changes and updates for ${project.name}`,\n            link: feedUrl\n        })\n\n        return new NextResponse(rss, {\n            headers: {\n                'Content-Type': 'application/xml;charset=utf-8',\n                'Cache-Control': 'public, max-age=3600',\n            }\n        })\n    } catch (error) {\n        console.error('Error generating RSS feed:', error)\n        return NextResponse.json(\n            { error: 'Failed to generate RSS feed' },\n            { status: 500 }\n        )\n    }\n}"
  },
  {
    "path": "app/changelog/custom-domain/[domain]/unsubscribed/page.tsx",
    "content": "import { getDomainByDomain } from '@/lib/custom-domains/service'\nimport { notFound } from 'next/navigation'\nimport { CheckCircle } from 'lucide-react'\n\ninterface CustomDomainUnsubscribedProps {\n    params: Promise<{\n        domain: string\n    }>\n    searchParams: Promise<{\n        email?: string\n    }>\n}\n\nexport default async function CustomDomainUnsubscribedPage({\n                                                               params,\n                                                               searchParams\n                                                           }: CustomDomainUnsubscribedProps) {\n    const { domain: encodedDomain } = await params\n    const { email } = await searchParams\n    const domain = decodeURIComponent(encodedDomain)\n\n    // Verify domain exists and is verified\n    const domainConfig = await getDomainByDomain(domain)\n\n    if (!domainConfig || !domainConfig.verified) {\n        notFound()\n    }\n\n    return (\n        <div className=\"min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4\">\n            <div className=\"max-w-md w-full\">\n                <div className=\"bg-white rounded-lg shadow-md p-8 text-center\">\n                    <div className=\"w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6\">\n                        <CheckCircle className=\"w-8 h-8 text-green-600\" />\n                    </div>\n\n                    <h1 className=\"text-2xl font-bold text-gray-900 mb-4\">\n                        Successfully Unsubscribed\n                    </h1>\n\n                    {email && (\n                        <p className=\"text-gray-600 mb-6\">\n                            <strong>{email}</strong> has been unsubscribed from email notifications.\n                        </p>\n                    )}\n\n                    <p className=\"text-gray-600 mb-6\">\n                        You will no longer receive email updates from this changelog.\n                    </p>\n\n                    <div className=\"space-y-3\">\n                        <a\n                            href={`https://${domain}`}\n                            className=\"inline-block w-full bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors\"\n                        >\n                            Return to Changelog\n                        </a>\n\n                        <p className=\"text-sm text-gray-500\">\n                            You can always resubscribe by visiting the changelog page.\n                        </p>\n                    </div>\n                </div>\n            </div>\n        </div>\n    )\n}\n\nexport async function generateMetadata({ params }: CustomDomainUnsubscribedProps) {\n    const { domain: encodedDomain } = await params\n    const domain = decodeURIComponent(encodedDomain)\n\n    return {\n        title: `Unsubscribed - ${domain}`,\n        description: 'You have been successfully unsubscribed from email notifications.',\n    }\n}"
  },
  {
    "path": "app/cli/auth/page.tsx",
    "content": "'use client';\n\nimport {useEffect, useState, Suspense} from 'react';\nimport {useSearchParams} from 'next/navigation';\nimport {Button} from '@/components/ui/button';\nimport {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@/components/ui/card';\nimport {Alert, AlertDescription} from '@/components/ui/alert';\nimport {CheckCircle, Terminal, ExternalLink} from 'lucide-react';\n\ninterface AuthState {\n    status: 'checking' | 'authenticated' | 'unauthenticated' | 'generating' | 'success' | 'error';\n    user?: {\n        id: string;\n        email: string;\n        name: string;\n        role: string;\n    };\n    error?: string;\n    authCode?: string;\n    callbackUrl?: string;\n}\n\n// Separate component that uses useSearchParams\nfunction CLIAuthContent() {\n    const searchParams = useSearchParams();\n    const callbackUrl = searchParams.get('callback');\n    const [authState, setAuthState] = useState<AuthState>({status: 'checking'});\n\n    const isChecking = authState.status === 'checking';\n    const isAuthenticated = authState.status === 'authenticated';\n    const isUnauthenticated = authState.status === 'unauthenticated';\n    const isGenerating = authState.status === 'generating';\n    const isSuccess = authState.status === 'success';\n    const isError = authState.status === 'error';\n\n    // Determine branding based on callback URL\n    const isWriteWithCum = callbackUrl?.startsWith('wwc://');\n    const appName = isWriteWithCum ? 'WriteWithCum' : 'Changerawr CLI';\n\n    useEffect(() => {\n        checkAuthenticationStatus();\n    }, []);\n\n    const checkAuthenticationStatus = async (): Promise<void> => {\n        try {\n            const response = await fetch('/api/auth/me');\n\n            if (response.ok) {\n                const user = await response.json();\n                setAuthState({\n                    status: 'authenticated',\n                    user,\n                    callbackUrl: callbackUrl || undefined\n                });\n            } else {\n                setAuthState({status: 'unauthenticated'});\n            }\n        } catch {\n            setAuthState({\n                status: 'error',\n                error: 'Failed to check authentication status'\n            });\n        }\n    };\n\n    const generateAuthCode = async (): Promise<void> => {\n        if (!callbackUrl) {\n            setAuthState({\n                status: 'error',\n                error: 'No callback URL provided'\n            });\n            return;\n        }\n\n        setAuthState(prev => ({...prev, status: 'generating'}));\n\n        try {\n            const response = await fetch('/api/auth/cli/generate', {\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json',\n                },\n                body: JSON.stringify({callbackUrl}),\n            });\n\n            if (response.ok) {\n                const {code, expires} = await response.json();\n\n                const redirectUrl = new URL(callbackUrl);\n                redirectUrl.searchParams.set('code', code);\n                redirectUrl.searchParams.set('expires', expires.toString());\n                redirectUrl.searchParams.set('instance', window.location.origin);\n\n                setAuthState({\n                    status: 'success',\n                    authCode: code,\n                    callbackUrl\n                });\n\n                setTimeout(() => {\n                    window.location.href = redirectUrl.toString();\n                }, 2000);\n\n            } else {\n                const errorData = await response.json();\n                setAuthState({\n                    status: 'error',\n                    error: errorData.error || 'Failed to generate authentication code'\n                });\n            }\n        } catch {\n            setAuthState({\n                status: 'error',\n                error: 'Network error while generating authentication code'\n            });\n        }\n    };\n\n    const handleLoginRedirect = (): void => {\n        const loginUrl = new URL('/login', window.location.origin);\n        loginUrl.searchParams.set('redirect', window.location.pathname + window.location.search);\n        window.location.href = loginUrl.toString();\n    };\n\n    return (\n        <div className=\"min-h-screen bg-transparent flex items-center justify-center p-4\">\n            <Card className=\"w-full max-w-md\">\n                <CardHeader className=\"text-center\">\n                    <div className=\"mx-auto w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mb-4\">\n                        <Terminal className=\"w-6 h-6 text-blue-600\"/>\n                    </div>\n                    <CardTitle>{appName} Authorization</CardTitle>\n                    <CardDescription>\n                        Authorize {appName} to access your account\n                    </CardDescription>\n                </CardHeader>\n\n                <CardContent className=\"space-y-4\">\n                    {isChecking && (\n                        <div className=\"text-center py-8\">\n                            <div\n                                className=\"animate-spin w-8 h-8 border-2 border-blue-600 border-t-transparent rounded-full mx-auto mb-4\"></div>\n                            <p className=\"text-gray-600\">Checking authentication status...</p>\n                        </div>\n                    )}\n\n                    {isUnauthenticated && (\n                        <>\n                            <Alert>\n                                <AlertDescription>\n                                    You need to be logged in to authorize {appName}.\n                                </AlertDescription>\n                            </Alert>\n                            <Button\n                                onClick={handleLoginRedirect}\n                                className=\"w-full\"\n                                size=\"lg\"\n                            >\n                                <ExternalLink className=\"w-4 h-4 mr-2\"/>\n                                Login to Changerawr\n                            </Button>\n                        </>\n                    )}\n\n                    {isAuthenticated && authState.user && (\n                        <>\n                            <div className=\"text-center py-4\">\n                                <CheckCircle className=\"w-8 h-8 text-green-600 mx-auto mb-2\"/>\n                                <p className=\"font-medium\">Logged in as</p>\n                                <p className=\"text-sm text-gray-600\">{authState.user.email}</p>\n                            </div>\n\n                            {callbackUrl ? (\n                                <>\n                                    <Alert>\n                                        <AlertDescription>\n                                            {appName} is requesting access to your account.\n                                            Click authorize to generate a temporary authentication code.\n                                        </AlertDescription>\n                                    </Alert>\n                                    <Button\n                                        onClick={generateAuthCode}\n                                        className=\"w-full\"\n                                        size=\"lg\"\n                                        disabled={isGenerating}\n                                    >\n                                        {isGenerating ? (\n                                            <>\n                                                <div\n                                                    className=\"animate-spin w-4 h-4 border-2 border-white border-t-transparent rounded-full mr-2\"></div>\n                                                Generating Code...\n                                            </>\n                                        ) : (\n                                            `Authorize ${appName} Access`\n                                        )}\n                                    </Button>\n                                </>\n                            ) : (\n                                <Alert>\n                                    <AlertDescription>\n                                        No callback URL provided. Please use {appName} to initiate authentication.\n                                    </AlertDescription>\n                                </Alert>\n                            )}\n                        </>\n                    )}\n\n                    {isGenerating && (\n                        <div className=\"text-center py-4\">\n                            <div\n                                className=\"animate-spin w-8 h-8 border-2 border-blue-600 border-t-transparent rounded-full mx-auto mb-4\"></div>\n                            <p className=\"text-gray-600\">Generating authentication code...</p>\n                        </div>\n                    )}\n\n                    {isSuccess && (\n                        <div className=\"text-center py-4\">\n                            <CheckCircle className=\"w-8 h-8 text-green-600 mx-auto mb-2\"/>\n                            <p className=\"font-medium text-green-600\">Authorization Successful!</p>\n                            <p className=\"text-sm text-gray-600 mt-2\">\n                                Redirecting to {appName}... You can close this window if it doesn&apos;t redirect\n                                automatically.\n                            </p>\n                        </div>\n                    )}\n\n                    {isError && (\n                        <>\n                            <Alert variant=\"destructive\">\n                                <AlertDescription>\n                                    {authState.error}\n                                </AlertDescription>\n                            </Alert>\n                            <Button\n                                onClick={checkAuthenticationStatus}\n                                variant=\"outline\"\n                                className=\"w-full\"\n                            >\n                                Try Again\n                            </Button>\n                        </>\n                    )}\n\n                    <div className=\"text-center pt-4 border-t\">\n                        <p className=\"text-xs text-gray-500\">\n                            This will give {appName} access to your Changerawr projects.\n                            You can revoke this access at any time from your account settings.\n                        </p>\n                    </div>\n                </CardContent>\n            </Card>\n        </div>\n    );\n}\n\n// Loading fallback component\nfunction CLIAuthLoading() {\n    return (\n        <div className=\"min-h-screen bg-transparent flex items-center justify-center p-4\">\n            <Card className=\"w-full max-w-md\">\n                <CardHeader className=\"text-center\">\n                    <div className=\"mx-auto w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mb-4\">\n                        <Terminal className=\"w-6 h-6 text-blue-600\"/>\n                    </div>\n                    <CardTitle>Authorization</CardTitle>\n                    <CardDescription>\n                        Loading...\n                    </CardDescription>\n                </CardHeader>\n                <CardContent className=\"space-y-4\">\n                    <div className=\"text-center py-8\">\n                        <div\n                            className=\"animate-spin w-8 h-8 border-2 border-blue-600 border-t-transparent rounded-full mx-auto mb-4\"></div>\n                        <p className=\"text-gray-600\">Loading page...</p>\n                    </div>\n                </CardContent>\n            </Card>\n        </div>\n    );\n}\n\n// Main component with Suspense boundary\nexport default function CLIAuthPage() {\n    return (\n        <Suspense fallback={<CLIAuthLoading/>}>\n            <CLIAuthContent/>\n        </Suspense>\n    );\n}"
  },
  {
    "path": "app/dashboard/admin/about/page.tsx",
    "content": "'use client'\n\nimport React, {useEffect, useState} from 'react';\nimport {Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle} from '@/components/ui/card';\nimport {Badge} from '@/components/ui/badge';\nimport {Button} from '@/components/ui/button';\nimport {appInfo, getCopyrightYears} from '@/lib/app-info';\nimport {Heart, History, Zap, Activity} from 'lucide-react';\nimport UpdateStatus from '@/components/UpdateStatus';\nimport {useWhatsNew} from '@/hooks/useWhatsNew';\nimport WhatsNewModal from '@/components/dashboard/WhatsNewModal';\nimport DinoGame from '@/components/DinoGame';\nimport {UpdateStatus as UpdateStatusType} from '@/lib/types/easypanel';\nimport {useTimezone} from '@/hooks/use-timezone';\n\nexport default function AboutPage() {\n    const [databaseInfo, setDatabaseInfo] = useState<{ databaseVersion?: string }>({});\n    const [updateStatus, setUpdateStatus] = useState<UpdateStatusType | null>(null);\n    const [showDinoGame, setShowDinoGame] = useState(false);\n    const [rawrClickCount, setRawrClickCount] = useState(0);\n    const [licenseActive, setLicenseActive] = useState(false);\n    const [sslEnabled, setSslEnabled] = useState(false);\n    const [agentVersion, setAgentVersion] = useState<{ version?: string; status?: string } | null>(null);\n    const timezone = useTimezone();\n\n    const {\n        showWhatsNew,\n        whatsNewContent,\n        closeWhatsNew,\n        manuallyShowWhatsNew,\n        isLoading,\n    } = useWhatsNew();\n\n    const handleRawrClick = () => {\n        setRawrClickCount(prev => {\n            const newCount = prev + 1;\n            if (newCount >= 5) {\n                setShowDinoGame(true);\n                return 0; // Reset counter\n            }\n            return newCount;\n        });\n    };\n\n    useEffect(() => {\n        async function fetchSystemInfo() {\n            try {\n                // Fetch runtime config\n                const configResponse = await fetch('/api/config/runtime');\n                const configData = await configResponse.json();\n                setSslEnabled(configData.sslEnabled);\n\n                // Fetch database info\n                const versionResponse = await fetch('/api/system/version');\n                const versionData = await versionResponse.json();\n                setDatabaseInfo(versionData);\n\n                // Fetch update status (includes Easypanel info)\n                const updateResponse = await fetch('/api/system/update-status');\n                if (updateResponse.ok) {\n                    const updateData = await updateResponse.json();\n                    setUpdateStatus(updateData);\n                }\n\n                // Fetch license status\n                try {\n                    const licenseResponse = await fetch('/api/admin/sponsor');\n                    if (licenseResponse.ok) {\n                        const licenseData = await licenseResponse.json();\n                        setLicenseActive(licenseData.active === true);\n                    }\n                } catch {}\n\n                // Fetch nginx-agent version if SSL is enabled\n                if (configData.sslEnabled) {\n                    try {\n                        const agentResponse = await fetch('/api/system/agent-version');\n                        if (agentResponse.ok) {\n                            const agentData = await agentResponse.json();\n                            setAgentVersion(agentData);\n                        }\n                    } catch {}\n                }\n            } catch (error) {\n                console.error('Failed to fetch system info:', error);\n            }\n        }\n\n        fetchSystemInfo();\n    }, []);\n\n    return (\n        <div className=\"max-w-lg mx-auto space-y-6 py-6\">\n            {/* What's New Modal */}\n            {whatsNewContent && (\n                <WhatsNewModal\n                    isOpen={showWhatsNew}\n                    onClose={closeWhatsNew}\n                    content={whatsNewContent}\n                />\n            )}\n\n            {/* Dino Game Modal */}\n            <DinoGame\n                isOpen={showDinoGame}\n                onClose={() => setShowDinoGame(false)}\n            />\n\n            <Card className=\"border-2 overflow-hidden\">\n                <CardHeader className=\"text-center pb-2\">\n                    <div className=\"flex justify-center mb-4\">\n                        <div className=\"w-24 h-24 rounded-full flex items-center justify-center bg-primary/10\">\n                            <span className=\"text-4xl font-bold\">🦖</span>\n                        </div>\n                    </div>\n                    <CardTitle className=\"text-3xl font-bold\">Changerawr</CardTitle>\n                    <CardDescription>Ship, Change, Rawr 🦖</CardDescription>\n                </CardHeader>\n                <CardContent className=\"text-center\">\n                    <div className=\"flex justify-center gap-2 mb-4\">\n                        <Badge variant=\"outline\" className=\"px-3 py-1\">v{appInfo.version}</Badge>\n                        <Badge variant=\"secondary\" className=\"px-3 py-1\">{appInfo.status}</Badge>\n                        {updateStatus?.easypanelConfigured && (\n                            <Badge variant=\"default\" className=\"px-3 py-1 bg-blue-600 hover:bg-blue-700\">\n                                <Zap className=\"h-3 w-3 mr-1\"/>\n                                Easypanel\n                            </Badge>\n                        )}\n                    </div>\n\n                    {/* What's New Button */}\n                    <Button\n                        onClick={manuallyShowWhatsNew}\n                        variant=\"outline\"\n                        size=\"sm\"\n                        className=\"mb-4\"\n                        disabled={isLoading}\n                    >\n                        <History className=\"h-4 w-4 mr-2\"/>\n                        {isLoading ? \"Loading...\" : \"What's New in This Version\"}\n                    </Button>\n\n                    {/* Update Status Component */}\n                    <div className=\"pt-2\">\n                        <UpdateStatus\n                            currentVersion={appInfo.version}\n                            checkOnMount={true}\n                            autoCheckInterval={60 * 60 * 1000} // Check every hour - pretty useless unless you left the page open\n                            showEasypanelInfo={false} // We handle this above\n                        />\n                    </div>\n\n                    <div className=\"max-w-xs mx-auto mt-4\">\n                        <p className=\"text-sm text-muted-foreground\">\n                            Making changelog management cute and simple since 2025!\n                            Keep your users updated with adorable, organized release notes. ✨\n                        </p>\n                    </div>\n                </CardContent>\n                <CardFooter className=\"flex flex-col items-center pt-2 pb-6\">\n                    <p className=\"text-sm text-muted-foreground flex items-center gap-1.5\">\n                        Made with <Heart className=\"h-4 w-4 text-red-500 fill-red-500\"/> by <a\n                        href=\"https://superdev.one\" className=\"hover:underline\">Supernova3339</a>\n                    </p>\n                    <p className=\"text-xs text-muted-foreground\">© {getCopyrightYears()} {appInfo.name} • All rights\n                        reserved</p>\n                </CardFooter>\n            </Card>\n\n            {/* System Information */}\n            <Card className=\"border overflow-hidden\">\n                <CardHeader>\n                    <CardTitle className=\"text-lg font-medium\">🛠️ System Information</CardTitle>\n                </CardHeader>\n                <CardContent>\n                    <div className=\"space-y-2 text-sm\">\n                        <div className=\"flex justify-between py-1 border-b border-border/40\">\n                            <span>Application</span>\n                            <span>{appInfo.name}</span>\n                        </div>\n                        <div className=\"flex justify-between py-1 border-b border-border/40\">\n                            <span>Version</span>\n                            <span>{appInfo.version} ({appInfo.status})</span>\n                        </div>\n                        <div className=\"flex justify-between py-1 border-b border-border/40\">\n                            <span>Framework</span>\n                            <span>{appInfo.framework}</span>\n                        </div>\n                        <div className=\"flex justify-between py-1 border-b border-border/40\">\n                            <span>Database</span>\n                            <span>PostgreSQL {databaseInfo?.databaseVersion || 'Unknown'}</span>\n                        </div>\n                        <div className=\"flex justify-between py-1 border-b border-border/40\">\n                            <span>CUM Engine</span>\n                            <span>v{appInfo.cumEngine}</span>\n                        </div>\n                        <div className=\"flex justify-between py-1 border-b border-border/40\">\n                            <span>Environment</span>\n                            <span>{appInfo.environment}</span>\n                        </div>\n                        <div className=\"flex justify-between py-1 border-b border-border/40\">\n                            <span>Released</span>\n                            <span>{new Date(appInfo.releaseDate).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', timeZone: timezone })}</span>\n                        </div>\n                        {sslEnabled && agentVersion && (\n                            <div className=\"flex justify-between py-1 border-b border-border/40\">\n                                <span>nginx-agent</span>\n                                <span className=\"flex items-center gap-1\">\n                                    {agentVersion.version || 'Unknown'}\n                                    {agentVersion.status === 'live' && (\n                                        <Activity className=\"h-3 w-3 text-green-500\"/>\n                                    )}\n                                </span>\n                            </div>\n                        )}\n                        {updateStatus?.easypanelConfigured && (\n                            <>\n                                <div className=\"flex justify-between py-1 border-b border-border/40\">\n                                    <span>Deployment</span>\n                                    <span className=\"flex items-center gap-1\">\n                                        <Activity className=\"h-3 w-3 text-green-500\"/>\n                                        Auto-managed\n                                    </span>\n                                </div>\n                                <div className=\"flex justify-between py-1\">\n                                    <span>Updates</span>\n                                    <span className=\"text-blue-600\">Automatic</span>\n                                </div>\n                            </>\n                        )}\n                    </div>\n                </CardContent>\n            </Card>\n\n            {/* Sponsor Thank You */}\n            {licenseActive && (\n                <Card className=\"border-2 border-pink-200 dark:border-pink-800 bg-gradient-to-br from-pink-50/50 to-purple-50/50 dark:from-pink-950/20 dark:to-purple-950/20 overflow-hidden\">\n                    <CardContent className=\"pt-6 text-center\">\n                        <div className=\"flex justify-center mb-3\">\n                            <Heart className=\"h-8 w-8 text-pink-500 fill-pink-500\"/>\n                        </div>\n                        <h3 className=\"text-lg font-semibold mb-1\">Thank You for Sponsoring!</h3>\n                        <p className=\"text-sm text-muted-foreground\">\n                            Your support helps keep Changerawr alive and growing.\n                            Extended features are unlocked for this instance.\n                        </p>\n                    </CardContent>\n                </Card>\n            )}\n\n            {/* Easter egg - Secret Dino Game */}\n            <div className=\"text-center text-xs text-muted-foreground pt-2\">\n                <span\n                    className=\"cursor-pointer hover:text-primary transition-colors select-none\"\n                    onClick={handleRawrClick}\n                    title={rawrClickCount > 0 ? `${5 - rawrClickCount} more clicks...` : 'Click me!'}\n                >\n                    rawr~ ʕ•ᴥ•ʔ\n                </span>\n                {rawrClickCount > 0 && rawrClickCount < 5 && (\n                    <div className=\"mt-1 text-[10px] opacity-50\">\n                        {5 - rawrClickCount} more...\n                    </div>\n                )}\n            </div>\n        </div>\n    );\n}"
  },
  {
    "path": "app/dashboard/admin/ai-settings/page.tsx",
    "content": "'use client'\n\nimport React, { useState, useEffect } from 'react'\nimport { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardFooter,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card'\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { Switch } from '@/components/ui/switch'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'\nimport { useToast } from '@/hooks/use-toast'\nimport {\n    Sparkles,\n    Info,\n    Save,\n    CheckCircle,\n    XCircle,\n    Copy,\n    Lock,\n    Loader2,\n    ExternalLink\n} from 'lucide-react'\nimport {\n    Tooltip,\n    TooltipContent,\n    TooltipProvider,\n    TooltipTrigger,\n} from '@/components/ui/tooltip'\nimport {\n    Alert,\n    AlertDescription,\n    AlertTitle,\n} from '@/components/ui/alert'\nimport { Badge } from '@/components/ui/badge'\nimport { motion, AnimatePresence } from 'framer-motion'\nimport { Separator } from '@/components/ui/separator'\n\ninterface AISettings {\n    enableAIAssistant: boolean\n    aiApiKey: string | null\n    aiDefaultModel: string | null\n}\n\nexport default function AISettingsPage() {\n    const { toast } = useToast()\n    const queryClient = useQueryClient()\n\n    // Local form state\n    const [formData, setFormData] = useState<AISettings>({\n        enableAIAssistant: false,\n        aiApiKey: '',\n        aiDefaultModel: 'copilot-zero',\n    })\n\n    // Track if the API key has been changed\n    const [apiKeyChanged, setApiKeyChanged] = useState(false)\n\n    // Track the copied state for the promo code\n    const [promoCodeCopied, setPromoCodeCopied] = useState(false)\n\n    // Validation state\n    const [keyValidated, setKeyValidated] = useState<boolean | null>(null)\n\n    // Fetch current settings\n    const { data: settings, isLoading } = useQuery<AISettings>({\n        queryKey: ['ai-settings'],\n        queryFn: async () => {\n            const response = await fetch('/api/admin/ai-settings')\n            if (!response.ok) throw new Error('Failed to fetch AI settings')\n\n            const data = await response.json()\n\n            // Update form data when settings are loaded\n            setFormData({\n                enableAIAssistant: data.enableAIAssistant,\n                aiApiKey: data.aiApiKey ? '••••••••••••••••' : '', // Mask the API key\n                aiDefaultModel: data.aiDefaultModel || 'copilot-zero',\n            })\n\n            return data\n        },\n        refetchOnWindowFocus: false,\n    })\n\n    // Mutation to save settings\n    const { mutate: saveSettings, isPending: isSaving } = useMutation({\n        mutationFn: async (data: Partial<AISettings>) => {\n            const response = await fetch('/api/admin/ai-settings', {\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json',\n                },\n                body: JSON.stringify({\n                    ...data,\n                    // Always set the provider to Secton\n                    aiApiProvider: 'secton',\n                }),\n            })\n\n            if (!response.ok) {\n                const error = await response.json()\n                throw new Error(error.message || 'Failed to save settings')\n            }\n\n            return response.json()\n        },\n        onSuccess: () => {\n            toast({\n                title: 'Settings saved',\n                description: 'AI Assistant settings have been updated successfully.',\n                duration: 3000,\n            })\n\n            // Reset API key changed flag\n            setApiKeyChanged(false)\n\n            // Invalidate the query to refresh data\n            queryClient.invalidateQueries({ queryKey: ['ai-settings'] })\n        },\n        onError: (error) => {\n            toast({\n                title: 'Error',\n                description: error.message || 'Failed to save settings',\n                variant: 'destructive',\n                duration: 5000,\n            })\n        },\n    })\n\n    // Test API key validity\n    const { mutate: testApiKey, isPending: isTesting } = useMutation({\n        mutationFn: async (apiKey: string) => {\n            const response = await fetch('/api/admin/ai-settings/test-key', {\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json',\n                },\n                body: JSON.stringify({\n                    apiKey,\n                    provider: 'secton'\n                }),\n            })\n\n            if (!response.ok) {\n                const error = await response.json()\n                throw new Error(error.message || 'API key validation failed')\n            }\n\n            return response.json()\n        },\n        // eslint-disable-next-line @typescript-eslint/no-unused-vars\n        onSuccess: (data) => {\n            setKeyValidated(true)\n            toast({\n                title: 'API Key Valid',\n                description: 'Your key has been validated and is working correctly.',\n                duration: 3000,\n            })\n        },\n        onError: (error) => {\n            setKeyValidated(false)\n            toast({\n                title: 'Invalid API Key',\n                description: error.message || 'Could not validate API key',\n                variant: 'destructive',\n                duration: 5000,\n            })\n        },\n    })\n\n    // Reset validation state when the API key changes\n    useEffect(() => {\n        if (apiKeyChanged) {\n            setKeyValidated(null)\n        }\n    }, [apiKeyChanged])\n\n    // Handle form changes\n    const handleChange = <K extends keyof AISettings>(key: K, value: AISettings[K]) => {\n        setFormData(prev => ({\n            ...prev,\n            [key]: value\n        }))\n\n        // Track if the API key was changed\n        if (key === 'aiApiKey') {\n            setApiKeyChanged(true)\n        }\n    }\n\n    // Handle form submission\n    const handleSubmit = (e: React.FormEvent) => {\n        e.preventDefault()\n\n        // Prepare the data for submission\n        const dataToSubmit: Partial<AISettings> = {\n            enableAIAssistant: formData.enableAIAssistant,\n            aiDefaultModel: formData.aiDefaultModel,\n        }\n\n        // Only include API key if it was changed and isn't masked\n        if (apiKeyChanged && !formData.aiApiKey?.includes('•')) {\n            dataToSubmit.aiApiKey = formData.aiApiKey\n        }\n\n        // Save the settings\n        saveSettings(dataToSubmit)\n    }\n\n    const handleTestApiKey = () => {\n        if (formData.aiApiKey && !formData.aiApiKey.includes('•')) {\n            testApiKey(formData.aiApiKey)\n        } else {\n            toast({\n                title: 'Cannot test API key',\n                description: 'Please enter a new API key first',\n                variant: 'destructive',\n                duration: 3000,\n            })\n        }\n    }\n\n    const handleCopyPromoCode = () => {\n        navigator.clipboard.writeText('CHANGERAWR')\n        setPromoCodeCopied(true)\n        setTimeout(() => setPromoCodeCopied(false), 2000)\n\n        toast({\n            title: 'Promo Code Copied',\n            description: 'Use CHANGERAWR at checkout for 30% off your first purchase',\n            duration: 3000,\n        })\n    }\n\n    if (isLoading) {\n        return (\n            <div className=\"container max-w-4xl mx-auto p-4\">\n                <Card className=\"w-full\">\n                    <CardHeader>\n                        <CardTitle>AI Integration Settings</CardTitle>\n                        <CardDescription>Loading configuration...</CardDescription>\n                    </CardHeader>\n                    <CardContent>\n                        <div className=\"flex items-center justify-center p-8\">\n                            <div className=\"h-10 w-10 animate-spin rounded-full border-4 border-primary border-t-transparent\"></div>\n                        </div>\n                    </CardContent>\n                </Card>\n            </div>\n        )\n    }\n\n    return (\n        <div className=\"container max-w-4xl mx-auto p-4\">\n            <motion.div\n                initial={{ opacity: 0, y: 20 }}\n                animate={{ opacity: 1, y: 0 }}\n                transition={{ duration: 0.3 }}\n            >\n                <div className=\"flex flex-col space-y-6\">\n                    {/* Header Card with Promo */}\n                    <Card className=\"overflow-hidden border-primary/20\">\n                        <div className=\"relative\">\n                            <div className=\"absolute top-0 right-0 w-64 h-64 bg-primary/5 rounded-full -translate-y-1/2 translate-x-1/2\" />\n                            <div className=\"absolute bottom-0 left-0 w-32 h-32 bg-primary/5 rounded-full translate-y-1/2 -translate-x-1/2\" />\n\n                            <CardHeader className=\"relative z-10\">\n                                <div className=\"flex flex-col md:flex-row items-start md:items-center justify-between space-y-2 md:space-y-0\">\n                                    <div className=\"flex items-center\">\n                                        <div className=\"mr-4 bg-primary/10 p-2 rounded-lg\">\n                                            <Sparkles className=\"h-6 w-6 text-primary\" />\n                                        </div>\n                                        <div>\n                                            <CardTitle className=\"text-2xl\">AI Integration</CardTitle>\n                                            <CardDescription className=\"text-base\">\n                                                Power up Changerawr with AI capabilities\n                                            </CardDescription>\n                                        </div>\n                                    </div>\n\n                                    {/*<Badge*/}\n                                    {/*    variant=\"outline\"*/}\n                                    {/*    className=\"border-primary/30 bg-primary/5 text-primary px-3 py-1 text-sm\"*/}\n                                    {/*>*/}\n                                    {/*    <Sparkles className=\"h-3.5 w-3.5 mr-1.5\" />*/}\n                                    {/*    <span>Premium Feature</span>*/}\n                                    {/*</Badge>*/}\n                                </div>\n                            </CardHeader>\n\n                            <CardContent className=\"relative z-10\">\n                                <div className=\"p-4 my-2 rounded-lg bg-gradient-to-r from-primary/10 via-primary/5 to-transparent border border-primary/20\">\n                                    <div className=\"flex flex-col md:flex-row justify-between items-start md:items-center gap-4\">\n                                        <div>\n                                            <h3 className=\"text-base font-medium flex items-center\">\n                                                <Badge variant=\"default\" className=\"mr-2\">Exclusive Offer</Badge>\n                                                <span>30% OFF Your First Purchase</span>\n                                            </h3>\n                                            <p className=\"text-sm text-muted-foreground mt-1\">\n                                                Use code CHANGERAWR at checkout for a complimentary discount\n                                            </p>\n                                        </div>\n\n                                        <div className=\"flex items-center space-x-2\">\n                                            <div className=\"flex items-center border rounded-md px-3 py-1.5 bg-background\">\n                                                <code className=\"font-mono font-semibold text-base\">CHANGERAWR</code>\n                                                <Button\n                                                    size=\"icon\"\n                                                    variant=\"ghost\"\n                                                    className=\"ml-2 h-6 w-6\"\n                                                    onClick={handleCopyPromoCode}\n                                                >\n                                                    {promoCodeCopied ? (\n                                                        <CheckCircle className=\"h-4 w-4 text-green-500\" />\n                                                    ) : (\n                                                        <Copy className=\"h-4 w-4\" />\n                                                    )}\n                                                </Button>\n                                            </div>\n\n                                            <Button\n                                                variant=\"default\"\n                                                size=\"sm\"\n                                                className=\"flex items-center gap-1\"\n                                                onClick={() => window.open('https://platform.secton.org/settings/organization/billing', '_blank')}\n                                            >\n                                                <span>Get Credits</span>\n                                                <ExternalLink className=\"h-3.5 w-3.5\" />\n                                            </Button>\n                                        </div>\n                                    </div>\n                                </div>\n                            </CardContent>\n                        </div>\n                    </Card>\n\n                    <form onSubmit={handleSubmit}>\n                        <Card>\n                            <CardHeader>\n                                <CardTitle>Configuration Settings</CardTitle>\n                                <CardDescription>\n                                    Manage your AI integration settings\n                                </CardDescription>\n                            </CardHeader>\n\n                            <CardContent className=\"space-y-6\">\n                                {/* Enable AI Assistant Toggle */}\n                                <div className=\"flex items-center justify-between space-x-2 p-4 rounded-lg bg-muted/50\">\n                                    <div className=\"space-y-0.5\">\n                                        <Label htmlFor=\"enableAIAssistant\" className=\"text-base\">Enable AI Assistant</Label>\n                                        <p className=\"text-sm text-muted-foreground\">\n                                            Allow users to use AI features in Changerawr\n                                        </p>\n                                    </div>\n                                    <Switch\n                                        id=\"enableAIAssistant\"\n                                        checked={formData.enableAIAssistant}\n                                        onCheckedChange={(checked) => handleChange('enableAIAssistant', checked)}\n                                    />\n                                </div>\n\n                                <Separator />\n\n                                {/* Default Model Selection */}\n                                <div className=\"space-y-3\">\n                                    <Label htmlFor=\"aiDefaultModel\" className=\"text-base\">Default AI Model</Label>\n                                    <Select\n                                        value={formData.aiDefaultModel || 'copilot-zero'}\n                                        onValueChange={(value) => handleChange('aiDefaultModel', value)}\n                                    >\n                                        <SelectTrigger id=\"aiDefaultModel\" className=\"w-full\">\n                                            <SelectValue placeholder=\"Select default model\" />\n                                        </SelectTrigger>\n                                        <SelectContent>\n                                            <SelectItem value=\"copilot-zero\">Copilot Zero (Fast & Efficient)</SelectItem>\n                                            {/*<SelectItem value=\"copilot-xl\">Copilot XL (Advanced Capabilities)</SelectItem>*/}\n                                        </SelectContent>\n                                    </Select>\n                                    <p className=\"text-sm text-muted-foreground\">\n                                        Choose the default AI model for all users. More capable models typically consume more credits.\n                                    </p>\n                                </div>\n\n                                <Separator />\n\n                                {/* API Key Input */}\n                                <div className=\"space-y-3\">\n                                    <div className=\"flex items-center space-x-2\">\n                                        <Label htmlFor=\"aiApiKey\" className=\"text-base\">Secton API Key</Label>\n                                        <TooltipProvider>\n                                            <Tooltip>\n                                                <TooltipTrigger asChild>\n                                                    <Info className=\"h-4 w-4 text-muted-foreground\" />\n                                                </TooltipTrigger>\n                                                <TooltipContent className=\"max-w-80\">\n                                                    <p>This key will be used as the system-wide key for all AI Assistant features.</p>\n                                                </TooltipContent>\n                                            </Tooltip>\n                                        </TooltipProvider>\n                                    </div>\n\n                                    <div className=\"grid grid-cols-1 md:grid-cols-[1fr,auto] gap-3\">\n                                        <div className=\"relative\">\n                                            <Input\n                                                id=\"aiApiKey\"\n                                                type=\"password\"\n                                                placeholder={settings?.aiApiKey ? \"••••••••••••••••\" : \"Enter Secton API key (starts with sk_)\"}\n                                                value={formData.aiApiKey || ''}\n                                                onChange={(e) => handleChange('aiApiKey', e.target.value)}\n                                                autoComplete=\"off\"\n                                                className=\"pr-10\"\n                                            />\n                                            <div className=\"absolute inset-y-0 right-3 flex items-center\">\n                                                <Lock className=\"h-4 w-4 text-muted-foreground\" />\n                                            </div>\n                                        </div>\n\n                                        <Button\n                                            type=\"button\"\n                                            variant=\"outline\"\n                                            onClick={handleTestApiKey}\n                                            disabled={isTesting || (!formData.aiApiKey || formData.aiApiKey.includes('•'))}\n                                            className=\"w-full md:w-auto\"\n                                        >\n                                            {isTesting ? (\n                                                <>\n                                                    <Loader2 className=\"h-4 w-4 mr-2 animate-spin\" />\n                                                    <span>Validating...</span>\n                                                </>\n                                            ) : (\n                                                <>\n                                                    {keyValidated === true && (\n                                                        <CheckCircle className=\"h-4 w-4 mr-2 text-green-500\" />\n                                                    )}\n                                                    {keyValidated === false && (\n                                                        <XCircle className=\"h-4 w-4 mr-2 text-destructive\" />\n                                                    )}\n                                                    <span>Validate Key</span>\n                                                </>\n                                            )}\n                                        </Button>\n                                    </div>\n\n                                    <p className=\"text-sm text-muted-foreground\">\n                                        Enter your Secton API key starting with &ldquo;sk_&rdquo;. Don&apos;t have one?\n                                        <a\n                                            href=\"https://platform.secton.org/settings/organization/api-keys\"\n                                            target=\"_blank\"\n                                            rel=\"noreferrer\"\n                                            className=\"text-primary hover:underline ml-1\"\n                                        >\n                                            Get one here\n                                        </a>\n                                    </p>\n\n                                    <AnimatePresence>\n                                        {keyValidated === true && (\n                                            <motion.div\n                                                initial={{ opacity: 0, y: -10 }}\n                                                animate={{ opacity: 1, y: 0 }}\n                                                exit={{ opacity: 0, y: -10 }}\n                                            >\n                                                <Alert variant=\"success\" className=\"border-green-200 bg-green-50 dark:bg-green-900/20 dark:border-green-900\">\n                                                    <AlertTitle>Valid API Key</AlertTitle>\n                                                    <AlertDescription>\n                                                        Your API key has been verified and is working correctly.\n                                                    </AlertDescription>\n                                                </Alert>\n                                            </motion.div>\n                                        )}\n\n                                        {keyValidated === false && (\n                                            <motion.div\n                                                initial={{ opacity: 0, y: -10 }}\n                                                animate={{ opacity: 1, y: 0 }}\n                                                exit={{ opacity: 0, y: -10 }}\n                                            >\n                                                <Alert variant=\"destructive\">\n                                                    <AlertTitle>Invalid API Key</AlertTitle>\n                                                    <AlertDescription>\n                                                        The API key could not be validated. Please check that you&apos;ve entered it correctly.\n                                                    </AlertDescription>\n                                                </Alert>\n                                            </motion.div>\n                                        )}\n                                    </AnimatePresence>\n                                </div>\n\n                                {/* Info Box */}\n                                <Alert>\n                                    <AlertTitle>About AI Integration</AlertTitle>\n                                    <AlertDescription>\n                                        <p className=\"leading-relaxed\">\n                                            The AI Assistant adds powerful AI capabilities to Changerawr, such as AI functionality in the content editor.\n                                        </p>\n                                    </AlertDescription>\n                                </Alert>\n                            </CardContent>\n\n                            <CardFooter className=\"flex justify-between\">\n                                <Button\n                                    variant=\"outline\"\n                                    onClick={() => {\n                                        // Reset form to current settings\n                                        if (settings) {\n                                            setFormData({\n                                                enableAIAssistant: settings.enableAIAssistant,\n                                                aiApiKey: settings.aiApiKey ? '••••••••••••••••' : '',\n                                                aiDefaultModel: settings.aiDefaultModel || 'copilot-zero',\n                                            })\n                                            setApiKeyChanged(false)\n                                            setKeyValidated(null)\n                                        }\n                                    }}\n                                    type=\"button\"\n                                    disabled={isSaving}\n                                >\n                                    Cancel\n                                </Button>\n\n                                <Button type=\"submit\" disabled={isSaving} className=\"min-w-[120px]\">\n                                    {isSaving ? (\n                                        <span className=\"flex items-center gap-1\">\n                      <Loader2 className=\"h-4 w-4 animate-spin\" />\n                      <span>Saving...</span>\n                    </span>\n                                    ) : (\n                                        <span className=\"flex items-center gap-1\">\n                      <Save className=\"h-4 w-4\" />\n                      <span>Save Settings</span>\n                    </span>\n                                    )}\n                                </Button>\n                            </CardFooter>\n                        </Card>\n                    </form>\n                </div>\n            </motion.div>\n        </div>\n    )\n}"
  },
  {
    "path": "app/dashboard/admin/analytics/page.tsx",
    "content": "// app/dashboard/admin/analytics/page.tsx\n'use client';\n\nimport {useState} from 'react';\nimport {useQuery} from '@tanstack/react-query';\nimport {motion} from 'framer-motion';\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card';\nimport {\n    Select,\n    SelectContent,\n    SelectItem,\n    SelectTrigger,\n    SelectValue,\n} from '@/components/ui/select';\nimport {Button} from '@/components/ui/button';\nimport {Skeleton} from '@/components/ui/skeleton';\nimport {\n    BarChart4,\n    Eye,\n    Users,\n    Globe,\n    Building2,\n    Download,\n    RefreshCw,\n    TrendingUp,\n    Shield,\n    Activity\n} from 'lucide-react';\nimport {AnalyticsChart} from '@/components/analytics/analytics-chart';\nimport {AnalyticsMetricCard} from '@/components/analytics/analytics-metric-card';\nimport {CountryAnalyticsTable} from '@/components/analytics/country-analytics-table';\nimport {ReferrerAnalyticsTable} from '@/components/analytics/referrer-analytics-table';\nimport {ProjectAnalyticsTable} from '@/components/analytics/project-analytics-table';\nimport type {AnalyticsPeriod, SystemAnalyticsData} from '@/lib/types/analytics';\n\nconst fadeIn = {\n    initial: {opacity: 0, y: 20},\n    animate: {opacity: 1, y: 0},\n    transition: {duration: 0.5}\n};\n\nconst staggerChildren = {\n    animate: {\n        transition: {\n            staggerChildren: 0.1\n        }\n    }\n};\n\nexport default function AdminAnalyticsPage() {\n    const [selectedPeriod, setSelectedPeriod] = useState<AnalyticsPeriod>('30d');\n\n    const {\n        data: analyticsData,\n        isLoading,\n        error,\n        refetch,\n        isRefetching\n    } = useQuery<{ success: boolean; data: SystemAnalyticsData }>({\n        queryKey: ['admin-analytics', selectedPeriod],\n        queryFn: async () => {\n            const response = await fetch(`/api/admin/analytics?period=${selectedPeriod}`);\n            if (!response.ok) {\n                const errorData = await response.json();\n                throw new Error(errorData.error || 'Failed to fetch analytics data');\n            }\n            return response.json();\n        },\n        refetchOnWindowFocus: false,\n        staleTime: 5 * 60 * 1000, // 5 minutes\n    });\n\n    const handleExport = async () => {\n        try {\n            const response = await fetch(`/api/admin/analytics/export?period=${selectedPeriod}`);\n            if (response.ok) {\n                const blob = await response.blob();\n                const url = window.URL.createObjectURL(blob);\n                const a = document.createElement('a');\n                a.href = url;\n                a.download = `system-analytics-${selectedPeriod}.csv`;\n                document.body.appendChild(a);\n                a.click();\n                window.URL.revokeObjectURL(url);\n                document.body.removeChild(a);\n            }\n        } catch (error) {\n            console.error('Failed to export analytics:', error);\n        }\n    };\n\n    if (error) {\n        return (\n            <div className=\"container max-w-7xl space-y-6 p-4 md:p-8\">\n                <Card>\n                    <CardContent className=\"pt-6\">\n                        <div className=\"text-center space-y-4\">\n                            <div className=\"text-destructive\">\n                                {error.message.includes('not authorized') ? (\n                                    <>\n                                        <Shield className=\"h-12 w-12 mx-auto mb-4\"/>\n                                        <h3 className=\"text-lg font-semibold\">Access Denied</h3>\n                                        <p className=\"text-muted-foreground\">\n                                            You need admin privileges to view system analytics\n                                        </p>\n                                    </>\n                                ) : error.message.includes('disabled') ? (\n                                    <>\n                                        <Activity className=\"h-12 w-12 mx-auto mb-4\"/>\n                                        <h3 className=\"text-lg font-semibold\">Analytics Disabled</h3>\n                                        <p className=\"text-muted-foreground\">\n                                            Analytics are currently disabled in system settings\n                                        </p>\n                                    </>\n                                ) : (\n                                    <>\n                                        <BarChart4 className=\"h-12 w-12 mx-auto mb-4\"/>\n                                        <h3 className=\"text-lg font-semibold\">Failed to Load Analytics</h3>\n                                        <p className=\"text-muted-foreground\">\n                                            {error instanceof Error ? error.message : 'An unexpected error occurred'}\n                                        </p>\n                                    </>\n                                )}\n                            </div>\n                            <div className=\"flex gap-2 justify-center\">\n                                <Button onClick={() => refetch()} variant=\"outline\">\n                                    <RefreshCw className=\"h-4 w-4 mr-2\"/>\n                                    Try Again\n                                </Button>\n                            </div>\n                        </div>\n                    </CardContent>\n                </Card>\n            </div>\n        );\n    }\n\n    const data = analyticsData?.data;\n\n    return (\n        <div className=\"container max-w-7xl space-y-6 p-4 md:p-8\">\n            {/* Header */}\n            <motion.div\n                variants={fadeIn}\n                initial=\"initial\"\n                animate=\"animate\"\n                className=\"flex flex-col md:flex-row md:items-center md:justify-between gap-4\"\n            >\n                <div className=\"flex items-center gap-4\">\n                    <div>\n                        <h1 className=\"text-3xl font-bold tracking-tight flex items-center gap-3\">\n                            <BarChart4 className=\"h-8 w-8 text-primary\"/>\n                            System Analytics\n                        </h1>\n                        {data && (\n                            <p className=\"text-muted-foreground\">\n                                Platform-wide overview • {data.period.toUpperCase()}\n                            </p>\n                        )}\n                    </div>\n                </div>\n\n                <div className=\"flex items-center gap-3\">\n                    <Select value={selectedPeriod} onValueChange={(value: AnalyticsPeriod) => setSelectedPeriod(value)}>\n                        <SelectTrigger className=\"w-32\">\n                            <SelectValue/>\n                        </SelectTrigger>\n                        <SelectContent>\n                            <SelectItem value=\"7d\">Last 7 days</SelectItem>\n                            <SelectItem value=\"30d\">Last 30 days</SelectItem>\n                            <SelectItem value=\"90d\">Last 90 days</SelectItem>\n                            <SelectItem value=\"1y\">Last year</SelectItem>\n                        </SelectContent>\n                    </Select>\n\n                    <Button\n                        variant=\"outline\"\n                        size=\"sm\"\n                        onClick={handleExport}\n                        disabled={isLoading || !data}\n                    >\n                        <Download className=\"h-4 w-4 mr-2\"/>\n                        Export\n                    </Button>\n\n                    <Button\n                        variant=\"outline\"\n                        size=\"sm\"\n                        onClick={() => refetch()}\n                        disabled={isRefetching}\n                    >\n                        <RefreshCw className={`h-4 w-4 mr-2 ${isRefetching ? 'animate-spin' : ''}`}/>\n                        Refresh\n                    </Button>\n                </div>\n            </motion.div>\n\n            {isLoading ? (\n                <div className=\"space-y-6\">\n                    <div className=\"grid gap-6 md:grid-cols-2 lg:grid-cols-4\">\n                        {Array.from({length: 4}).map((_, i) => (\n                            <Card key={i}>\n                                <CardHeader>\n                                    <Skeleton className=\"h-4 w-24\"/>\n                                </CardHeader>\n                                <CardContent>\n                                    <Skeleton className=\"h-8 w-16 mb-2\"/>\n                                    <Skeleton className=\"h-3 w-20\"/>\n                                </CardContent>\n                            </Card>\n                        ))}\n                    </div>\n                    <Card>\n                        <CardHeader>\n                            <Skeleton className=\"h-6 w-32\"/>\n                        </CardHeader>\n                        <CardContent>\n                            <Skeleton className=\"h-80 w-full\"/>\n                        </CardContent>\n                    </Card>\n                </div>\n            ) : data ? (\n                <motion.div\n                    variants={staggerChildren}\n                    initial=\"initial\"\n                    animate=\"animate\"\n                    className=\"space-y-6\"\n                >\n                    {/* System Metrics Overview */}\n                    <motion.div variants={fadeIn} className=\"grid gap-6 md:grid-cols-2 lg:grid-cols-4\">\n                        <AnalyticsMetricCard\n                            title=\"Total Views\"\n                            value={data.totalViews}\n                            icon={Eye}\n                            description=\"System-wide page views\"\n                        />\n                        <AnalyticsMetricCard\n                            title=\"Unique Visitors\"\n                            value={data.uniqueVisitors}\n                            icon={Users}\n                            description=\"Across all projects\"\n                        />\n                        <AnalyticsMetricCard\n                            title=\"Active Projects\"\n                            value={data.topProjects.length}\n                            icon={Building2}\n                            description=\"Projects with views\"\n                        />\n                        <AnalyticsMetricCard\n                            title=\"Countries\"\n                            value={data.topCountries.length}\n                            icon={Globe}\n                            description=\"Global reach\"\n                        />\n                    </motion.div>\n\n                    {/* System Views Chart */}\n                    <motion.div variants={fadeIn}>\n                        <Card>\n                            <CardHeader>\n                                <CardTitle className=\"flex items-center gap-2\">\n                                    <TrendingUp className=\"h-5 w-5\"/>\n                                    System-wide Views Over Time\n                                </CardTitle>\n                                <CardDescription>\n                                    Combined views and visitors across all public projects\n                                </CardDescription>\n                            </CardHeader>\n                            <CardContent>\n                                <AnalyticsChart data={data.dailyViews}/>\n                            </CardContent>\n                        </Card>\n                    </motion.div>\n\n                    {/* Top Projects Table */}\n                    <motion.div variants={fadeIn}>\n                        <ProjectAnalyticsTable projects={data.topProjects}/>\n                    </motion.div>\n\n                    {/* Geographic and Referrer Data */}\n                    <div className=\"grid gap-6 lg:grid-cols-2\">\n                        <motion.div variants={fadeIn}>\n                            <CountryAnalyticsTable countries={data.topCountries}/>\n                        </motion.div>\n                        <motion.div variants={fadeIn}>\n                            <ReferrerAnalyticsTable referrers={data.topReferrers}/>\n                        </motion.div>\n                    </div>\n                </motion.div>\n            ) : null}\n        </div>\n    );\n}"
  },
  {
    "path": "app/dashboard/admin/api-keys/page.tsx",
    "content": "'use client'\n\nimport React, { useState } from 'react';\nimport { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';\nimport {\n    Card,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card';\nimport { Button } from '@/components/ui/button';\nimport {\n    AlertDialog,\n    AlertDialogAction,\n    AlertDialogCancel,\n    AlertDialogContent,\n    AlertDialogDescription,\n    AlertDialogFooter,\n    AlertDialogHeader,\n    AlertDialogTitle,\n    AlertDialogTrigger,\n} from '@/components/ui/alert-dialog';\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogFooter,\n    DialogHeader,\n    DialogTitle,\n    DialogTrigger,\n} from '@/components/ui/dialog';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport { Skeleton } from '@/components/ui/skeleton';\nimport { toast } from '@/hooks/use-toast';\nimport {\n    Key,\n    Plus,\n    Trash2,\n    Ban,\n    Pencil,\n    Copy,\n    FileText,\n    Shield,\n    X,\n    ExternalLink\n} from 'lucide-react';\nimport { format } from 'date-fns';\nimport { AnimatePresence, motion } from 'framer-motion';\nimport Link from 'next/link';\nimport SDKShowcaseCompact from '@/components/admin/api/sdk-showcase-compact';\nimport { Badge } from '@/components/ui/badge';\nimport { PermissionsModal } from '@/components/admin/api/PermissionsModal';\nimport { PERMISSION_GROUPS } from '@/lib/api/permissions';\n\ninterface ApiKey {\n    id: string;\n    name: string;\n    key: string;\n    lastUsed: string | null;\n    createdAt: string;\n    expiresAt: string | null;\n    isRevoked: boolean;\n    projectId: string | null;\n    project?: {\n        id: string;\n        name: string;\n    };\n    permissions: string[];\n}\n\ninterface RenameDialogProps {\n    open: boolean;\n    onOpenChange: (open: boolean) => void;\n    onRename: (newName: string) => Promise<void>;\n    currentName: string;\n}\n\nfunction RenameDialog({\n                          open,\n                          onOpenChange,\n                          onRename,\n                          currentName,\n                      }: RenameDialogProps) {\n    const [newName, setNewName] = useState(currentName);\n    const [isSubmitting, setIsSubmitting] = useState(false);\n\n    const handleSubmit = async (e: React.FormEvent) => {\n        e.preventDefault();\n        if (!newName.trim() || newName === currentName) return;\n\n        setIsSubmitting(true);\n        try {\n            await onRename(newName);\n            onOpenChange(false);\n        } catch (error) {\n            console.error('Failed to rename API key:', error);\n        } finally {\n            setIsSubmitting(false);\n        }\n    };\n\n    return (\n        <Dialog open={open} onOpenChange={onOpenChange}>\n            <DialogContent className=\"max-w-md\">\n                <DialogHeader>\n                    <DialogTitle>Rename API Key</DialogTitle>\n                    <DialogDescription>\n                        Enter a new name for your API key.\n                    </DialogDescription>\n                </DialogHeader>\n                <form onSubmit={handleSubmit}>\n                    <div className=\"grid gap-4 py-4\">\n                        <div className=\"grid gap-2\">\n                            <Label htmlFor=\"new-name\">New Name</Label>\n                            <Input\n                                id=\"new-name\"\n                                value={newName}\n                                onChange={(e) => setNewName(e.target.value)}\n                                placeholder=\"e.g., Production API Key\"\n                            />\n                        </div>\n                    </div>\n                    <DialogFooter>\n                        <Button\n                            type=\"button\"\n                            variant=\"outline\"\n                            onClick={() => onOpenChange(false)}\n                            disabled={isSubmitting}\n                        >\n                            Cancel\n                        </Button>\n                        <Button\n                            type=\"submit\"\n                            disabled={!newName.trim() || newName === currentName || isSubmitting}\n                        >\n                            {isSubmitting ? 'Renaming...' : 'Rename'}\n                        </Button>\n                    </DialogFooter>\n                </form>\n            </DialogContent>\n        </Dialog>\n    );\n}\n\nfunction NewKeyAlert({ keyData, onClose, onCopy }: { keyData: { key: string; id: string }; onClose: () => void; onCopy: (key: string) => void }) {\n    return (\n        <motion.div\n            initial={{ opacity: 0, y: -10 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0, y: -10 }}\n            className=\"bg-yellow-50 dark:bg-yellow-950/40 border border-yellow-200 dark:border-yellow-900/50 rounded-lg mb-6\"\n        >\n            <div className=\"px-4 py-4\">\n                <div className=\"flex items-center justify-between mb-2\">\n                    <div className=\"flex items-center gap-2\">\n                        <Shield className=\"h-5 w-5 text-yellow-600 dark:text-yellow-500\" />\n                        <h4 className=\"font-medium text-yellow-800 dark:text-yellow-500\">New API Key Created</h4>\n                    </div>\n                    <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        onClick={onClose}\n                        className=\"h-8 w-8 p-0 text-yellow-600 dark:text-yellow-500 hover:text-yellow-700 dark:hover:text-yellow-600 hover:bg-transparent\"\n                    >\n                        <X className=\"h-4 w-4\" />\n                    </Button>\n                </div>\n\n                <p className=\"text-sm text-yellow-700 dark:text-yellow-400 mb-3\">\n                    Save your API key now. For security reasons, you won&apos;t be able to view it again.\n                </p>\n\n                <div className=\"relative\">\n                    <div className=\"bg-yellow-100 dark:bg-yellow-950/60 border border-yellow-200 dark:border-yellow-900/30 rounded-md p-3 font-mono text-sm break-all text-yellow-800 dark:text-yellow-300\">\n                        {keyData.key}\n                    </div>\n                    <Button\n                        variant=\"outline\"\n                        size=\"sm\"\n                        onClick={() => onCopy(keyData.key)}\n                        className=\"absolute top-2 right-2 h-8 bg-yellow-100 dark:bg-yellow-950/70 border-yellow-200 dark:border-yellow-900/50 text-yellow-800 dark:text-yellow-300 hover:bg-yellow-200 dark:hover:bg-yellow-900 hover:text-yellow-900 dark:hover:text-yellow-200\"\n                    >\n                        <Copy className=\"h-3.5 w-3.5 mr-1\" />\n                        Copy\n                    </Button>\n                </div>\n            </div>\n        </motion.div>\n    );\n}\n\nexport default function ApiKeysPage() {\n    const queryClient = useQueryClient();\n    const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);\n    const [isPermissionsModalOpen, setIsPermissionsModalOpen] = useState(false);\n    const [newKeyName, setNewKeyName] = useState('');\n    const [newKeyPermissions, setNewKeyPermissions] = useState<string[]>(PERMISSION_GROUPS.FULL_ACCESS);\n    const [newKeyData, setNewKeyData] = useState<{ key: string; id: string } | null>(null);\n    const [renameKey, setRenameKey] = useState<ApiKey | null>(null);\n\n    const { data: apiKeys, isLoading } = useQuery<ApiKey[]>({\n        queryKey: ['api-keys'],\n        queryFn: async () => {\n            const response = await fetch('/api/admin/api-keys');\n            if (!response.ok) throw new Error('Failed to fetch API keys');\n            return response.json();\n        },\n        refetchInterval: 30000, // Refetch every 30 seconds\n        staleTime: 15000, // Consider data stale after 15 seconds\n    });\n\n    const createApiKey = useMutation({\n        mutationFn: async ({ name, permissions }: { name: string; permissions: string[] }) => {\n            const response = await fetch('/api/admin/api-keys', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({ name, permissions }),\n            });\n            if (!response.ok) throw new Error('Failed to create API key');\n            return response.json();\n        },\n        onSuccess: (data) => {\n            queryClient.setQueryData(['api-keys'], (old: ApiKey[] | undefined) => {\n                return old ? [...old, data] : [data];\n            });\n            setNewKeyData({ key: data.key, id: data.id });\n            toast({\n                title: 'API Key Created',\n                description: 'The new API key has been created successfully.',\n            });\n        },\n    });\n\n    const renameApiKey = useMutation({\n        mutationFn: async ({ id, name }: { id: string; name: string }) => {\n            const response = await fetch(`/api/admin/api-keys/${id}`, {\n                method: 'PATCH',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({ name }),\n            });\n            if (!response.ok) throw new Error('Failed to rename API key');\n            return response.json();\n        },\n        onMutate: async ({ id, name }) => {\n            await queryClient.cancelQueries({ queryKey: ['api-keys'] });\n            const previousKeys = queryClient.getQueryData(['api-keys']);\n\n            queryClient.setQueryData(['api-keys'], (old: ApiKey[] | undefined) => {\n                return old?.map(key =>\n                    key.id === id ? { ...key, name } : key\n                );\n            });\n\n            return { previousKeys };\n        },\n        onError: (err, variables, context) => {\n            if (context?.previousKeys) {\n                queryClient.setQueryData(['api-keys'], context.previousKeys);\n            }\n        },\n        onSettled: () => {\n            queryClient.invalidateQueries({ queryKey: ['api-keys'] });\n        },\n    });\n\n    const revokeApiKey = useMutation({\n        mutationFn: async (id: string) => {\n            const response = await fetch(`/api/admin/api-keys/${id}`, {\n                method: 'PATCH',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({ isRevoked: true }),\n            });\n            if (!response.ok) throw new Error('Failed to revoke API key');\n            return response.json();\n        },\n        onMutate: async (id) => {\n            await queryClient.cancelQueries({ queryKey: ['api-keys'] });\n            const previousKeys = queryClient.getQueryData(['api-keys']);\n\n            queryClient.setQueryData(['api-keys'], (old: ApiKey[] | undefined) => {\n                return old?.map(key =>\n                    key.id === id ? { ...key, isRevoked: true } : key\n                );\n            });\n\n            return { previousKeys };\n        },\n        onError: (err, id, context) => {\n            if (context?.previousKeys) {\n                queryClient.setQueryData(['api-keys'], context.previousKeys);\n            }\n        },\n        onSettled: () => {\n            queryClient.invalidateQueries({ queryKey: ['api-keys'] });\n        },\n    });\n\n    const deleteApiKey = useMutation({\n        mutationFn: async (id: string) => {\n            const response = await fetch(`/api/admin/api-keys/${id}`, {\n                method: 'DELETE',\n            });\n            if (!response.ok) throw new Error('Failed to delete API key');\n        },\n        onMutate: async (id) => {\n            await queryClient.cancelQueries({ queryKey: ['api-keys'] });\n            const previousKeys = queryClient.getQueryData(['api-keys']);\n\n            queryClient.setQueryData(['api-keys'], (old: ApiKey[] | undefined) => {\n                return old?.filter(key => key.id !== id);\n            });\n\n            return { previousKeys };\n        },\n        onError: (err, id, context) => {\n            if (context?.previousKeys) {\n                queryClient.setQueryData(['api-keys'], context.previousKeys);\n            }\n        },\n        onSettled: () => {\n            queryClient.invalidateQueries({ queryKey: ['api-keys'] });\n        },\n    });\n\n    const handleCreateKey = async (e: React.FormEvent) => {\n        e.preventDefault();\n        if (!newKeyName.trim()) return;\n\n        await createApiKey.mutate({ name: newKeyName, permissions: newKeyPermissions });\n        setNewKeyName('');\n        setNewKeyPermissions(PERMISSION_GROUPS.FULL_ACCESS);\n        setIsCreateDialogOpen(false);\n    };\n\n    const handleCopyKey = (key: string) => {\n        navigator.clipboard.writeText(key);\n        toast({\n            title: 'API Key Copied',\n            description: 'The API key has been copied to your clipboard.',\n        });\n    };\n\n    if (isLoading) {\n        return (\n            <div className=\"container max-w-screen-2xl mx-auto py-6 px-4 sm:px-6 lg:px-8\">\n                <div className=\"mb-6 flex justify-between items-center\">\n                    <Skeleton className=\"h-8 w-32\" />\n                    <Skeleton className=\"h-10 w-32\" />\n                </div>\n                <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n                    <div className=\"lg:col-span-2\">\n                        <Skeleton className=\"h-64 w-full rounded-lg\" />\n                    </div>\n                    <div className=\"lg:col-span-1\">\n                        <Skeleton className=\"h-64 w-full rounded-lg\" />\n                    </div>\n                </div>\n            </div>\n        );\n    }\n\n    return (\n        <div className=\"container max-w-screen-2xl mx-auto py-6 px-4 sm:px-6 lg:px-8\">\n            {/* Page header */}\n            <div className=\"flex flex-col md:flex-row md:items-center justify-between mb-6\">\n                <div>\n                    <h1 className=\"text-2xl font-bold\">API Keys</h1>\n                    <p className=\"text-muted-foreground mt-1\">\n                        Create and manage your API keys for authentication.\n                    </p>\n                </div>\n                <div className=\"flex items-center gap-3 mt-4 md:mt-0\">\n                    <Button variant=\"outline\" size=\"sm\" asChild className=\"h-9\">\n                        <Link href=\"/api-docs\" className=\"flex items-center\">\n                            <FileText className=\"h-4 w-4 mr-2\" />\n                            API Docs\n                            <ExternalLink className=\"ml-1 h-3 w-3\" />\n                        </Link>\n                    </Button>\n                    <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>\n                        <DialogTrigger asChild>\n                            <Button size=\"sm\" className=\"h-9\">\n                                <Plus className=\"h-4 w-4 mr-2\" />\n                                Create Key\n                            </Button>\n                        </DialogTrigger>\n                        <DialogContent className=\"max-w-md\">\n                            <DialogHeader>\n                                <DialogTitle>Create New API Key</DialogTitle>\n                                <DialogDescription>\n                                    Give your API key a name to help you identify its use.\n                                </DialogDescription>\n                            </DialogHeader>\n                            <form onSubmit={handleCreateKey}>\n                                <div className=\"grid gap-4 py-4\">\n                                    <div className=\"grid gap-2\">\n                                        <Label htmlFor=\"name\">API Key Name</Label>\n                                        <Input\n                                            id=\"name\"\n                                            value={newKeyName}\n                                            onChange={(e) => setNewKeyName(e.target.value)}\n                                            placeholder=\"e.g., Production API Key\"\n                                        />\n                                    </div>\n                                    <div className=\"grid gap-2\">\n                                        <Label>Permissions</Label>\n                                        <Button\n                                            type=\"button\"\n                                            variant=\"outline\"\n                                            onClick={() => setIsPermissionsModalOpen(true)}\n                                            className=\"justify-start\"\n                                        >\n                                            <Shield className=\"h-4 w-4 mr-2\" />\n                                            {newKeyPermissions.length} permission{newKeyPermissions.length !== 1 ? 's' : ''} selected\n                                        </Button>\n                                    </div>\n                                </div>\n                                <DialogFooter>\n                                    <Button type=\"submit\" disabled={!newKeyName.trim()}>\n                                        Create Key\n                                    </Button>\n                                </DialogFooter>\n                            </form>\n                        </DialogContent>\n                    </Dialog>\n                </div>\n            </div>\n\n            <AnimatePresence>\n                {newKeyData && (\n                    <NewKeyAlert\n                        keyData={newKeyData}\n                        onClose={() => setNewKeyData(null)}\n                        onCopy={handleCopyKey}\n                    />\n                )}\n            </AnimatePresence>\n\n            {/* Main Content - Table and SDK Card */}\n            <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n                {/* API Keys Card */}\n                <div className=\"lg:col-span-2\">\n                    <Card className=\"shadow-sm\">\n                        <CardHeader className=\"pb-3 border-b\">\n                            <CardTitle className=\"text-base font-medium\">Your API Keys</CardTitle>\n                        </CardHeader>\n\n                        {/* API Keys Table */}\n                        <div className=\"overflow-x-auto\">\n                            <table className=\"w-full\">\n                                <thead>\n                                <tr className=\"border-b\">\n                                    <th className=\"text-left text-xs font-medium text-muted-foreground uppercase tracking-wider py-3 px-6\">\n                                        Name\n                                    </th>\n                                    <th className=\"text-left text-xs font-medium text-muted-foreground uppercase tracking-wider py-3 px-6\">\n                                        Permissions\n                                    </th>\n                                    <th className=\"text-left text-xs font-medium text-muted-foreground uppercase tracking-wider py-3 px-6\">\n                                        Created\n                                    </th>\n                                    <th className=\"text-left text-xs font-medium text-muted-foreground uppercase tracking-wider py-3 px-6\">\n                                        Connection\n                                    </th>\n                                    <th className=\"text-left text-xs font-medium text-muted-foreground uppercase tracking-wider py-3 px-6\">\n                                        Status\n                                    </th>\n                                    <th className=\"text-right text-xs font-medium text-muted-foreground uppercase tracking-wider py-3 px-6\">\n                                        Actions\n                                    </th>\n                                </tr>\n                                </thead>\n                                <tbody className=\"divide-y\">\n                                {apiKeys && apiKeys.length > 0 ? (\n                                    apiKeys.map((key) => (\n                                        <tr\n                                            key={key.id}\n                                            className=\"hover:bg-muted/50 transition-colors\"\n                                        >\n                                            <td className=\"py-4 px-6 text-sm font-medium\">\n                                                {key.name}\n                                            </td>\n                                            <td className=\"py-4 px-6 text-sm\">\n                                                <div className=\"flex items-center gap-1\">\n                                                    <Badge variant=\"secondary\" className=\"text-xs\">\n                                                        {key.permissions.length} permission{key.permissions.length !== 1 ? 's' : ''}\n                                                    </Badge>\n                                                </div>\n                                            </td>\n                                            <td className=\"py-4 px-6 text-sm text-muted-foreground\">\n                                                {format(new Date(key.createdAt), 'PPP')}\n                                            </td>\n                                            <td className=\"py-4 px-6 text-sm\">\n                                                {key.lastUsed ? (\n                                                    <Badge variant=\"default\" className=\"bg-green-500/10 text-green-700 dark:text-green-400 border-green-500/20\">\n                                                        Connected\n                                                    </Badge>\n                                                ) : (\n                                                    <Badge variant=\"outline\" className=\"text-muted-foreground\">\n                                                        Waiting for first request\n                                                    </Badge>\n                                                )}\n                                            </td>\n                                            <td className=\"py-4 px-6 text-sm\">\n                                                {key.isRevoked ? (\n                                                    <Badge variant=\"destructive\">Revoked</Badge>\n                                                ) : (\n                                                    <Badge variant=\"default\">Active</Badge>\n                                                )}\n                                            </td>\n                                            <td className=\"py-2 px-6 text-sm text-right\">\n                                                <div className=\"flex items-center justify-end gap-2\">\n                                                    {!key.isRevoked && (\n                                                        <>\n                                                            <Button\n                                                                variant=\"ghost\"\n                                                                size=\"sm\"\n                                                                onClick={() => setRenameKey(key)}\n                                                                className=\"h-8 w-8 p-0\"\n                                                            >\n                                                                <span className=\"sr-only\">Rename</span>\n                                                                <Pencil className=\"h-4 w-4\" />\n                                                            </Button>\n                                                            <AlertDialog>\n                                                                <AlertDialogTrigger asChild>\n                                                                    <Button\n                                                                        variant=\"ghost\"\n                                                                        size=\"sm\"\n                                                                        className=\"h-8 w-8 p-0 text-destructive/80 hover:text-destructive hover:bg-destructive/10\"\n                                                                    >\n                                                                        <span className=\"sr-only\">Revoke</span>\n                                                                        <Ban className=\"h-4 w-4\" />\n                                                                    </Button>\n                                                                </AlertDialogTrigger>\n                                                                <AlertDialogContent>\n                                                                    <AlertDialogHeader>\n                                                                        <AlertDialogTitle>Revoke API Key</AlertDialogTitle>\n                                                                        <AlertDialogDescription>\n                                                                            Are you sure you want to\n                                                                            revoke &ldquo;{key.name}&rdquo;?\n                                                                            This will immediately prevent any further use of\n                                                                            this key.\n                                                                        </AlertDialogDescription>\n                                                                    </AlertDialogHeader>\n                                                                    <AlertDialogFooter>\n                                                                        <AlertDialogCancel>Cancel</AlertDialogCancel>\n                                                                        <AlertDialogAction\n                                                                            onClick={() => revokeApiKey.mutate(key.id)}\n                                                                            className=\"bg-destructive hover:bg-destructive/90 focus:ring-destructive\"\n                                                                        >\n                                                                            Revoke Key\n                                                                        </AlertDialogAction>\n                                                                    </AlertDialogFooter>\n                                                                </AlertDialogContent>\n                                                            </AlertDialog>\n                                                        </>\n                                                    )}\n                                                    {key.isRevoked && (\n                                                        <AlertDialog>\n                                                            <AlertDialogTrigger asChild>\n                                                                <Button\n                                                                    variant=\"ghost\"\n                                                                    size=\"sm\"\n                                                                    className=\"h-8 w-8 p-0 text-destructive/80 hover:text-destructive hover:bg-destructive/10\"\n                                                                >\n                                                                    <span className=\"sr-only\">Delete</span>\n                                                                    <Trash2 className=\"h-4 w-4\" />\n                                                                </Button>\n                                                            </AlertDialogTrigger>\n                                                            <AlertDialogContent>\n                                                                <AlertDialogHeader>\n                                                                    <AlertDialogTitle>Delete API Key</AlertDialogTitle>\n                                                                    <AlertDialogDescription>\n                                                                        Are you sure you want to permanently\n                                                                        delete &ldquo;{key.name}&rdquo;?\n                                                                        This action cannot be undone.\n                                                                    </AlertDialogDescription>\n                                                                </AlertDialogHeader>\n                                                                <AlertDialogFooter>\n                                                                    <AlertDialogCancel>Cancel</AlertDialogCancel>\n                                                                    <AlertDialogAction\n                                                                        onClick={() => deleteApiKey.mutate(key.id)}\n                                                                        className=\"bg-destructive hover:bg-destructive/90 focus:ring-destructive\"\n                                                                    >\n                                                                        Delete Key\n                                                                    </AlertDialogAction>\n                                                                </AlertDialogFooter>\n                                                            </AlertDialogContent>\n                                                        </AlertDialog>\n                                                    )}\n                                                </div>\n                                            </td>\n                                        </tr>\n                                    ))\n                                ) : (\n                                    <tr>\n                                        <td colSpan={6} className=\"py-16 text-center\">\n                                            <div className=\"flex flex-col items-center justify-center\">\n                                                <div className=\"rounded-full p-4 mb-4 bg-muted\">\n                                                    <Key className=\"h-8 w-8 text-muted-foreground/60\" />\n                                                </div>\n                                                <h3 className=\"text-lg font-medium mb-1\">No API Keys</h3>\n                                                <p className=\"text-sm text-muted-foreground mb-4 max-w-sm\">\n                                                    Create an API key to get started with the Changerawr API.\n                                                </p>\n                                                <Button\n                                                    onClick={() => setIsCreateDialogOpen(true)}\n                                                    size=\"sm\"\n                                                >\n                                                    <Plus className=\"h-4 w-4 mr-2\" />\n                                                    Create Key\n                                                </Button>\n                                            </div>\n                                        </td>\n                                    </tr>\n                                )}\n                                </tbody>\n                            </table>\n                        </div>\n                    </Card>\n                </div>\n\n                {/* SDKs Showcase - Right Side */}\n                <div className=\"lg:col-span-1\">\n                    <SDKShowcaseCompact />\n                </div>\n            </div>\n\n            <RenameDialog\n                open={!!renameKey}\n                onOpenChange={(open) => !open && setRenameKey(null)}\n                currentName={renameKey?.name ?? ''}\n                onRename={async (newName) => {\n                    if (!renameKey) return;\n                    await renameApiKey.mutateAsync({ id: renameKey.id, name: newName });\n                }}\n            />\n\n            <PermissionsModal\n                open={isPermissionsModalOpen}\n                onOpenChange={setIsPermissionsModalOpen}\n                selectedPermissions={newKeyPermissions}\n                onSave={setNewKeyPermissions}\n            />\n        </div>\n    );\n}"
  },
  {
    "path": "app/dashboard/admin/audit-logs/page.tsx",
    "content": "'use client'\n\nimport React, { useState, useEffect, useCallback } from 'react'\nimport { format, parseISO, subDays } from 'date-fns'\nimport { motion } from 'framer-motion'\nimport {\n    Card,\n    CardContent,\n    CardHeader,\n} from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Input } from '@/components/ui/input'\nimport {\n    Select,\n    SelectContent,\n    SelectItem,\n    SelectTrigger,\n    SelectValue,\n} from '@/components/ui/select'\nimport { Button } from '@/components/ui/button'\nimport {\n    Dialog,\n    DialogContent,\n    DialogHeader,\n    DialogTitle,\n    DialogDescription,\n    DialogFooter,\n} from '@/components/ui/dialog'\nimport {\n    Popover,\n    PopoverContent,\n    PopoverTrigger,\n} from '@/components/ui/popover'\nimport { Calendar } from '@/components/ui/calendar'\nimport {\n    RefreshCw,\n    XCircle,\n    Info,\n    Calendar as CalendarIcon,\n    Filter,\n    Download,\n    Loader2,\n    Search,\n    Copy,\n    Check,\n    Pause,\n    Play,\n    User,\n    UserX,\n} from 'lucide-react'\nimport { Progress } from '@/components/ui/progress'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { cn } from '@/lib/utils'\n\n// Type definitions\ninterface PreservedUserData {\n    id: string;\n    email: string;\n    name: string | null;\n    role: string;\n    deletedAt: string;\n    deletedBy: string;\n}\n\ninterface AuditLogDetails {\n    [key: string]: unknown;\n    _preservedUser?: PreservedUserData;\n    _preservedTargetUser?: PreservedUserData;\n}\n\ninterface UserInfo {\n    name?: string | null\n    email?: string | null\n    isDeleted?: boolean\n}\n\ninterface AuditLog {\n    id: string\n    action: string\n    userId: string | null\n    targetUserId?: string | null\n    details?: AuditLogDetails | string\n    createdAt: string\n    user?: UserInfo | null\n    targetUser?: UserInfo | null\n    performer?: UserInfo | null\n    target?: UserInfo | null\n    performer_email?: string | null\n    target_email?: string | null\n}\n\ninterface AuditLogsResponse {\n    logs: AuditLog[]\n    total: number\n    nextCursor?: string | null\n}\n\ninterface AuditAction {\n    action: string\n    count: number\n}\n\ninterface FilterState {\n    search: string\n    action: string\n    dateRange: {\n        from: Date | null\n        to: Date | null\n    }\n    userId?: string\n    targetId?: string\n}\n\n// User Display Component\ninterface UserDisplayProps {\n    user: UserInfo | null\n    showEmail?: boolean\n    className?: string\n    size?: 'sm' | 'md'\n}\n\nfunction UserDisplay({ user, showEmail = true, className, size = 'sm' }: UserDisplayProps) {\n    if (!user) {\n        return (\n            <div className={cn(\"flex items-center gap-2 text-muted-foreground\", className)}>\n                <User className=\"h-4 w-4 flex-shrink-0\" />\n                <span className=\"text-sm\">Unknown User</span>\n            </div>\n        );\n    }\n\n    const displayName = user.name || user.email || 'Unknown User';\n    const isDeleted = user.isDeleted;\n\n    return (\n        <div className={cn(\"flex items-center gap-2\", className)}>\n            {isDeleted ? (\n                <UserX className=\"h-4 w-4 text-red-500 flex-shrink-0\" />\n            ) : (\n                <User className=\"h-4 w-4 text-muted-foreground flex-shrink-0\" />\n            )}\n\n            <div className=\"flex flex-col min-w-0 flex-1\">\n                <div className=\"flex items-center gap-2\">\n                    <span className={cn(\n                        \"font-medium truncate\",\n                        size === 'sm' ? 'text-sm' : 'text-base',\n                        isDeleted && \"text-red-600 dark:text-red-400\"\n                    )}>\n                        {displayName}\n                    </span>\n\n                    {isDeleted && (\n                        <Badge variant=\"destructive\" className=\"text-xs flex-shrink-0\">\n                            Deleted\n                        </Badge>\n                    )}\n                </div>\n\n                {showEmail && user.email && user.name && (\n                    <span className={cn(\n                        \"text-xs text-muted-foreground truncate\",\n                        isDeleted && \"text-red-500 dark:text-red-400\"\n                    )}>\n                        {user.email}\n                    </span>\n                )}\n            </div>\n        </div>\n    );\n}\n\n// Utility functions\nconst formatLogDetails = (details?: string | AuditLogDetails): string => {\n    if (!details) return 'No additional details'\n\n    try {\n        const parsedDetails = typeof details === 'string' ? JSON.parse(details) : details\n        return JSON.stringify(parsedDetails, null, 2)\n    } catch {\n        return typeof details === 'string' ? details : 'Unable to parse log details'\n    }\n}\n\n// Get user info with fallback to preserved data\nconst getUserInfo = (log: AuditLog, isTarget = false): UserInfo | null => {\n    // Try new API format first\n    if (isTarget && log.target) {\n        return log.target;\n    }\n    if (!isTarget && log.performer) {\n        return log.performer;\n    }\n\n    // Fallback to old format\n    const user = isTarget ? log.targetUser : log.user;\n    return user || null;\n}\n\n// Helper function to safely access preserved user data\nconst getPreservedUserData = (details: AuditLogDetails | string | undefined, key: '_preservedUser' | '_preservedTargetUser'): PreservedUserData | null => {\n    if (!details || typeof details === 'string') {\n        return null;\n    }\n\n    const preservedData = details[key];\n    if (!preservedData || typeof preservedData !== 'object') {\n        return null;\n    }\n\n    return preservedData as PreservedUserData;\n}\n\n// Custom debounce hook\nfunction useDebounce<T>(value: T, delay: number = 500): T {\n    const [debouncedValue, setDebouncedValue] = useState<T>(value)\n\n    useEffect(() => {\n        const handler = setTimeout(() => {\n            setDebouncedValue(value)\n        }, delay)\n\n        return () => {\n            clearTimeout(handler)\n        }\n    }, [value, delay])\n\n    return debouncedValue\n}\n\nexport default function AuditLogsPage() {\n    // State for all logs\n    const [allLogs, setAllLogs] = useState<AuditLog[]>([])\n    const [isLoading, setIsLoading] = useState(false)\n    const [isLoadingMore, setIsLoadingMore] = useState(false)\n    const [isError, setIsError] = useState(false)\n    const [nextCursor, setNextCursor] = useState<string | null>(null)\n    const [hasMore, setHasMore] = useState(true)\n    const [total, setTotal] = useState(0)\n    const [loadProgress, setLoadProgress] = useState(0)\n    const [tableHeight, setTableHeight] = useState(450)\n    const [isAutoLoading, setIsAutoLoading] = useState(true)\n    const [isPaused, setIsPaused] = useState(false)\n\n    // Available actions from API\n    const [availableActions, setAvailableActions] = useState<AuditAction[]>([])\n    const [isLoadingActions, setIsLoadingActions] = useState(false)\n\n    // UI state\n    const [searchInput, setSearchInput] = useState('')\n    const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null)\n    const [isFilterOpen, setIsFilterOpen] = useState(false)\n    const [copied, setCopied] = useState(false)\n    const [activeFilters, setActiveFilters] = useState(0)\n\n    // Filters state\n    const [filters, setFilters] = useState<FilterState>({\n        search: '',\n        action: '',\n        dateRange: {\n            from: subDays(new Date(), 31),\n            to: new Date()\n        }\n    })\n\n    // Constants\n    const chunkSize = 100\n\n    // Debounced search\n    const debouncedSearch = useDebounce(searchInput)\n\n    // Fetch available actions for filter dropdown\n    const fetchAvailableActions = useCallback(async () => {\n        try {\n            setIsLoadingActions(true)\n            const response = await fetch('/api/admin/audit-logs/actions')\n            if (!response.ok) throw new Error('Failed to fetch actions')\n\n            const data = await response.json()\n            setAvailableActions(data.actions || [])\n        } catch (error) {\n            console.error('Error fetching available actions:', error)\n        } finally {\n            setIsLoadingActions(false)\n        }\n    }, [])\n\n    // Load available actions on mount\n    useEffect(() => {\n        fetchAvailableActions()\n    }, [fetchAvailableActions])\n\n    // UseEffect to calculate available height for the table\n    useEffect(() => {\n        const calculateHeight = () => {\n            const windowHeight = window.innerHeight;\n            const otherElementsHeight = 220;\n            const availableHeight = Math.max(350, windowHeight - otherElementsHeight);\n            setTableHeight(availableHeight);\n        };\n\n        calculateHeight();\n        window.addEventListener('resize', calculateHeight);\n        return () => window.removeEventListener('resize', calculateHeight);\n    }, []);\n\n    // Update filters when debounced search changes\n    useEffect(() => {\n        setFilters(prev => ({ ...prev, search: debouncedSearch }));\n        if (allLogs.length > 0) {\n            resetData();\n        }\n    }, [debouncedSearch]);\n\n    // Count active filters\n    useEffect(() => {\n        let count = 0;\n        if (filters.action) count++;\n        if (filters.dateRange.from || filters.dateRange.to) count++;\n        if (filters.userId) count++;\n        if (filters.targetId) count++;\n        setActiveFilters(count);\n    }, [filters]);\n\n    // Auto-load all data when filters change\n    useEffect(() => {\n        if (isAutoLoading && hasMore && !isLoadingMore && !isPaused && allLogs.length > 0) {\n            const timeoutId = setTimeout(() => {\n                loadMoreLogs()\n            }, 100) // Small delay to prevent overwhelming the API\n\n            return () => clearTimeout(timeoutId)\n        }\n    }, [allLogs, hasMore, isLoadingMore, isAutoLoading, isPaused])\n\n    // Function to reset data\n    const resetData = useCallback(() => {\n        setAllLogs([]);\n        setNextCursor(null);\n        setHasMore(true);\n        setLoadProgress(0);\n        setIsError(false);\n        setIsPaused(false);\n        fetchInitialData();\n    }, [filters]); // Added filters dependency\n\n    // Function to fetch initial data\n    const fetchInitialData = useCallback(async () => {\n        try {\n            setIsLoading(true);\n            setIsError(false);\n\n            const params = new URLSearchParams({\n                useChunking: 'true',\n                chunkSize: chunkSize.toString(),\n                search: filters.search,\n                action: filters.action,\n                from: filters.dateRange.from?.toISOString() || '',\n                to: filters.dateRange.to?.toISOString() || '',\n                userId: filters.userId || '',\n                targetId: filters.targetId || ''\n            });\n\n            const response = await fetch(`/api/admin/audit-logs?${params}`);\n\n            if (!response.ok) throw new Error('Failed to fetch audit logs');\n\n            const data: AuditLogsResponse = await response.json();\n\n            setAllLogs(data.logs || []);\n            setNextCursor(data.nextCursor || null);\n            setHasMore(!!data.nextCursor);\n            setTotal(data.total || 0);\n\n            if (data.total > 0) {\n                setLoadProgress(Math.min(100, ((data.logs?.length || 0) / data.total) * 100));\n            } else {\n                setLoadProgress(0);\n            }\n        } catch (error) {\n            console.error('Error fetching audit logs:', error);\n            setIsError(true);\n        } finally {\n            setIsLoading(false);\n        }\n    }, [filters, chunkSize]);\n\n    // Function to load more data\n    const loadMoreLogs = useCallback(async () => {\n        if (isLoadingMore || !hasMore || !nextCursor || isPaused) return;\n\n        try {\n            setIsLoadingMore(true);\n\n            const params = new URLSearchParams({\n                useChunking: 'true',\n                cursor: nextCursor,\n                chunkSize: chunkSize.toString(),\n                search: filters.search,\n                action: filters.action,\n                from: filters.dateRange.from?.toISOString() || '',\n                to: filters.dateRange.to?.toISOString() || '',\n                userId: filters.userId || '',\n                targetId: filters.targetId || ''\n            });\n\n            const response = await fetch(`/api/admin/audit-logs?${params}`);\n\n            if (!response.ok) throw new Error('Failed to fetch more audit logs');\n\n            const data: AuditLogsResponse = await response.json();\n\n            // Merge new logs with existing logs (avoiding duplicates)\n            const existingIds = new Set(allLogs.map(log => log.id));\n            const uniqueNewLogs = (data.logs || []).filter(log => !existingIds.has(log.id));\n\n            if (uniqueNewLogs.length > 0) {\n                setAllLogs(prev => [...prev, ...uniqueNewLogs]);\n            }\n\n            setNextCursor(data.nextCursor || null);\n            setHasMore(!!data.nextCursor);\n\n            // Update progress percentage\n            if (data.total > 0) {\n                setLoadProgress(Math.min(100, ((allLogs.length + uniqueNewLogs.length) / data.total) * 100));\n            }\n        } catch (error) {\n            console.error('Error fetching more audit logs:', error);\n        } finally {\n            setIsLoadingMore(false);\n        }\n    }, [allLogs, nextCursor, hasMore, isLoadingMore, filters, chunkSize, isPaused]);\n\n    // Load initial data\n    useEffect(() => {\n        fetchInitialData();\n    }, [fetchInitialData]);\n\n    // Copy details to clipboard\n    const copyDetails = useCallback(() => {\n        if (!selectedLog) return;\n\n        const detailsText = formatLogDetails(selectedLog.details);\n        navigator.clipboard.writeText(detailsText).then(() => {\n            setCopied(true);\n            setTimeout(() => setCopied(false), 2000);\n        });\n    }, [selectedLog]);\n\n    // Export function\n    const exportLogs = async () => {\n        try {\n            const params = new URLSearchParams({\n                search: filters.search,\n                action: filters.action,\n                from: filters.dateRange.from?.toISOString() || '',\n                to: filters.dateRange.to?.toISOString() || '',\n                userId: filters.userId || '',\n                targetId: filters.targetId || '',\n                export: 'true'\n            });\n\n            const response = await fetch(`/api/admin/audit-logs?${params}`);\n            if (!response.ok) throw new Error('Failed to export logs');\n\n            const blob = await response.blob();\n            const url = window.URL.createObjectURL(blob);\n            const a = document.createElement('a');\n            a.href = url;\n            a.download = `audit-logs-${format(new Date(), 'yyyy-MM-dd')}.csv`;\n            document.body.appendChild(a);\n            a.click();\n            window.URL.revokeObjectURL(url);\n            document.body.removeChild(a);\n        } catch (error) {\n            console.error('Export failed:', error);\n        }\n    };\n\n    // Reset filters\n    const resetFilters = () => {\n        setSearchInput('');\n        setFilters({\n            search: '',\n            action: '',\n            dateRange: {\n                from: subDays(new Date(), 31),\n                to: new Date()\n            }\n        });\n        setIsFilterOpen(false);\n        resetData();\n    };\n\n    // Toggle auto-loading\n    const toggleAutoLoading = () => {\n        setIsAutoLoading(!isAutoLoading);\n        if (!isAutoLoading && hasMore) {\n            setIsPaused(false);\n        }\n    };\n\n    // Pause/resume loading\n    const togglePause = () => {\n        setIsPaused(!isPaused);\n    };\n\n    // Action badge variant\n    const getActionVariant = (action: string) => {\n        if (action.includes('CREATE') || action.includes('ADD')) return 'success';\n        if (action.includes('DELETE') || action.includes('REMOVE')) return 'destructive';\n        if (action.includes('UPDATE') || action.includes('EDIT')) return 'warning';\n        if (action.includes('LOGIN') || action.includes('LOGOUT') || action.includes('VIEW')) return 'default';\n        if (action.includes('REVOKE') || action.includes('REVOCATION')) return 'outline';\n        return 'secondary';\n    };\n\n    // Handle scroll to bottom for loading more\n    const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {\n        if (!hasMore || isLoadingMore || !isAutoLoading || isPaused) return;\n\n        const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;\n        if (scrollTop + clientHeight >= scrollHeight - 100) {\n            loadMoreLogs();\n        }\n    };\n\n    return (\n        <div className=\"container mx-auto py-6\">\n            <Card className=\"border shadow-sm\">\n                <CardHeader className=\"flex flex-row items-center justify-between px-6 py-4 space-y-0 border-b\">\n                    <div>\n                        <h2 className=\"text-xl font-semibold leading-none tracking-tight\">Audit Logs</h2>\n                        <p className=\"text-sm text-muted-foreground mt-1\">Track and monitor system activities</p>\n                    </div>\n                    <div className=\"flex items-center gap-2\">\n                        <Button\n                            variant=\"outline\"\n                            size=\"sm\"\n                            onClick={exportLogs}\n                            disabled={isLoading || allLogs.length === 0}\n                            className=\"flex items-center gap-1\"\n                        >\n                            <Download className=\"h-4 w-4\" />\n                            Export\n                        </Button>\n                        <Button\n                            variant=\"outline\"\n                            size=\"sm\"\n                            onClick={resetData}\n                            disabled={isLoading}\n                            className=\"flex items-center gap-1\"\n                        >\n                            <RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />\n                            Refresh\n                        </Button>\n                    </div>\n                </CardHeader>\n\n                <CardContent className=\"p-6\">\n                    {/* Progress indicator with auto-loading controls */}\n                    <div className=\"mb-4\">\n                        <div className=\"flex justify-between items-center text-sm mb-1.5\">\n                            <div className=\"flex items-center gap-1\">\n                                <span className=\"font-medium\">{allLogs.length}</span>\n                                <span className=\"text-muted-foreground\">of</span>\n                                <span className=\"font-medium\">{total}</span>\n                                <span className=\"text-muted-foreground\">logs</span>\n                                {(isLoading || isLoadingMore) && (\n                                    <span className=\"inline-flex items-center gap-1 ml-2 text-muted-foreground\">\n                                        <Loader2 className=\"h-3 w-3 animate-spin\" />\n                                        {isLoading ? 'Loading...' : 'Loading more...'}\n                                    </span>\n                                )}\n                            </div>\n                            <div className=\"flex items-center gap-2\">\n                                <div className=\"flex items-center gap-1\">\n                                    <Button\n                                        variant=\"ghost\"\n                                        size=\"sm\"\n                                        onClick={toggleAutoLoading}\n                                        className=\"h-7 px-2\"\n                                    >\n                                        {isAutoLoading ? 'Auto-load: ON' : 'Auto-load: OFF'}\n                                    </Button>\n                                    {hasMore && isAutoLoading && (\n                                        <Button\n                                            variant=\"ghost\"\n                                            size=\"sm\"\n                                            onClick={togglePause}\n                                            className=\"h-7 px-2\"\n                                        >\n                                            {isPaused ? <Play className=\"h-3 w-3\" /> : <Pause className=\"h-3 w-3\" />}\n                                        </Button>\n                                    )}\n                                </div>\n                                <span className=\"text-muted-foreground\">{Math.floor(loadProgress)}% loaded</span>\n                            </div>\n                        </div>\n                        <Progress value={loadProgress} className=\"h-2\" />\n                    </div>\n\n                    {/* Search and filters */}\n                    <div className=\"flex gap-2 mb-4\">\n                        <div className=\"relative flex-1\">\n                            <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground\" />\n                            <Input\n                                placeholder=\"Search logs, users, actions...\"\n                                value={searchInput}\n                                onChange={e => setSearchInput(e.target.value)}\n                                className=\"pl-9\"\n                            />\n                        </div>\n\n                        <Popover open={isFilterOpen} onOpenChange={setIsFilterOpen}>\n                            <PopoverTrigger asChild>\n                                <Button variant=\"outline\" className=\"flex items-center gap-2\">\n                                    <Filter className=\"h-4 w-4\" />\n                                    Filters\n                                    {activeFilters > 0 && (\n                                        <Badge variant=\"secondary\" className=\"ml-1\">\n                                            {activeFilters}\n                                        </Badge>\n                                    )}\n                                </Button>\n                            </PopoverTrigger>\n                            <PopoverContent className=\"w-80 p-4\" align=\"end\">\n                                <div className=\"space-y-4\">\n                                    <h3 className=\"font-medium\">Filter Logs</h3>\n                                    {/* Action filter */}\n                                    <div className=\"space-y-2\">\n                                        <label className=\"text-sm font-medium\">Action Type</label>\n                                        <Select\n                                            value={filters.action}\n                                            onValueChange={(value) => {\n                                                setFilters(prev => ({\n                                                    ...prev,\n                                                    action: value === 'all' ? '' : value\n                                                }));\n                                            }}\n                                            disabled={isLoadingActions}\n                                        >\n                                            <SelectTrigger>\n                                                <SelectValue placeholder={isLoadingActions ? \"Loading actions...\" : \"Select action\"} />\n                                            </SelectTrigger>\n                                            <SelectContent>\n                                                <SelectItem value=\"all\">All Actions</SelectItem>\n                                                {availableActions.map((actionItem) => (\n                                                    <SelectItem key={actionItem.action} value={actionItem.action}>\n                                                        {actionItem.action} ({actionItem.count})\n                                                    </SelectItem>\n                                                ))}\n                                            </SelectContent>\n                                        </Select>\n                                    </div>\n\n                                    {/* Date range filter */}\n                                    <div className=\"space-y-2\">\n                                        <label className=\"text-sm font-medium\">Date Range</label>\n                                        <div className=\"flex items-center gap-2 p-2 bg-muted/50 rounded-md text-sm\">\n                                            <CalendarIcon className=\"h-4 w-4 text-muted-foreground\" />\n                                            <span>\n                                                {filters.dateRange.from\n                                                    ? format(filters.dateRange.from, 'MMM d, yyyy')\n                                                    : 'Start date'\n                                                }\n                                                {' → '}\n                                                {filters.dateRange.to\n                                                    ? format(filters.dateRange.to, 'MMM d, yyyy')\n                                                    : 'End date'\n                                                }\n                                            </span>\n                                        </div>\n                                        <Calendar\n                                            mode=\"range\"\n                                            selected={{\n                                                from: filters.dateRange.from || undefined,\n                                                to: filters.dateRange.to || undefined\n                                            }}\n                                            onSelect={(range) => {\n                                                setFilters(prev => ({\n                                                    ...prev,\n                                                    dateRange: {\n                                                        from: range?.from || null,\n                                                        to: range?.to || null\n                                                    }\n                                                }));\n                                            }}\n                                            className=\"border rounded-md p-3\"\n                                        />\n                                    </div>\n\n                                    <div className=\"flex justify-between pt-2\">\n                                        <Button\n                                            variant=\"ghost\"\n                                            size=\"sm\"\n                                            onClick={resetFilters}\n                                        >\n                                            Reset\n                                        </Button>\n                                        <Button\n                                            size=\"sm\"\n                                            onClick={() => {\n                                                resetData();\n                                                setIsFilterOpen(false);\n                                            }}\n                                        >\n                                            Apply\n                                        </Button>\n                                    </div>\n                                </div>\n                            </PopoverContent>\n                        </Popover>\n\n                        {(filters.search || filters.action || filters.dateRange.from || filters.dateRange.to) && (\n                            <Button\n                                variant=\"ghost\"\n                                size=\"icon\"\n                                onClick={resetFilters}\n                                title=\"Clear all filters\"\n                            >\n                                <XCircle className=\"h-4 w-4\" />\n                            </Button>\n                        )}\n                    </div>\n\n                    {/* Simple Table */}\n                    <div className=\"border rounded-md\">\n                        {isError ? (\n                            <div className=\"flex items-center justify-center p-8\">\n                                <div className=\"text-center\">\n                                    <p className=\"text-sm text-destructive font-medium mb-2\">Failed to load audit logs</p>\n                                    <Button\n                                        onClick={resetData}\n                                        variant=\"outline\"\n                                        size=\"sm\"\n                                    >\n                                        Try Again\n                                    </Button>\n                                </div>\n                            </div>\n                        ) : allLogs.length === 0 && !isLoading ? (\n                            <div className=\"flex items-center justify-center p-8\">\n                                <div className=\"text-center\">\n                                    <p className=\"text-sm font-medium mb-1\">No audit logs found</p>\n                                    <p className=\"text-sm text-muted-foreground mb-4\">Try adjusting your search criteria</p>\n                                    {(filters.search || filters.action || filters.dateRange.from || filters.dateRange.to) && (\n                                        <Button\n                                            onClick={resetFilters}\n                                            variant=\"outline\"\n                                            size=\"sm\"\n                                        >\n                                            Reset Filters\n                                        </Button>\n                                    )}\n                                </div>\n                            </div>\n                        ) : (\n                            <div\n                                className=\"overflow-auto relative\"\n                                style={{ height: tableHeight }}\n                                onScroll={handleScroll}\n                            >\n                                <table className=\"w-full\">\n                                    <thead className=\"sticky top-0 bg-background z-10\">\n                                    <tr className=\"border-b\">\n                                        <th className=\"text-left p-3 font-medium text-muted-foreground text-sm\">Timestamp</th>\n                                        <th className=\"text-left p-3 font-medium text-muted-foreground text-sm\">Action</th>\n                                        <th className=\"text-left p-3 font-medium text-muted-foreground text-sm w-48\">Performer</th>\n                                        <th className=\"text-left p-3 font-medium text-muted-foreground text-sm w-48\">Target</th>\n                                        <th className=\"text-right p-3 font-medium text-muted-foreground text-sm w-20\">Details</th>\n                                    </tr>\n                                    </thead>\n                                    <tbody>\n                                    {allLogs.map((log, index) => {\n                                        const performer = getUserInfo(log, false);\n                                        const target = getUserInfo(log, true);\n\n                                        return (\n                                            <motion.tr\n                                                key={log.id}\n                                                className={`${index % 2 === 0 ? 'bg-background' : 'bg-muted/5'} hover:bg-muted/20 cursor-pointer`}\n                                                onClick={() => setSelectedLog(log)}\n                                                initial={{ opacity: 0 }}\n                                                animate={{ opacity: 1 }}\n                                                transition={{ duration: 0.2, delay: index * 0.01 }}\n                                            >\n                                                <td className=\"p-3 text-sm whitespace-nowrap\">\n                                                    {format(parseISO(log.createdAt), 'yyyy-MM-dd HH:mm:ss')}\n                                                </td>\n                                                <td className=\"p-3\">\n                                                    <Badge variant={getActionVariant(log.action)} className=\"font-medium\">\n                                                        {log.action}\n                                                    </Badge>\n                                                </td>\n                                                <td className=\"p-3 max-w-48\">\n                                                    <UserDisplay\n                                                        user={performer}\n                                                        showEmail={false}\n                                                        className=\"max-w-full\"\n                                                    />\n                                                </td>\n                                                <td className=\"p-3 max-w-48\">\n                                                    {target ? (\n                                                        <UserDisplay\n                                                            user={target}\n                                                            showEmail={false}\n                                                            className=\"max-w-full\"\n                                                        />\n                                                    ) : (\n                                                        <span className=\"text-muted-foreground text-sm\">—</span>\n                                                    )}\n                                                </td>\n                                                <td className=\"p-3 text-right\">\n                                                    <Button\n                                                        variant=\"ghost\"\n                                                        size=\"icon\"\n                                                        className=\"ml-auto h-8 w-8\"\n                                                        onClick={(e) => {\n                                                            e.stopPropagation();\n                                                            setSelectedLog(log);\n                                                        }}\n                                                    >\n                                                        <Info className=\"h-4 w-4\" />\n                                                    </Button>\n                                                </td>\n                                            </motion.tr>\n                                        );\n                                    })}\n                                    </tbody>\n                                </table>\n\n                                {isLoadingMore && (\n                                    <div className=\"text-center py-3 bg-background bg-opacity-75 backdrop-blur-sm\">\n                                        <div className=\"flex items-center justify-center gap-2 text-sm\">\n                                            <Loader2 className=\"h-4 w-4 animate-spin\" />\n                                            <span>Loading more logs...</span>\n                                        </div>\n                                    </div>\n                                )}\n\n                                {!hasMore && allLogs.length > 0 && (\n                                    <div className=\"text-center py-3 text-sm text-muted-foreground bg-background bg-opacity-75 backdrop-blur-sm\">\n                                        All logs loaded ({allLogs.length} records)\n                                    </div>\n                                )}\n                            </div>\n                        )}\n                    </div>\n                </CardContent>\n            </Card>\n\n            {/* Details Dialog */}\n            <Dialog\n                open={!!selectedLog}\n                onOpenChange={(open) => !open && setSelectedLog(null)}\n            >\n                <DialogContent className=\"max-w-lg\">\n                    <DialogHeader>\n                        <DialogTitle className=\"flex items-center\">\n                            Log Details\n                            {selectedLog && (\n                                <Badge\n                                    variant={getActionVariant(selectedLog.action)}\n                                    className=\"ml-2\"\n                                >\n                                    {selectedLog.action}\n                                </Badge>\n                            )}\n                        </DialogTitle>\n                        <DialogDescription>\n                            {selectedLog && format(parseISO(selectedLog.createdAt), 'PPpp')}\n                        </DialogDescription>\n                    </DialogHeader>\n\n                    {selectedLog && (\n                        <ScrollArea className=\"max-h-[60vh]\">\n                            <div className=\"space-y-4\">\n                                <div className=\"grid grid-cols-1 gap-4\">\n                                    {/* Performer */}\n                                    <div>\n                                        <h4 className=\"text-sm font-medium text-muted-foreground mb-2\">Performer</h4>\n                                        <div className=\"bg-muted p-3 rounded-md\">\n                                            <UserDisplay\n                                                user={getUserInfo(selectedLog, false)}\n                                                showEmail={true}\n                                                size=\"md\"\n                                            />\n                                        </div>\n                                    </div>\n\n                                    {/* Target (if exists) */}\n                                    {getUserInfo(selectedLog, true) && (\n                                        <div>\n                                            <h4 className=\"text-sm font-medium text-muted-foreground mb-2\">Target</h4>\n                                            <div className=\"bg-muted p-3 rounded-md\">\n                                                <UserDisplay\n                                                    user={getUserInfo(selectedLog, true)}\n                                                    showEmail={true}\n                                                    size=\"md\"\n                                                />\n                                            </div>\n                                        </div>\n                                    )}\n                                </div>\n\n                                {selectedLog.details && (\n                                    <div>\n                                        <div className=\"flex items-center justify-between mb-2\">\n                                            <h4 className=\"text-sm font-medium text-muted-foreground\">Details</h4>\n\n                                            <Button\n                                                variant=\"outline\"\n                                                size=\"sm\"\n                                                onClick={copyDetails}\n                                            >\n                                                {copied ? (\n                                                    <>\n                                                        <Check className=\"h-3.5 w-3.5 mr-1\" />\n                                                        Copied\n                                                    </>\n                                                ) : (\n                                                    <>\n                                                        <Copy className=\"h-3.5 w-3.5 mr-1\" />\n                                                        Copy\n                                                    </>\n                                                )}\n                                            </Button>\n                                        </div>\n\n                                        <div className=\"bg-muted/50 rounded-md overflow-hidden\">\n                                            <pre className=\"p-3 text-sm whitespace-pre-wrap font-mono overflow-auto max-h-64\">\n                                                {formatLogDetails(selectedLog.details)}\n                                            </pre>\n                                        </div>\n                                    </div>\n                                )}\n\n                                <div>\n                                    <h4 className=\"text-sm font-medium text-muted-foreground mb-2\">System Information</h4>\n                                    <div className=\"grid grid-cols-1 gap-3 text-sm\">\n                                        <div className=\"bg-muted/50 p-3 rounded-md\">\n                                            <p className=\"text-muted-foreground mb-1\">Log ID</p>\n                                            <p className=\"font-mono break-all\">{selectedLog.id}</p>\n                                        </div>\n\n                                        {selectedLog.userId && (\n                                            <div className=\"bg-muted/50 p-3 rounded-md\">\n                                                <p className=\"text-muted-foreground mb-1\">User ID</p>\n                                                <p className=\"font-mono break-all\">{selectedLog.userId}</p>\n                                            </div>\n                                        )}\n\n                                        {selectedLog.targetUserId && (\n                                            <div className=\"bg-muted/50 p-3 rounded-md\">\n                                                <p className=\"text-muted-foreground mb-1\">Target User ID</p>\n                                                <p className=\"font-mono break-all\">{selectedLog.targetUserId}</p>\n                                            </div>\n                                        )}\n\n                                        {/* Show preserved user info if available */}\n                                        {(() => {\n                                            const preservedUser = getPreservedUserData(selectedLog.details, '_preservedUser');\n                                            if (!preservedUser) return null;\n\n                                            return (\n                                                <div className=\"bg-red-50 dark:bg-red-950/20 p-3 rounded-md border border-red-200 dark:border-red-800\">\n                                                    <p className=\"text-red-600 dark:text-red-400 mb-1 text-xs font-medium\">\n                                                        Preserved User Data (Account Deleted)\n                                                    </p>\n                                                    <div className=\"text-xs space-y-1\">\n                                                        <p><span className=\"font-medium\">Email:</span> {preservedUser.email}</p>\n                                                        <p><span className=\"font-medium\">Name:</span> {preservedUser.name || 'N/A'}</p>\n                                                        <p><span className=\"font-medium\">Role:</span> {preservedUser.role}</p>\n                                                        <p><span className=\"font-medium\">Deleted:</span> {format(parseISO(preservedUser.deletedAt), 'PPpp')}</p>\n                                                    </div>\n                                                </div>\n                                            );\n                                        })()}\n\n                                        {(() => {\n                                            const preservedTargetUser = getPreservedUserData(selectedLog.details, '_preservedTargetUser');\n                                            if (!preservedTargetUser) return null;\n\n                                            return (\n                                                <div className=\"bg-red-50 dark:bg-red-950/20 p-3 rounded-md border border-red-200 dark:border-red-800\">\n                                                    <p className=\"text-red-600 dark:text-red-400 mb-1 text-xs font-medium\">\n                                                        Preserved Target User Data (Account Deleted)\n                                                    </p>\n                                                    <div className=\"text-xs space-y-1\">\n                                                        <p><span className=\"font-medium\">Email:</span> {preservedTargetUser.email}</p>\n                                                        <p><span className=\"font-medium\">Name:</span> {preservedTargetUser.name || 'N/A'}</p>\n                                                        <p><span className=\"font-medium\">Role:</span> {preservedTargetUser.role}</p>\n                                                        <p><span className=\"font-medium\">Deleted:</span> {format(parseISO(preservedTargetUser.deletedAt), 'PPpp')}</p>\n                                                    </div>\n                                                </div>\n                                            );\n                                        })()}\n                                    </div>\n                                </div>\n                            </div>\n                        </ScrollArea>\n                    )}\n\n                    <DialogFooter>\n                        <Button\n                            variant=\"secondary\"\n                            onClick={() => setSelectedLog(null)}\n                        >\n                            Close\n                        </Button>\n                    </DialogFooter>\n                </DialogContent>\n            </Dialog>\n        </div>\n    )\n}"
  },
  {
    "path": "app/dashboard/admin/domains/page.tsx",
    "content": "'use client'\n\nimport { useState, useEffect } from 'react'\nimport { useTimezone } from '@/hooks/use-timezone'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { Alert, AlertDescription } from '@/components/ui/alert'\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogHeader,\n    DialogTitle,\n    DialogTrigger\n} from '@/components/ui/dialog'\nimport {\n    DropdownMenu,\n    DropdownMenuContent,\n    DropdownMenuItem,\n    DropdownMenuSeparator,\n    DropdownMenuTrigger\n} from '@/components/ui/dropdown-menu'\nimport {\n    Table,\n    TableBody,\n    TableCell,\n    TableHead,\n    TableHeader,\n    TableRow\n} from '@/components/ui/table'\nimport {\n    Trash2,\n    RefreshCw,\n    Plus,\n    ExternalLink,\n    Globe,\n    CheckCircle,\n    Clock,\n    MoreVertical,\n    Copy,\n    Eye,\n    Shield,\n    Search,\n    Filter,\n    TrendingUp,\n    Server,\n    Zap\n} from 'lucide-react'\nimport { motion, AnimatePresence } from 'framer-motion'\nimport type { CustomDomain, DNSInstructions } from '@/lib/types/custom-domains'\n\ninterface DomainStats {\n    total: number\n    verified: number\n    pending: number\n    sslEnabled: number\n    expiringSoon: number\n}\n\nexport default function AdminDomainsPage() {\n    const timezone = useTimezone()\n    const [domains, setDomains] = useState<CustomDomain[]>([])\n    const [filteredDomains, setFilteredDomains] = useState<CustomDomain[]>([])\n    const [isLoading, setIsLoading] = useState(true)\n    const [isAddingDomain, setIsAddingDomain] = useState(false)\n    const [newDomain, setNewDomain] = useState('')\n    const [newProjectId, setNewProjectId] = useState('')\n    const [dnsInstructions, setDnsInstructions] = useState<DNSInstructions | null>(null)\n    const [verifyingDomain, setVerifyingDomain] = useState<string | null>(null)\n    const [error, setError] = useState<string | null>(null)\n    const [success, setSuccess] = useState<string | null>(null)\n    const [searchQuery, setSearchQuery] = useState('')\n    const [statusFilter, setStatusFilter] = useState<'all' | 'verified' | 'pending'>('all')\n    const [sslEnabled, setSslEnabled] = useState(false)\n\n    const stats: DomainStats = {\n        total: domains.length,\n        verified: domains.filter(d => d.verified).length,\n        pending: domains.filter(d => !d.verified).length,\n        sslEnabled: domains.filter(d => d.sslMode === 'LETS_ENCRYPT').length,\n        expiringSoon: domains.filter(d => {\n            const activeCert = d.certificates?.find(c => c.status === 'ISSUED')\n            if (!activeCert?.expiresAt) return false\n            const daysUntilExpiry = (new Date(activeCert.expiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24)\n            return daysUntilExpiry <= 30 && daysUntilExpiry > 0\n        }).length,\n    }\n\n    useEffect(() => {\n        loadDomains()\n        loadRuntimeConfig()\n    }, [])\n\n    const loadRuntimeConfig = async () => {\n        try {\n            const response = await fetch('/api/config/runtime')\n            const config = await response.json()\n            setSslEnabled(config.sslEnabled)\n        } catch (error) {\n            console.error('Failed to load runtime config:', error)\n            setSslEnabled(false)\n        }\n    }\n\n    useEffect(() => {\n        let filtered = domains\n\n        if (searchQuery) {\n            filtered = filtered.filter(domain =>\n                domain.domain.toLowerCase().includes(searchQuery.toLowerCase()) ||\n                domain.projectId.toLowerCase().includes(searchQuery.toLowerCase())\n            )\n        }\n\n        if (statusFilter !== 'all') {\n            filtered = filtered.filter(domain =>\n                statusFilter === 'verified' ? domain.verified : !domain.verified\n            )\n        }\n\n        setFilteredDomains(filtered)\n    }, [domains, searchQuery, statusFilter])\n\n    const loadDomains = async (): Promise<void> => {\n        try {\n            setIsLoading(true)\n            setError(null)\n            const response = await fetch('/api/custom-domains/list?scope=all')\n            const result = await response.json()\n\n            if (result.success) {\n                setDomains(result.domains || [])\n            } else {\n                setError(result.error || 'Failed to load domains')\n            }\n        } catch (error) {\n            setError('Failed to load domains')\n            console.error('Failed to load domains:', error)\n        } finally {\n            setIsLoading(false)\n        }\n    }\n\n    const handleAddDomain = async (e: React.FormEvent): Promise<void> => {\n        e.preventDefault()\n        if (!newDomain || !newProjectId) return\n\n        setIsAddingDomain(true)\n        setError(null)\n\n        try {\n            const response = await fetch('/api/custom-domains/add', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({\n                    domain: newDomain,\n                    projectId: newProjectId\n                })\n            })\n\n            const result = await response.json()\n            if (result.success) {\n                setNewDomain('')\n                setNewProjectId('')\n                setDnsInstructions(result.domain.dnsInstructions)\n                setSuccess(`Domain ${newDomain} added successfully!`)\n                await loadDomains()\n            } else {\n                setError(result.error || 'Failed to add domain')\n            }\n        } catch (error) {\n            setError('Failed to add domain')\n            console.error('Failed to add domain:', error)\n        } finally {\n            setIsAddingDomain(false)\n        }\n    }\n\n    const handleVerifyDomain = async (domain: string): Promise<void> => {\n        setVerifyingDomain(domain)\n        setError(null)\n\n        try {\n            const response = await fetch('/api/custom-domains/verify', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({ domain })\n            })\n\n            const result = await response.json()\n            if (result.success) {\n                await loadDomains()\n                if (result.verification.verified) {\n                    setSuccess(`Domain ${domain} verified successfully!`)\n                } else {\n                    setError(`Verification failed: ${result.verification.errors?.join(', ') || 'DNS records not found'}`)\n                }\n            } else {\n                setError(result.error || 'Verification failed')\n            }\n        } catch (error) {\n            setError('Failed to verify domain')\n            console.error('Failed to verify domain:', error)\n        } finally {\n            setVerifyingDomain(null)\n        }\n    }\n\n    const handleDeleteDomain = async (domain: string): Promise<void> => {\n        if (!confirm(`Are you sure you want to delete ${domain}? This action cannot be undone.`)) {\n            return\n        }\n\n        setError(null)\n\n        try {\n            const response = await fetch(`/api/custom-domains/${encodeURIComponent(domain)}?admin=true`, {\n                method: 'DELETE'\n            })\n\n            const result = await response.json()\n            if (result.success) {\n                setSuccess(`Domain ${domain} deleted successfully`)\n                await loadDomains()\n            } else {\n                setError(result.error || 'Failed to delete domain')\n            }\n        } catch (error) {\n            setError('Failed to delete domain')\n            console.error('Failed to delete domain:', error)\n        }\n    }\n\n    const copyToClipboard = async (text: string): Promise<void> => {\n        try {\n            await navigator.clipboard.writeText(text)\n            setSuccess('Copied to clipboard!')\n        } catch {\n            setError('Failed to copy to clipboard')\n        }\n    }\n\n    const formatDate = (date: Date | string): string => {\n        return new Date(date).toLocaleDateString('en-US', {\n            year: 'numeric',\n            month: 'short',\n            day: 'numeric',\n            hour: '2-digit',\n            minute: '2-digit',\n            timeZone: timezone,\n        })\n    }\n\n    const getStatusBadge = (domain: CustomDomain) => {\n        if (domain.verified) {\n            return (\n                <Badge variant=\"default\" className=\"text-green-700 bg-green-50 border-green-200 hover:bg-green-100\">\n                    <CheckCircle className=\"w-3 h-3 mr-1\" />\n                    Verified\n                </Badge>\n            )\n        }\n        return (\n            <Badge variant=\"secondary\" className=\"text-yellow-700 bg-yellow-50 border-yellow-200 hover:bg-yellow-100\">\n                <Clock className=\"w-3 h-3 mr-1\" />\n                Pending\n            </Badge>\n        )\n    }\n\n    const getSSLModeBadge = (sslMode: string) => {\n        if (sslMode === 'LETS_ENCRYPT') {\n            return (\n                <Badge variant=\"default\" className=\"text-purple-700 bg-purple-50 border-purple-200 hover:bg-purple-100\">\n                    <Shield className=\"w-3 h-3 mr-1\" />\n                    Let's Encrypt\n                </Badge>\n            )\n        }\n        if (sslMode === 'EXTERNAL') {\n            return (\n                <Badge variant=\"default\" className=\"text-blue-700 bg-blue-50 border-blue-200 hover:bg-blue-100\">\n                    <Shield className=\"w-3 h-3 mr-1\" />\n                    External\n                </Badge>\n            )\n        }\n        return (\n            <Badge variant=\"outline\" className=\"text-gray-500\">\n                None\n            </Badge>\n        )\n    }\n\n    const getCertificateStatusBadge = (domain: CustomDomain) => {\n        if (domain.sslMode === 'NONE') {\n            return <span className=\"text-xs text-muted-foreground\">—</span>\n        }\n\n        const activeCert = domain.certificates?.find(c => c.status === 'ISSUED')\n        const pendingCert = domain.certificates?.find(c =>\n            c.status === 'PENDING_HTTP01' || c.status === 'PENDING_DNS01'\n        )\n\n        if (activeCert) {\n            const expiresAt = new Date(activeCert.expiresAt!)\n            const daysUntilExpiry = (expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24)\n\n            if (daysUntilExpiry <= 30 && daysUntilExpiry > 0) {\n                return (\n                    <Badge variant=\"default\" className=\"text-orange-700 bg-orange-50 border-orange-200 hover:bg-orange-100\">\n                        Expiring Soon\n                    </Badge>\n                )\n            }\n\n            return (\n                <Badge variant=\"default\" className=\"text-green-700 bg-green-50 border-green-200 hover:bg-green-100\">\n                    Active\n                </Badge>\n            )\n        }\n\n        if (pendingCert) {\n            return (\n                <Badge variant=\"secondary\" className=\"text-blue-700 bg-blue-50 border-blue-200 hover:bg-blue-100\">\n                    Pending\n                </Badge>\n            )\n        }\n\n        const failedCert = domain.certificates?.find(c => c.status === 'FAILED')\n        if (failedCert) {\n            return (\n                <Badge variant=\"destructive\" className=\"text-xs\">\n                    Failed\n                </Badge>\n            )\n        }\n\n        return <span className=\"text-xs text-muted-foreground\">No Certificate</span>\n    }\n\n    useEffect(() => {\n        if (success || error) {\n            const timer = setTimeout(() => {\n                setSuccess(null)\n                setError(null)\n            }, 5000)\n            return () => clearTimeout(timer)\n        }\n    }, [success, error])\n\n    if (isLoading) {\n        return (\n            <div className=\"flex items-center justify-center min-h-[50vh]\">\n                <div className=\"text-center\">\n                    <RefreshCw className=\"w-8 h-8 animate-spin mx-auto mb-4 text-muted-foreground\" />\n                    <p className=\"text-muted-foreground\">Loading administration panel...</p>\n                </div>\n            </div>\n        )\n    }\n\n    return (\n        <div className=\"space-y-6\">\n            {/* Enhanced Header with Better Visual Hierarchy */}\n            <div className=\"flex items-center justify-between\">\n                <div className=\"space-y-1\">\n                    <div className=\"flex items-center gap-3\">\n                        <div className=\"flex items-center justify-center w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600\">\n                            <Globe className=\"w-4 h-4 text-white\" />\n                        </div>\n                        <div>\n                            <h1 className=\"text-2xl font-bold tracking-tight\">Domain Administration</h1>\n                            <p className=\"text-sm text-muted-foreground\">\n                                Manage custom domains across all projects\n                            </p>\n                        </div>\n                    </div>\n                </div>\n                <div className=\"flex items-center gap-2\">\n                    <Button variant=\"outline\" size=\"sm\" onClick={loadDomains} className=\"gap-2\">\n                        <RefreshCw className=\"w-4 h-4\" />\n                        Refresh\n                    </Button>\n                    <Dialog>\n                        <DialogTrigger asChild>\n                            <Button className=\"gap-2\">\n                                <Plus className=\"w-4 h-4\" />\n                                Add Domain\n                            </Button>\n                        </DialogTrigger>\n                        <DialogContent className=\"sm:max-w-md\">\n                            <DialogHeader>\n                                <DialogTitle>Add Custom Domain</DialogTitle>\n                                <DialogDescription>\n                                    Configure a new custom domain for a project\n                                </DialogDescription>\n                            </DialogHeader>\n                            <form onSubmit={handleAddDomain} className=\"space-y-4\">\n                                <div>\n                                    <Label htmlFor=\"domain\">Custom Domain</Label>\n                                    <Input\n                                        id=\"domain\"\n                                        type=\"text\"\n                                        value={newDomain}\n                                        onChange={(e) => setNewDomain(e.target.value)}\n                                        placeholder=\"changelog.example.com\"\n                                        required\n                                    />\n                                </div>\n                                <div>\n                                    <Label htmlFor=\"projectId\">Project ID</Label>\n                                    <Input\n                                        id=\"projectId\"\n                                        type=\"text\"\n                                        value={newProjectId}\n                                        onChange={(e) => setNewProjectId(e.target.value)}\n                                        placeholder=\"cm7zegrfx000ipp6g5ogohwuj\"\n                                        required\n                                    />\n                                </div>\n                                <Button type=\"submit\" disabled={isAddingDomain} className=\"w-full\">\n                                    {isAddingDomain ? 'Adding...' : 'Add Domain'}\n                                </Button>\n                            </form>\n                        </DialogContent>\n                    </Dialog>\n                </div>\n            </div>\n\n            {/* Improved Stats Cards - More compact and informative */}\n            <div className={`grid grid-cols-2 ${sslEnabled ? 'md:grid-cols-4' : 'md:grid-cols-3'} gap-4`}>\n                <Card>\n                    <CardContent className=\"p-4\">\n                        <div className=\"flex items-center justify-between\">\n                            <div>\n                                <p className=\"text-xs font-medium text-blue-700 uppercase tracking-wide\">Total Domains</p>\n                                <p className=\"text-2xl font-bold text-blue-900\">{stats.total}</p>\n                            </div>\n                            <div className=\"w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center\">\n                                <Globe className=\"w-5 h-5 text-blue-600\" />\n                            </div>\n                        </div>\n                    </CardContent>\n                </Card>\n\n                <Card>\n                    <CardContent className=\"p-4\">\n                        <div className=\"flex items-center justify-between\">\n                            <div>\n                                <p className=\"text-xs font-medium text-green-700 uppercase tracking-wide\">Verified</p>\n                                <p className=\"text-2xl font-bold text-green-900\">{stats.verified}</p>\n                            </div>\n                            <div className=\"w-10 h-10 bg-green-100 rounded-full flex items-center justify-center\">\n                                <CheckCircle className=\"w-5 h-5 text-green-600\" />\n                            </div>\n                        </div>\n                    </CardContent>\n                </Card>\n\n                <Card>\n                    <CardContent className=\"p-4\">\n                        <div className=\"flex items-center justify-between\">\n                            <div>\n                                <p className=\"text-xs font-medium text-yellow-700 uppercase tracking-wide\">Pending</p>\n                                <p className=\"text-2xl font-bold text-yellow-900\">{stats.pending}</p>\n                            </div>\n                            <div className=\"w-10 h-10 bg-yellow-100 rounded-full flex items-center justify-center\">\n                                <Clock className=\"w-5 h-5 text-yellow-600\" />\n                            </div>\n                        </div>\n                    </CardContent>\n                </Card>\n\n                {sslEnabled && (\n                    <Card>\n                        <CardContent className=\"p-4\">\n                            <div className=\"flex items-center justify-between\">\n                                <div>\n                                    <p className=\"text-xs font-medium text-purple-700 uppercase tracking-wide\">SSL Enabled</p>\n                                    <p className=\"text-2xl font-bold text-purple-900\">{stats.sslEnabled}</p>\n                                </div>\n                                <div className=\"w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center\">\n                                    <Shield className=\"w-5 h-5 text-purple-600\" />\n                                </div>\n                            </div>\n                        </CardContent>\n                    </Card>\n                )}\n            </div>\n\n            {/* Alerts with better positioning */}\n            <AnimatePresence>\n                {error && (\n                    <motion.div\n                        initial={{ opacity: 0, y: -10 }}\n                        animate={{ opacity: 1, y: 0 }}\n                        exit={{ opacity: 0, y: -10 }}\n                        className=\"relative\"\n                    >\n                        <Alert variant=\"destructive\">\n                            <AlertDescription className=\"text-red-800\">{error}</AlertDescription>\n                        </Alert>\n                    </motion.div>\n                )}\n                {success && (\n                    <motion.div\n                        initial={{ opacity: 0, y: -10 }}\n                        animate={{ opacity: 1, y: 0 }}\n                        exit={{ opacity: 0, y: -10 }}\n                        className=\"relative\"\n                    >\n                        <Alert variant=\"success\">\n                            <AlertDescription className=\"text-green-800\">{success}</AlertDescription>\n                        </Alert>\n                    </motion.div>\n                )}\n            </AnimatePresence>\n\n            {/* DNS Instructions - More compact design */}\n            {dnsInstructions && (\n                <motion.div\n                    initial={{ opacity: 0, scale: 0.98 }}\n                    animate={{ opacity: 1, scale: 1 }}\n                    className=\"relative\"\n                >\n                    <Card className=\"border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50\">\n                        <CardHeader className=\"pb-3\">\n                            <CardTitle className=\"text-blue-900 flex items-center gap-2 text-lg\">\n                                <Shield className=\"w-5 h-5\" />\n                                DNS Configuration Required\n                            </CardTitle>\n                        </CardHeader>\n                        <CardContent className=\"space-y-4\">\n                            <p className=\"text-blue-800 text-sm\">\n                                Add these DNS records to your domain provider to complete the setup:\n                            </p>\n\n                            <div className=\"grid gap-3\">\n                                <div className=\"bg-white/80 p-3 rounded-lg border border-blue-100\">\n                                    <div className=\"flex items-center justify-between mb-2\">\n                                        <h4 className=\"font-semibold text-blue-900 text-sm flex items-center gap-2\">\n                                            <span className=\"bg-blue-200 text-blue-800 px-2 py-0.5 rounded text-xs\">CNAME</span>\n                                            Record\n                                        </h4>\n                                        <Button\n                                            variant=\"ghost\"\n                                            size=\"sm\"\n                                            onClick={() => copyToClipboard(`${dnsInstructions.cname.name} CNAME ${dnsInstructions.cname.value}`)}\n                                            className=\"h-7 px-2\"\n                                        >\n                                            <Copy className=\"w-3 h-3\" />\n                                        </Button>\n                                    </div>\n                                    <div className=\"grid grid-cols-2 gap-2 text-xs font-mono text-gray-700\">\n                                        <div><strong>Name:</strong> {dnsInstructions.cname.name}</div>\n                                        <div><strong>Value:</strong> {dnsInstructions.cname.value}</div>\n                                    </div>\n                                </div>\n\n                                <div className=\"bg-white/80 p-3 rounded-lg border border-blue-100\">\n                                    <div className=\"flex items-center justify-between mb-2\">\n                                        <h4 className=\"font-semibold text-blue-900 text-sm flex items-center gap-2\">\n                                            <span className=\"bg-blue-200 text-blue-800 px-2 py-0.5 rounded text-xs\">TXT</span>\n                                            Verification\n                                        </h4>\n                                        <Button\n                                            variant=\"ghost\"\n                                            size=\"sm\"\n                                            onClick={() => copyToClipboard(`${dnsInstructions.txt.name} TXT ${dnsInstructions.txt.value}`)}\n                                            className=\"h-7 px-2\"\n                                        >\n                                            <Copy className=\"w-3 h-3\" />\n                                        </Button>\n                                    </div>\n                                    <div className=\"space-y-1 text-xs font-mono text-gray-700\">\n                                        <div><strong>Name:</strong> {dnsInstructions.txt.name}</div>\n                                        <div><strong>Value:</strong> <span className=\"break-all\">{dnsInstructions.txt.value}</span></div>\n                                    </div>\n                                </div>\n                            </div>\n\n                            <div className=\"flex items-center justify-between pt-2\">\n                                <Alert>\n                                    <AlertDescription className=\"text-xs text-blue-700\">\n                                        DNS changes can take up to 48 hours to propagate.\n                                    </AlertDescription>\n                                </Alert>\n                                <Button variant=\"outline\" size=\"sm\" onClick={() => setDnsInstructions(null)}>\n                                    Close\n                                </Button>\n                            </div>\n                        </CardContent>\n                    </Card>\n                </motion.div>\n            )}\n\n            {/* Enhanced Domains Table */}\n            <Card className=\"border-0 shadow-sm\">\n                <CardHeader className=\"pb-4\">\n                    <div className=\"flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4\">\n                        <div>\n                            <CardTitle className=\"text-lg font-semibold flex items-center gap-2\">\n                                <Server className=\"w-5 h-5 text-muted-foreground\" />\n                                Domain Management\n                                <Badge variant=\"secondary\" className=\"ml-2\">\n                                    {filteredDomains.length} of {domains.length}\n                                </Badge>\n                            </CardTitle>\n                        </div>\n                        <div className=\"flex items-center gap-2\">\n                            <div className=\"relative\">\n                                <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground\" />\n                                <Input\n                                    placeholder=\"Search domains or projects...\"\n                                    value={searchQuery}\n                                    onChange={(e) => setSearchQuery(e.target.value)}\n                                    className=\"pl-10 w-64 h-9\"\n                                />\n                            </div>\n                            <DropdownMenu>\n                                <DropdownMenuTrigger asChild>\n                                    <Button variant=\"outline\" size=\"sm\" className=\"gap-2\">\n                                        <Filter className=\"w-4 h-4\" />\n                                        {statusFilter === 'all' ? 'All' : statusFilter === 'verified' ? 'Verified' : 'Pending'}\n                                    </Button>\n                                </DropdownMenuTrigger>\n                                <DropdownMenuContent align=\"end\">\n                                    <DropdownMenuItem onClick={() => setStatusFilter('all')}>\n                                        All Status\n                                    </DropdownMenuItem>\n                                    <DropdownMenuItem onClick={() => setStatusFilter('verified')}>\n                                        Verified Only\n                                    </DropdownMenuItem>\n                                    <DropdownMenuItem onClick={() => setStatusFilter('pending')}>\n                                        Pending Only\n                                    </DropdownMenuItem>\n                                </DropdownMenuContent>\n                            </DropdownMenu>\n                        </div>\n                    </div>\n                </CardHeader>\n                <CardContent className=\"p-0\">\n                    {filteredDomains.length === 0 ? (\n                        <div className=\"text-center py-12\">\n                            <div className=\"w-16 h-16 bg-muted rounded-full flex items-center justify-center mx-auto mb-4\">\n                                <Globe className=\"w-8 h-8 text-muted-foreground\" />\n                            </div>\n                            <h3 className=\"text-lg font-semibold text-foreground mb-2\">\n                                {searchQuery || statusFilter !== 'all' ? 'No domains match your filters' : 'No custom domains yet'}\n                            </h3>\n                            <p className=\"text-muted-foreground text-sm\">\n                                {searchQuery || statusFilter !== 'all'\n                                    ? 'Try adjusting your search or filter criteria'\n                                    : 'Add a custom domain to get started'\n                                }\n                            </p>\n                        </div>\n                    ) : (\n                        <div className=\"overflow-hidden\">\n                            <Table>\n                                <TableHeader>\n                                    <TableRow className=\"hover:bg-transparent border-b\">\n                                        <TableHead className=\"font-semibold text-xs uppercase tracking-wider\">Domain</TableHead>\n                                        <TableHead className=\"font-semibold text-xs uppercase tracking-wider\">Status</TableHead>\n                                        {sslEnabled && (\n                                            <>\n                                                <TableHead className=\"font-semibold text-xs uppercase tracking-wider\">SSL Mode</TableHead>\n                                                <TableHead className=\"font-semibold text-xs uppercase tracking-wider\">Certificate</TableHead>\n                                            </>\n                                        )}\n                                        <TableHead className=\"font-semibold text-xs uppercase tracking-wider\">Project ID</TableHead>\n                                        <TableHead className=\"font-semibold text-xs uppercase tracking-wider\">Created</TableHead>\n                                        <TableHead className=\"font-semibold text-xs uppercase tracking-wider\">Verified</TableHead>\n                                        <TableHead className=\"font-semibold text-xs uppercase tracking-wider text-right\">Actions</TableHead>\n                                    </TableRow>\n                                </TableHeader>\n                                <TableBody>\n                                    {filteredDomains.map((domain) => (\n                                        <motion.tr\n                                            key={domain.id}\n                                            layout\n                                            className=\"group hover:bg-muted/30 transition-all duration-200 border-b border-border/50\"\n                                        >\n                                            <TableCell className=\"font-medium py-4\">\n                                                <div className=\"flex items-center space-x-3\">\n                                                    <div className=\"flex items-center gap-2\">\n                                                        <Zap className=\"w-4 h-4 text-muted-foreground\" />\n                                                        <span className=\"text-foreground font-medium\">{domain.domain}</span>\n                                                    </div>\n                                                    {domain.verified && (\n                                                        <a\n                                                            href={`https://${domain.domain}`}\n                                                            target=\"_blank\"\n                                                            rel=\"noopener noreferrer\"\n                                                            className=\"text-muted-foreground hover:text-primary transition-colors\"\n                                                        >\n                                                            <ExternalLink className=\"w-4 h-4\" />\n                                                        </a>\n                                                    )}\n                                                </div>\n                                            </TableCell>\n                                            <TableCell className=\"py-4\">\n                                                {getStatusBadge(domain)}\n                                            </TableCell>\n                                            {sslEnabled && (\n                                                <>\n                                                    <TableCell className=\"py-4\">\n                                                        {getSSLModeBadge(domain.sslMode)}\n                                                    </TableCell>\n                                                    <TableCell className=\"py-4\">\n                                                        {getCertificateStatusBadge(domain)}\n                                                    </TableCell>\n                                                </>\n                                            )}\n                                            <TableCell className=\"py-4\">\n                                                <code className=\"bg-muted/60 px-2 py-1 rounded text-xs text-foreground font-mono\">\n                                                    {domain.projectId}\n                                                </code>\n                                            </TableCell>\n                                            <TableCell className=\"text-muted-foreground text-sm py-4\">\n                                                {formatDate(domain.createdAt)}\n                                            </TableCell>\n                                            <TableCell className=\"text-muted-foreground text-sm py-4\">\n                                                {domain.verifiedAt ? formatDate(domain.verifiedAt) : '—'}\n                                            </TableCell>\n                                            <TableCell className=\"text-right py-4\">\n                                                <div className=\"flex items-center justify-end space-x-1\">\n                                                    {!domain.verified && (\n                                                        <Button\n                                                            variant=\"outline\"\n                                                            size=\"sm\"\n                                                            onClick={() => handleVerifyDomain(domain.domain)}\n                                                            disabled={verifyingDomain === domain.domain}\n                                                            className=\"h-8 text-xs\"\n                                                        >\n                                                            {verifyingDomain === domain.domain ? (\n                                                                <RefreshCw className=\"w-3 h-3 animate-spin\" />\n                                                            ) : (\n                                                                'Verify'\n                                                            )}\n                                                        </Button>\n                                                    )}\n                                                    <DropdownMenu>\n                                                        <DropdownMenuTrigger asChild>\n                                                            <Button variant=\"ghost\" size=\"sm\" className=\"h-8 w-8 p-0\">\n                                                                <MoreVertical className=\"w-4 h-4\" />\n                                                            </Button>\n                                                        </DropdownMenuTrigger>\n                                                        <DropdownMenuContent align=\"end\">\n                                                            <DropdownMenuItem\n                                                                onClick={() => copyToClipboard(domain.domain)}\n                                                                className=\"gap-2\"\n                                                            >\n                                                                <Copy className=\"w-4 h-4\" />\n                                                                Copy Domain\n                                                            </DropdownMenuItem>\n                                                            <DropdownMenuItem\n                                                                onClick={() => copyToClipboard(domain.projectId)}\n                                                                className=\"gap-2\"\n                                                            >\n                                                                <Eye className=\"w-4 h-4\" />\n                                                                Copy Project ID\n                                                            </DropdownMenuItem>\n                                                            <DropdownMenuSeparator />\n                                                            <DropdownMenuItem\n                                                                onClick={() => handleDeleteDomain(domain.domain)}\n                                                                className=\"text-destructive focus:text-destructive gap-2\"\n                                                            >\n                                                                <Trash2 className=\"w-4 h-4\" />\n                                                                Delete\n                                                            </DropdownMenuItem>\n                                                        </DropdownMenuContent>\n                                                    </DropdownMenu>\n                                                </div>\n                                            </TableCell>\n                                        </motion.tr>\n                                    ))}\n                                </TableBody>\n                            </Table>\n                        </div>\n                    )}\n                </CardContent>\n            </Card>\n        </div>\n    )\n}"
  },
  {
    "path": "app/dashboard/admin/layout.tsx",
    "content": "'use client'\n\nimport React from 'react';\nimport { useAuth } from '@/context/auth';\nimport {\n    Card,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card';\nimport {\n    Users,\n    Key,\n    Shield,\n    Settings,\n    FileText,\n    ClipboardCheck,\n    Fingerprint,\n    Info,\n    Sparkles,\n    ChartNoAxesCombined, Globe\n} from 'lucide-react';\nimport { usePathname } from 'next/navigation';\nimport { motion } from 'framer-motion';\nimport Link from 'next/link';\nimport { UpdateIndicatorBadge } from '@/components/UpdateIndicatorBadge';\n\nconst tabs = [\n    {\n        id: 'overview',\n        label: 'Overview',\n        icon: Shield,\n        path: '/dashboard/admin',\n        pattern: /^\\/dashboard\\/admin$/\n    },\n    {\n        id: 'users',\n        label: 'Users',\n        icon: Users,\n        path: '/dashboard/admin/users',\n        pattern: /^\\/dashboard\\/admin\\/users/\n    },\n    {\n        id: 'analytics',\n        label: 'Analytics',\n        icon: ChartNoAxesCombined,\n        path: \"/dashboard/admin/analytics\",\n        pattern: /^\\/dashboard\\/admin\\/analytics/\n    },\n    {\n        id: 'oauth',\n        label: 'SSO Providers',\n        icon: Fingerprint,\n        path: '/dashboard/admin/oauth',\n        pattern: /^\\/dashboard\\/admin\\/oauth/\n    },\n    {\n        id: 'domains',\n        label: 'Domains',\n        icon: Globe,\n        path: '/dashboard/admin/domains',\n        pattern: /^\\/dashboard\\/admin\\/domains/\n    },\n    {\n        id: 'api-keys',\n        label: 'API Keys',\n        icon: Key,\n        path: '/dashboard/admin/api-keys',\n        pattern: /^\\/dashboard\\/admin\\/api-keys/\n    },\n    {\n        id: 'audit-logs',\n        label: 'Audit Logs',\n        icon: FileText,\n        path: '/dashboard/admin/audit-logs',\n        pattern: /^\\/dashboard\\/admin\\/audit-logs/\n    },\n    {\n        id: 'ai-settings',\n        label: 'AI Integration',\n        icon: Sparkles,\n        path: '/dashboard/admin/ai-settings',\n        pattern: /^\\/dashboard\\/admin\\/ai-settings/\n    },\n    {\n        id: 'requests',\n        label: 'Requests',\n        icon: ClipboardCheck,\n        path: '/dashboard/admin/requests',\n        pattern: /^\\/dashboard\\/admin\\/requests/\n    },\n    {\n        id: 'settings',\n        label: 'Settings',\n        icon: Settings,\n        path: '/dashboard/admin/system',\n        pattern: /^\\/dashboard\\/admin\\/system/\n    },\n    {\n        id: 'about',\n        label: 'About',\n        icon: Info,\n        path: '/dashboard/admin/about',\n        pattern: /^\\/dashboard\\/admin\\/about/\n    }\n];\n\nexport default function AdminLayout({ children }: { children: React.ReactNode }) {\n    const { user } = useAuth();\n    const pathname = usePathname();\n\n    // Only admin can access this layout\n    if (user?.role !== 'ADMIN') {\n        return (\n            <div className=\"container mx-auto p-8\">\n                <Card>\n                    <CardHeader>\n                        <CardTitle>Access Denied</CardTitle>\n                        <CardDescription>\n                            You need administrator privileges to access this section.\n                        </CardDescription>\n                    </CardHeader>\n                </Card>\n            </div>\n        );\n    }\n\n    return (\n        <div className=\"min-h-screen bg-background\">\n            <div className=\"container max-w-screen-xl px-4 py-4 md:py-8\">\n                <div className=\"flex flex-col md:flex-row gap-6\">\n                    {/* Left sidebar */}\n                    <div className=\"w-full md:w-64 shrink-0\">\n                        <h1 className=\"text-2xl font-bold mb-4\">Admin</h1>\n                        <nav className=\"flex md:flex-col gap-1 overflow-x-auto md:overflow-x-visible pb-4 md:pb-0\">\n                            {tabs.map(({ id, label, icon: Icon, path }) => (\n                                <div key={id} className=\"flex items-center gap-2\">\n                                    <Link\n                                        href={path}\n                                        className={`\n                                            flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium flex-1\n                                            ${pathname && new RegExp(tabs.find(t => t.id === id)?.pattern || '').test(pathname)\n                                            ? 'bg-secondary'\n                                            : 'hover:bg-secondary/50'}\n                                            text-foreground\n                                            whitespace-nowrap\n                                        `}\n                                    >\n                                        <Icon className=\"h-4 w-4\" />\n                                        {label}\n                                    </Link>\n                                    {id === 'about' && <UpdateIndicatorBadge />}\n                                </div>\n                            ))}\n                        </nav>\n                    </div>\n\n                    {/* Main content */}\n                    <div className=\"flex-1\">\n                        <motion.div\n                            initial={{ opacity: 0, y: 20 }}\n                            animate={{ opacity: 1, y: 0 }}\n                        >\n                            {children}\n                        </motion.div>\n                    </div>\n                </div>\n            </div>\n        </div>\n    );\n}"
  },
  {
    "path": "app/dashboard/admin/oauth/page.tsx",
    "content": "'use client'\n\nimport React, {JSX, useState} from 'react';\nimport {useQuery, useMutation, useQueryClient} from '@tanstack/react-query';\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardFooter,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card';\nimport {\n    Form,\n    FormControl,\n    FormDescription,\n    FormField,\n    FormItem,\n    FormLabel,\n    FormMessage,\n} from '@/components/ui/form';\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogFooter,\n    DialogHeader,\n    DialogTitle,\n} from '@/components/ui/dialog';\nimport {Button} from '@/components/ui/button';\nimport {Input} from '@/components/ui/input';\nimport {Switch} from '@/components/ui/switch';\nimport {\n    Select,\n    SelectContent,\n    SelectItem,\n    SelectTrigger,\n    SelectValue,\n} from '@/components/ui/select';\nimport {\n    AlertDialog,\n    AlertDialogAction,\n    AlertDialogCancel,\n    AlertDialogContent,\n    AlertDialogDescription,\n    AlertDialogFooter,\n    AlertDialogHeader,\n    AlertDialogTitle\n} from '@/components/ui/alert-dialog';\nimport {toast} from '@/hooks/use-toast';\nimport {\n    Plus,\n    Pencil,\n    Trash2,\n    Check,\n    X,\n    Loader2,\n    KeyRound,\n    Fingerprint,\n    Copy,\n    Settings,\n    ExternalLink,\n    Globe,\n    Link2,\n    Info,\n    Shield,\n    User\n} from 'lucide-react';\nimport {motion, AnimatePresence} from 'framer-motion';\nimport {z} from 'zod';\nimport {useForm} from 'react-hook-form';\nimport {zodResolver} from '@hookform/resolvers/zod';\nimport {useAuth} from '@/context/auth';\nimport {Badge} from '@/components/ui/badge';\nimport {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs';\nimport {Separator} from '@/components/ui/separator';\nimport 'dotenv/config';\n\n// Enhanced form schema with custom URL options\nconst providerFormSchema = z.object({\n    name: z.string().min(1, 'Name is required'),\n    urlMode: z.enum(['preset', 'custom']).default('preset'),\n    preset: z.string().optional(),\n    baseUrl: z.string().optional(),\n    authorizationUrl: z.string().optional(),\n    tokenUrl: z.string().optional(),\n    userInfoUrl: z.string().optional(),\n    clientId: z.string().min(1, 'Client ID is required'),\n    clientSecret: z.string().min(1, 'Client Secret is required'),\n    scopes: z.string().refine(value => {\n        const scopes = value.split(',').map(s => s.trim()).filter(Boolean);\n        return scopes.length > 0;\n    }, {\n        message: 'At least one scope is required'\n    }),\n    enabled: z.boolean().default(true),\n    isDefault: z.boolean().default(false),\n    allowedEmailDomains: z.string().default(''),\n    blockExistingUsers: z.boolean().default(false),\n}).refine((data) => {\n    if (data.urlMode === 'preset' && !data.preset) {\n        return false;\n    }\n    if (data.urlMode === 'custom') {\n        return data.authorizationUrl && data.tokenUrl && data.userInfoUrl;\n    }\n    return true;\n}, {\n    message: 'Please select a preset or provide all custom URLs',\n    path: ['urlMode']\n});\n\ntype ProviderFormValues = z.infer<typeof providerFormSchema>;\n\n// Provider presets with accurate configurations\nconst PROVIDER_PRESETS = {\n    microsoft: {\n        name: 'Microsoft',\n        authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',\n        tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',\n        userInfoUrl: 'https://graph.microsoft.com/v1.0/me',\n        defaultScopes: 'openid,profile,email,User.Read'\n    },\n    google: {\n        name: 'Google',\n        authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',\n        tokenUrl: 'https://oauth2.googleapis.com/token',\n        userInfoUrl: 'https://www.googleapis.com/oauth2/v2/userinfo',\n        defaultScopes: 'openid,profile,email'\n    },\n    github: {\n        name: 'GitHub',\n        authorizationUrl: 'https://github.com/login/oauth/authorize',\n        tokenUrl: 'https://github.com/login/oauth/access_token',\n        userInfoUrl: 'https://api.github.com/user',\n        defaultScopes: 'user:email,read:user'\n    },\n    auth0: {\n        name: 'Auth0',\n        authorizationUrl: 'https://your-domain.auth0.com/authorize',\n        tokenUrl: 'https://your-domain.auth0.com/oauth/token',\n        userInfoUrl: 'https://your-domain.auth0.com/userinfo',\n        defaultScopes: 'openid,profile,email'\n    },\n    okta: {\n        name: 'Okta',\n        authorizationUrl: 'https://your-domain.okta.com/oauth2/default/v1/authorize',\n        tokenUrl: 'https://your-domain.okta.com/oauth2/default/v1/token',\n        userInfoUrl: 'https://your-domain.okta.com/oauth2/default/v1/userinfo',\n        defaultScopes: 'openid,profile,email'\n    },\n    easypanel: {\n        name: 'Easypanel',\n        authorizationUrl: 'https://your-easypanel-instance.com/oauth/authorize',\n        tokenUrl: 'https://your-easypanel-instance.com/oauth/token',\n        userInfoUrl: 'https://your-easypanel-instance.com/oauth/userinfo',\n        defaultScopes: 'openid,profile,email'\n    },\n    custom: {\n        name: 'Custom Provider',\n        authorizationUrl: '',\n        tokenUrl: '',\n        userInfoUrl: '',\n        defaultScopes: 'openid,profile,email'\n    }\n};\n\n// Add type for providers\ninterface OAuthProvider {\n    id: string;\n    name: string;\n    authorizationUrl: string;\n    tokenUrl: string;\n    userInfoUrl: string;\n    clientId: string;\n    clientSecret: string;\n    scopes: string[];\n    enabled: boolean;\n    isDefault: boolean;\n    allowedEmailDomains?: string[];\n    blockExistingUsers?: boolean;\n    requiredClaims?: Record<string, string>;\n}\n\n// Type for the API request body\ninterface ProviderApiData {\n    name: string;\n    clientId: string;\n    clientSecret: string;\n    scopes: string[];\n    enabled: boolean;\n    isDefault: boolean;\n    authorizationUrl: string;\n    tokenUrl: string;\n    userInfoUrl: string;\n    allowedEmailDomains?: string[];\n    blockExistingUsers?: boolean;\n    requiredClaims?: Record<string, string>;\n}\n\n// Provider logo component that handles placeholders\nconst ProviderLogo: React.FC<{ providerName: string }> = ({providerName}) => {\n    // Map known providers to predefined SVG placeholders\n    const knownProviders: Record<string, JSX.Element> = {\n        'easypanel': (\n            <div className=\"w-10 h-10 rounded-md bg-transparent flex items-center justify-center text-primary\">\n                <svg height=\"310\" width=\"310\" fill=\"none\" viewBox=\"0 0 310 310\" xmlns=\"http://www.w3.org/2000/svg\">\n                    <rect height=\"310\" width=\"310\" fill=\"url(#paint0_linear_3064_30643)\" rx=\"79.2222\"/>\n                    <g filter=\"url(#filter0_di_3064_30643)\">\n                        <path\n                            d=\"M171.445 131.475C168.064 127.549 163.14 125.291 157.958 125.291H96.9979L113.357 85.8796C116.115 79.2351 122.602 74.9043 129.796 74.9043L181.204 74.9043C186.354 74.9044 191.251 77.1347 194.632 81.0194L229.195 120.74C233.646 125.855 234.804 133.053 232.183 139.306L214.503 181.477L171.445 131.475ZM138.438 178.501C141.82 182.442 146.753 184.709 151.946 184.709H213.172L196.557 224.2C193.779 230.802 187.314 235.096 180.151 235.096H128.681C123.531 235.096 118.634 232.865 115.253 228.981L80.7119 189.285C76.2499 184.158 75.098 176.936 77.7432 170.675L95.5501 128.523L138.438 178.501Z\"\n                            fill=\"url(#paint1_linear_3064_30643)\" fillRule=\"evenodd\"/>\n                    </g>\n                    <defs>\n                        <filter height=\"192.191\" id=\"filter0_di_3064_30643\" width=\"189.228\" x=\"62.3398\" y=\"62.9043\"\n                                filterUnits=\"userSpaceOnUse\" colorInterpolationFilters=\"sRGB\">\n                            <feFlood result=\"BackgroundImageFix\" floodOpacity=\"0\"/>\n                            <feGaussianBlur stdDeviation=\"8\"/>\n                            <feGaussianBlur stdDeviation=\"2\"/>\n                            <feBlend result=\"effect1_dropShadow_3064_30643\" in2=\"BackgroundImageFix\"/>\n                            <feBlend result=\"shape\" in=\"SourceGraphic\" in2=\"effect1_dropShadow_3064_30643\"/>\n                            <feBlend result=\"effect2_innerShadow_3064_30643\" in2=\"shape\"/>\n                        </filter>\n                        <linearGradient id=\"paint0_linear_3064_30643\" gradientUnits=\"userSpaceOnUse\" x1=\"92.3325\"\n                                        x2=\"312.451\" y1=\"-71.1962\" y2=\"484.052\">\n                            <stop stopColor=\"#0BA864\"/>\n                            <stop offset=\"1\" stopColor=\"#19BFBF\"/>\n                        </linearGradient>\n                        <linearGradient id=\"paint1_linear_3064_30643\" gradientUnits=\"userSpaceOnUse\" x1=\"154.954\"\n                                        x2=\"154.954\" y1=\"74.9043\" y2=\"235.096\">\n                            <stop stopColor=\"white\"/>\n                            <stop offset=\"1\" stopColor=\"#D4E8D5\"/>\n                        </linearGradient>\n                    </defs>\n                </svg>\n            </div>\n        ),\n        'github': (\n            <div className=\"w-10 h-10 rounded-md bg-slate-900 flex items-center justify-center text-white\">\n                <svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\" stroke=\"currentColor\" strokeWidth=\"2\" fill=\"none\"\n                     strokeLinecap=\"round\" strokeLinejoin=\"round\">\n                    <path\n                        d=\"M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22\"/>\n                </svg>\n            </div>\n        ),\n        'google': (\n            <div className=\"w-10 h-10 rounded-md bg-white border flex items-center justify-center\">\n                <svg xmlns=\"http://www.w3.org/2000/svg\" x=\"0px\" y=\"0px\" width=\"100\" height=\"100\" viewBox=\"0 0 48 48\">\n                    <path fill=\"#fbc02d\"\n                          d=\"M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12\ts5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24s8.955,20,20,20\ts20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z\"></path>\n                    <path fill=\"#e53935\"\n                          d=\"M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039\tl5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z\"></path>\n                    <path fill=\"#4caf50\"\n                          d=\"M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36\tc-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z\"></path>\n                    <path fill=\"#1565c0\"\n                          d=\"M43.611,20.083L43.595,20L42,20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571\tc0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z\"></path>\n                </svg>\n            </div>\n        ),\n        'microsoft': (\n            <div className=\"w-10 h-10 rounded-md bg-white border flex items-center justify-center\">\n                <svg xmlns=\"http://www.w3.org/2000/svg\" x=\"0px\" y=\"0px\" width=\"100\" height=\"100\" viewBox=\"0 0 48 48\">\n                    <path fill=\"#ff5722\" d=\"M6 6H22V22H6z\" transform=\"rotate(-180 14 14)\"></path>\n                    <path fill=\"#4caf50\" d=\"M26 6H42V22H26z\" transform=\"rotate(-180 34 14)\"></path>\n                    <path fill=\"#ffc107\" d=\"M26 26H42V42H26z\" transform=\"rotate(-180 34 34)\"></path>\n                    <path fill=\"#03a9f4\" d=\"M6 26H22V42H6z\" transform=\"rotate(-180 14 34)\"></path>\n                </svg>\n            </div>\n        ),\n        'auth0': (\n            <div className=\"w-10 h-10 rounded-md bg-orange-50 flex items-center justify-center\">\n                <div className=\"w-6 h-6 rounded-full bg-orange-500\"></div>\n            </div>\n        ),\n        'okta': (\n            <div className=\"w-10 h-10 rounded-md bg-blue-50 flex items-center justify-center text-blue-600\">\n                <svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" stroke=\"currentColor\" strokeWidth=\"2\" fill=\"none\"\n                     strokeLinecap=\"round\" strokeLinejoin=\"round\">\n                    <circle cx=\"12\" cy=\"12\" r=\"10\"/>\n                    <circle cx=\"12\" cy=\"12\" r=\"4\"/>\n                </svg>\n            </div>\n        ),\n        'pocketid': (\n            <div\n                className=\"w-10 h-10 rounded-md bg-blue-50 flex items-center justify-center text-blue-600\">\n                <svg viewBox=\"0 0 24 24\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\">\n                    <g transform=\"scale(0.125)\">\n                        <path\n                            d=\"M0 0 C1.22240387 -0.00852997 2.44480774 -0.01705994 3.70425415 -0.02584839 C5.0560304 -0.02514938 6.40780655 -0.02422633 7.75958252 -0.02310181 C9.18560132 -0.02908737 10.61161687 -0.03589298 12.03762817 -0.04345703 C15.90972313 -0.06112467 19.78172968 -0.06581282 23.65386105 -0.0670886 C26.07480474 -0.06851069 28.495726 -0.07278145 30.91666412 -0.07808304 C39.36789882 -0.09658807 47.81907043 -0.10475334 56.27032471 -0.10317993 C64.13881545 -0.10197942 72.0070315 -0.12306266 79.87545276 -0.15466726 C86.63739901 -0.1808562 93.39925571 -0.19154608 100.16125226 -0.19026911 C104.19690178 -0.18976011 108.23230733 -0.19540854 112.26790619 -0.21662521 C116.06668745 -0.23605112 119.8649932 -0.23610277 123.66379166 -0.22159195 C125.05369133 -0.21950789 126.4436211 -0.22418075 127.83346939 -0.23631287 C139.69821631 -0.33312596 146.28346497 2.07529934 155.04962158 10.17758179 C157.75025927 13.02736712 159.33021801 15.7316911 161.03009033 19.26742554 C161.38680124 19.8713274 161.74351215 20.47522926 162.11103249 21.09743118 C163.3042125 23.91465743 163.28786842 26.17883227 163.29751587 29.23733521 C163.31031082 31.07094101 163.31031082 31.07094101 163.32336426 32.94158936 C163.32266525 34.29336561 163.3217422 35.64514176 163.32061768 36.99691772 C163.32660323 38.42293652 163.33340885 39.84895208 163.3409729 41.27496338 C163.35864054 45.14705833 163.36332869 49.01906489 163.36460447 52.89119625 C163.36602656 55.31213995 163.37029732 57.73306121 163.37559891 60.15399933 C163.39410394 68.60523402 163.40226921 77.05640563 163.4006958 85.50765991 C163.39949529 93.37615066 163.42057853 101.2443667 163.45218313 109.11278796 C163.47837207 115.87473421 163.48906195 122.63659091 163.48778498 129.39858747 C163.48727598 133.43423698 163.49292441 137.46964254 163.51414108 141.50524139 C163.53356699 145.30402265 163.53361864 149.10232841 163.51910782 152.90112686 C163.51702376 154.29102654 163.52169662 155.68095631 163.53382874 157.0708046 C163.63064183 168.93555152 161.22221653 175.52080017 153.11993408 184.28695679 C150.27014875 186.98759447 147.56582477 188.56755321 144.03009033 190.26742554 C143.42618847 190.62413645 142.82228661 190.98084736 142.20008469 191.34836769 C139.38285843 192.5415477 137.1186836 192.52520363 134.06018066 192.53485107 C132.22657486 192.54764603 132.22657486 192.54764603 130.35592651 192.56069946 C129.00415026 192.56000045 127.65237411 192.5590774 126.30059814 192.55795288 C124.87457935 192.56393844 123.44856379 192.57074405 122.02255249 192.57830811 C118.15045754 192.59597575 114.27845098 192.60066389 110.40631962 192.60193968 C107.98537592 192.60336177 105.56445466 192.60763253 103.14351654 192.61293411 C94.69228185 192.63143915 86.24111024 192.63960441 77.78985596 192.63803101 C69.92136521 192.63683049 62.05314917 192.65791373 54.18472791 192.68951833 C47.42278166 192.71570727 40.66092496 192.72639716 33.8989284 192.72512019 C29.86327888 192.72461118 25.82787333 192.73025962 21.79227448 192.75147629 C17.99349322 192.77090219 14.19518746 192.77095385 10.39638901 192.75644302 C9.00648933 192.75435896 7.61655956 192.75903183 6.22671127 192.77116394 C-5.63803565 192.86797704 -12.22328431 190.45955173 -20.98944092 182.35726929 C-23.69007861 179.50748396 -25.27003734 176.80315997 -26.96990967 173.26742554 C-27.32662058 172.66352367 -27.68333149 172.05962181 -28.05085182 171.43741989 C-29.24403183 168.62019364 -29.22768776 166.3560188 -29.23733521 163.29751587 C-29.24586517 162.075112 -29.25439514 160.85270813 -29.26318359 159.59326172 C-29.26248459 158.24148547 -29.26156153 156.88970931 -29.26043701 155.53793335 C-29.26642257 154.11191455 -29.27322818 152.68589899 -29.28079224 151.2598877 C-29.29845988 147.38779274 -29.30314802 143.51578619 -29.30442381 139.64365482 C-29.3058459 137.22271113 -29.31011666 134.80178987 -29.31541824 132.38085175 C-29.33392328 123.92961705 -29.34208854 115.47844544 -29.34051514 107.02719116 C-29.33931462 99.15870042 -29.36039786 91.29048437 -29.39200246 83.42206311 C-29.4181914 76.66011686 -29.42888129 69.89826016 -29.42760432 63.13626361 C-29.42709531 59.10061409 -29.43274375 55.06520853 -29.45396042 51.02960968 C-29.47338632 47.23082842 -29.47343798 43.43252267 -29.45892715 39.63372421 C-29.45684309 38.24382454 -29.46151596 36.85389477 -29.47364807 35.46404648 C-29.57046117 23.59929956 -27.16203586 17.0140509 -19.05975342 8.24789429 C-16.20996809 5.5472566 -13.5056441 3.96729786 -9.96990967 2.26742554 C-9.3660078 1.91071463 -8.76210594 1.55400372 -8.13990402 1.18648338 C-5.32267777 -0.00669663 -3.05850293 0.00964745 0 0 Z\"\n                            fill=\"#040707\" transform=\"translate(28.96990966796875,-0.267425537109375)\"/>\n                        <path\n                            d=\"M0 0 C69.15867003 0 69.15867003 0 86.60546875 16.3203125 C96.2607962 27.12545871 100.6588566 40.1036397 100.28125 54.515625 C99.09555807 69.2050306 92.6403778 80.94218717 82 91 C75.97607376 95.9592323 69.10089652 101 61 101 C59.15135737 93.85751712 57.41626671 86.68704743 55.75 79.5 C55.56276367 78.72285645 55.37552734 77.94571289 55.18261719 77.14501953 C53.6605808 70.47337821 53.6605808 70.47337821 55 67 C55.928125 66.443125 56.85625 65.88625 57.8125 65.3125 C62.54142834 61.88170885 64.35238285 57.58171936 65.3828125 51.8828125 C65.67786326 46.8276094 64.2577339 42.79148692 61 39 C57.08312453 35.42607676 53.31213672 33.67738317 48 33.375 C42.70291537 33.68467572 38.84385735 35.33327893 35 39 C31.51693504 43.70312162 30.25350858 48.13471025 31 54 C33.12803967 60.49612109 36.3592823 64.23952153 42 68 C41.67381545 74.14126771 41.07899677 80.06545141 39.6875 86.0625 C38.07994321 93.13890194 36.98907922 100.25885726 35.9375 107.4375 C35.77531982 108.54424072 35.61313965 109.65098145 35.44604492 110.79125977 C34.23281433 119.18661851 33.12120708 127.5909469 32 136 C21.44 136 10.88 136 0 136 C0 91.12 0 46.24 0 0 Z\"\n                            fill=\"#FBFBFB\" transform=\"translate(51,28)\"/>\n                    </g>\n                </svg>\n\n            </div>\n        )\n    };\n\n    // Normalize provider name for lookup\n    const normalizedName = providerName.toLowerCase();\n\n    // Return known provider logo or generate a default\n    return knownProviders[normalizedName] || (\n        <div className=\"w-10 h-10 rounded-md bg-secondary flex items-center justify-center text-secondary-foreground\">\n            <span className=\"text-lg font-semibold\">{providerName.substring(0, 2).toUpperCase()}</span>\n        </div>\n    );\n};\n\n// SAML provider interface\ninterface SAMLProvider {\n    id: string;\n    name: string;\n    entityId: string;\n    ssoUrl: string;\n    certificate: string;\n    spEntityId?: string | null;\n    nameIdFormat: string;\n    emailAttribute: string;\n    nameAttribute: string;\n    enabled: boolean;\n    isDefault: boolean;\n    allowedEmailDomains: string[];\n    blockExistingUsers: boolean;\n    requiredClaims?: Record<string, string>;\n}\n\nconst samlProviderSchema = z.object({\n    name: z.string().min(1, 'Name is required'),\n    entityId: z.string().min(1, 'IdP Entity ID is required'),\n    ssoUrl: z.string().url('SSO URL must be valid'),\n    certificate: z.string().min(1, 'Certificate is required'),\n    spEntityId: z.string().optional(),\n    nameIdFormat: z.string().default('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'),\n    emailAttribute: z.string().default('email'),\n    nameAttribute: z.string().default('name'),\n    enabled: z.boolean().default(true),\n    isDefault: z.boolean().default(false),\n    allowedEmailDomains: z.string().default(''),\n    blockExistingUsers: z.boolean().default(false),\n});\n\ntype SAMLProviderFormValues = z.infer<typeof samlProviderSchema>;\n\nexport default function OAuthProvidersPage() {\n    const {user} = useAuth();\n    const queryClient = useQueryClient();\n    const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);\n    const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);\n    const [selectedProvider, setSelectedProvider] = useState<OAuthProvider | null>(null);\n    const [providerToDelete, setProviderToDelete] = useState<OAuthProvider | null>(null);\n    const [activeTab, setActiveTab] = useState('active');\n    const [ssoType, setSsoType] = useState<'oauth' | 'saml'>('oauth');\n\n    // SAML provider state\n    const [isSAMLAddDialogOpen, setIsSAMLAddDialogOpen] = useState(false);\n    const [isSAMLEditDialogOpen, setIsSAMLEditDialogOpen] = useState(false);\n    const [selectedSAMLProvider, setSelectedSAMLProvider] = useState<SAMLProvider | null>(null);\n    const [samlProviderToDelete, setSAMLProviderToDelete] = useState<SAMLProvider | null>(null);\n\n    // Required claims state for OAuth and SAML\n    const [oauthCreateClaims, setOauthCreateClaims] = useState<Record<string, string>>({});\n    const [oauthEditClaims, setOauthEditClaims] = useState<Record<string, string>>({});\n    const [samlCreateClaims, setSamlCreateClaims] = useState<Record<string, string>>({});\n    const [samlEditClaims, setSamlEditClaims] = useState<Record<string, string>>({});\n\n    // Get all OAuth providers\n    const {data: providers, isLoading} = useQuery({\n        queryKey: ['oauth-providers'],\n        queryFn: async () => {\n            const response = await fetch('/api/admin/oauth/providers?includeAll=true');\n            if (!response.ok) throw new Error('Failed to fetch OAuth providers');\n            return response.json();\n        }\n    });\n\n    // Create provider form\n    const createForm = useForm<ProviderFormValues>({\n        resolver: zodResolver(providerFormSchema),\n        defaultValues: {\n            name: '',\n            urlMode: 'preset',\n            preset: '',\n            baseUrl: '',\n            authorizationUrl: '',\n            tokenUrl: '',\n            userInfoUrl: '',\n            clientId: '',\n            clientSecret: '',\n            scopes: 'openid,profile,email',\n            enabled: true,\n            isDefault: false\n        }\n    });\n\n    // Edit provider form\n    const editForm = useForm<ProviderFormValues>({\n        resolver: zodResolver(providerFormSchema),\n        defaultValues: {\n            name: '',\n            urlMode: 'custom',\n            authorizationUrl: '',\n            tokenUrl: '',\n            userInfoUrl: '',\n            clientId: '',\n            clientSecret: '',\n            scopes: '',\n            enabled: true,\n            isDefault: false\n        }\n    });\n\n    // Watch form values for dynamic updates\n    const watchCreateUrlMode = createForm.watch('urlMode');\n    const watchCreatePreset = createForm.watch('preset');\n    const watchEditUrlMode = editForm.watch('urlMode');\n    const watchEditPreset = editForm.watch('preset');\n\n    // Update URLs when preset changes (create form)\n    React.useEffect(() => {\n        if (watchCreateUrlMode === 'preset' && watchCreatePreset && PROVIDER_PRESETS[watchCreatePreset as keyof typeof PROVIDER_PRESETS]) {\n            const preset = PROVIDER_PRESETS[watchCreatePreset as keyof typeof PROVIDER_PRESETS];\n            createForm.setValue('name', preset.name);\n            createForm.setValue('scopes', preset.defaultScopes);\n        }\n    }, [watchCreateUrlMode, watchCreatePreset, createForm]);\n\n    // Update URLs when preset changes (edit form)\n    React.useEffect(() => {\n        if (watchEditUrlMode === 'preset' && watchEditPreset && PROVIDER_PRESETS[watchEditPreset as keyof typeof PROVIDER_PRESETS]) {\n            const preset = PROVIDER_PRESETS[watchEditPreset as keyof typeof PROVIDER_PRESETS];\n            editForm.setValue('name', preset.name);\n            editForm.setValue('authorizationUrl', preset.authorizationUrl);\n            editForm.setValue('tokenUrl', preset.tokenUrl);\n            editForm.setValue('userInfoUrl', preset.userInfoUrl);\n            editForm.setValue('scopes', preset.defaultScopes);\n        }\n    }, [watchEditUrlMode, watchEditPreset, editForm]);\n\n    // Add provider mutation\n    const addProvider = useMutation({\n        mutationFn: async (data: ProviderFormValues) => {\n            const finalData: ProviderApiData = {\n                name: data.name,\n                clientId: data.clientId,\n                clientSecret: data.clientSecret,\n                scopes: data.scopes.split(',').map(s => s.trim()).filter(Boolean),\n                enabled: data.enabled,\n                isDefault: data.isDefault,\n                authorizationUrl: '',\n                tokenUrl: '',\n                userInfoUrl: '',\n                allowedEmailDomains: data.allowedEmailDomains ? data.allowedEmailDomains.split(',').map(d => d.trim()).filter(Boolean) : [],\n                blockExistingUsers: data.blockExistingUsers,\n                requiredClaims: oauthCreateClaims,\n            };\n\n            if (data.urlMode === 'preset' && data.preset) {\n                const preset = PROVIDER_PRESETS[data.preset as keyof typeof PROVIDER_PRESETS];\n                finalData.authorizationUrl = preset.authorizationUrl;\n                finalData.tokenUrl = preset.tokenUrl;\n                finalData.userInfoUrl = preset.userInfoUrl;\n            } else if (data.urlMode === 'custom') {\n                finalData.authorizationUrl = data.authorizationUrl || '';\n                finalData.tokenUrl = data.tokenUrl || '';\n                finalData.userInfoUrl = data.userInfoUrl || '';\n            }\n\n            const response = await fetch('/api/admin/oauth/providers', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify(finalData)\n            });\n\n            if (!response.ok) {\n                const error = await response.json();\n                throw new Error(error.error || 'Failed to add provider');\n            }\n\n            return response.json();\n        },\n        onSuccess: () => {\n            queryClient.invalidateQueries({queryKey: ['oauth-providers']});\n            toast({\n                title: 'Provider Added',\n                description: 'The OAuth provider has been added successfully.'\n            });\n            setIsAddDialogOpen(false);\n            createForm.reset();\n            setOauthCreateClaims({});\n        },\n        onError: (error: Error) => {\n            toast({\n                title: 'Failed to Add Provider',\n                description: error.message,\n                variant: 'destructive'\n            });\n        }\n    });\n\n    // Edit provider mutation\n    const updateProvider = useMutation({\n        mutationFn: async (data: ProviderFormValues & { id: string }) => {\n            const updateData: ProviderApiData = {\n                name: data.name,\n                clientId: data.clientId,\n                clientSecret: data.clientSecret,\n                scopes: data.scopes.split(',').map(s => s.trim()).filter(Boolean),\n                enabled: data.enabled,\n                isDefault: data.isDefault,\n                authorizationUrl: '',\n                tokenUrl: '',\n                userInfoUrl: '',\n                allowedEmailDomains: data.allowedEmailDomains ? data.allowedEmailDomains.split(',').map(d => d.trim()).filter(Boolean) : [],\n                blockExistingUsers: data.blockExistingUsers,\n                requiredClaims: oauthEditClaims,\n            };\n\n            if (data.urlMode === 'preset' && data.preset) {\n                const preset = PROVIDER_PRESETS[data.preset as keyof typeof PROVIDER_PRESETS];\n                updateData.authorizationUrl = preset.authorizationUrl;\n                updateData.tokenUrl = preset.tokenUrl;\n                updateData.userInfoUrl = preset.userInfoUrl;\n            } else if (data.urlMode === 'custom') {\n                updateData.authorizationUrl = data.authorizationUrl || '';\n                updateData.tokenUrl = data.tokenUrl || '';\n                updateData.userInfoUrl = data.userInfoUrl || '';\n            }\n\n            const response = await fetch(`/api/admin/oauth/providers/${data.id}`, {\n                method: 'PATCH',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify(updateData)\n            });\n\n            if (!response.ok) {\n                const error = await response.json();\n                throw new Error(error.error || 'Failed to update provider');\n            }\n\n            return response.json();\n        },\n        onSuccess: () => {\n            queryClient.invalidateQueries({queryKey: ['oauth-providers']});\n            toast({\n                title: 'Provider Updated',\n                description: 'The OAuth provider has been updated successfully.'\n            });\n            setIsEditDialogOpen(false);\n            setSelectedProvider(null);\n        },\n        onError: (error: Error) => {\n            toast({\n                title: 'Failed to Update Provider',\n                description: error.message,\n                variant: 'destructive'\n            });\n        }\n    });\n\n    // Delete provider mutation\n    const deleteProvider = useMutation({\n        mutationFn: async (id: string) => {\n            const response = await fetch(`/api/admin/oauth/providers/${id}`, {\n                method: 'DELETE'\n            });\n\n            if (!response.ok) {\n                const error = await response.json();\n                throw new Error(error.error || 'Failed to delete provider');\n            }\n\n            return response.json();\n        },\n        onSuccess: () => {\n            queryClient.invalidateQueries({queryKey: ['oauth-providers']});\n            toast({\n                title: 'Provider Deleted',\n                description: 'The OAuth provider has been deleted successfully.'\n            });\n            setProviderToDelete(null);\n        },\n        onError: (error: Error) => {\n            toast({\n                title: 'Failed to Delete Provider',\n                description: error.message,\n                variant: 'destructive'\n            });\n        }\n    });\n\n    // Handle create form submission\n    const onCreateSubmit = (data: ProviderFormValues) => {\n        addProvider.mutate(data);\n    };\n\n    // Handle edit form submission\n    const onEditSubmit = (data: ProviderFormValues) => {\n        if (!selectedProvider) return;\n        updateProvider.mutate({...data, id: selectedProvider.id});\n    };\n\n    // Handle edit provider\n    const handleEditProvider = (provider: OAuthProvider) => {\n        setSelectedProvider(provider);\n\n        // Check if this matches a preset\n        let matchedPreset = '';\n        let urlMode: 'preset' | 'custom' = 'custom';\n\n        for (const [key, preset] of Object.entries(PROVIDER_PRESETS)) {\n            if (\n                provider.authorizationUrl === preset.authorizationUrl &&\n                provider.tokenUrl === preset.tokenUrl &&\n                provider.userInfoUrl === preset.userInfoUrl\n            ) {\n                matchedPreset = key;\n                urlMode = 'preset';\n                break;\n            }\n        }\n\n        editForm.reset({\n            name: provider.name,\n            urlMode,\n            preset: matchedPreset || '',\n            authorizationUrl: provider.authorizationUrl,\n            tokenUrl: provider.tokenUrl,\n            userInfoUrl: provider.userInfoUrl,\n            clientId: provider.clientId,\n            clientSecret: provider.clientSecret,\n            scopes: provider.scopes.join(','),\n            enabled: provider.enabled,\n            isDefault: provider.isDefault,\n            allowedEmailDomains: provider.allowedEmailDomains?.join(', ') || '',\n            blockExistingUsers: provider.blockExistingUsers || false\n        });\n\n        setOauthEditClaims(provider.requiredClaims || {});\n        setIsEditDialogOpen(true);\n    };\n\n    // Filter providers based on active tab\n    const filteredProviders = providers?.providers?.filter((provider: OAuthProvider) => {\n        if (activeTab === 'active') return provider.enabled;\n        if (activeTab === 'disabled') return !provider.enabled;\n        return true; // \"all\" tab\n    });\n\n    // ── SAML ──────────────────────────────────────────────────────────────────\n\n    const {data: samlData, isLoading: isSAMLLoading} = useQuery({\n        queryKey: ['saml-providers'],\n        queryFn: async () => {\n            const response = await fetch('/api/admin/saml/providers?includeAll=true');\n            if (!response.ok) throw new Error('Failed to fetch SAML providers');\n            return response.json();\n        }\n    });\n\n    const samlCreateForm = useForm<SAMLProviderFormValues>({\n        resolver: zodResolver(samlProviderSchema),\n        defaultValues: {\n            name: '',\n            entityId: '',\n            ssoUrl: '',\n            certificate: '',\n            spEntityId: '',\n            nameIdFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',\n            emailAttribute: 'email',\n            nameAttribute: 'name',\n            enabled: true,\n            isDefault: false,\n        }\n    });\n\n    const samlEditForm = useForm<SAMLProviderFormValues>({\n        resolver: zodResolver(samlProviderSchema),\n        defaultValues: {\n            name: '',\n            entityId: '',\n            ssoUrl: '',\n            certificate: '',\n            spEntityId: '',\n            nameIdFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',\n            emailAttribute: 'email',\n            nameAttribute: 'name',\n            enabled: true,\n            isDefault: false,\n        }\n    });\n\n    const addSAMLProvider = useMutation({\n        mutationFn: async (data: SAMLProviderFormValues) => {\n            const apiData = {\n                ...data,\n                allowedEmailDomains: data.allowedEmailDomains ? data.allowedEmailDomains.split(',').map(d => d.trim()).filter(Boolean) : [],\n                requiredClaims: samlCreateClaims,\n            };\n            const response = await fetch('/api/admin/saml/providers', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify(apiData)\n            });\n            if (!response.ok) {\n                const err = await response.json();\n                throw new Error(err.error || 'Failed to add SAML provider');\n            }\n            return response.json();\n        },\n        onSuccess: () => {\n            queryClient.invalidateQueries({queryKey: ['saml-providers']});\n            toast({title: 'SAML Provider Added', description: 'The SAML provider has been added successfully.'});\n            setIsSAMLAddDialogOpen(false);\n            samlCreateForm.reset();\n            setSamlCreateClaims({});\n        },\n        onError: (error: Error) => {\n            toast({title: 'Failed to Add SAML Provider', description: error.message, variant: 'destructive'});\n        }\n    });\n\n    const updateSAMLProvider = useMutation({\n        mutationFn: async (data: SAMLProviderFormValues & {id: string}) => {\n            const apiData = {\n                ...data,\n                allowedEmailDomains: data.allowedEmailDomains ? data.allowedEmailDomains.split(',').map(d => d.trim()).filter(Boolean) : [],\n                requiredClaims: samlEditClaims,\n            };\n            const response = await fetch(`/api/admin/saml/providers/${data.id}`, {\n                method: 'PATCH',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify(apiData)\n            });\n            if (!response.ok) {\n                const err = await response.json();\n                throw new Error(err.error || 'Failed to update SAML provider');\n            }\n            return response.json();\n        },\n        onSuccess: () => {\n            queryClient.invalidateQueries({queryKey: ['saml-providers']});\n            toast({title: 'SAML Provider Updated', description: 'The SAML provider has been updated successfully.'});\n            setIsSAMLEditDialogOpen(false);\n            setSelectedSAMLProvider(null);\n        },\n        onError: (error: Error) => {\n            toast({title: 'Failed to Update SAML Provider', description: error.message, variant: 'destructive'});\n        }\n    });\n\n    const deleteSAMLProvider = useMutation({\n        mutationFn: async (id: string) => {\n            const response = await fetch(`/api/admin/saml/providers/${id}`, {method: 'DELETE'});\n            if (!response.ok) {\n                const err = await response.json();\n                throw new Error(err.error || 'Failed to delete SAML provider');\n            }\n            return response.json();\n        },\n        onSuccess: () => {\n            queryClient.invalidateQueries({queryKey: ['saml-providers']});\n            toast({title: 'SAML Provider Deleted', description: 'The SAML provider has been deleted successfully.'});\n            setSAMLProviderToDelete(null);\n        },\n        onError: (error: Error) => {\n            toast({title: 'Failed to Delete SAML Provider', description: error.message, variant: 'destructive'});\n        }\n    });\n\n    const handleEditSAMLProvider = (provider: SAMLProvider) => {\n        setSelectedSAMLProvider(provider);\n        samlEditForm.reset({\n            name: provider.name,\n            entityId: provider.entityId,\n            ssoUrl: provider.ssoUrl,\n            certificate: provider.certificate,\n            spEntityId: provider.spEntityId || '',\n            nameIdFormat: provider.nameIdFormat,\n            emailAttribute: provider.emailAttribute,\n            nameAttribute: provider.nameAttribute,\n            enabled: provider.enabled,\n            isDefault: provider.isDefault,\n            allowedEmailDomains: provider.allowedEmailDomains?.join(', ') || '',\n            blockExistingUsers: provider.blockExistingUsers,\n        });\n        setSamlEditClaims(provider.requiredClaims || {});\n        setIsSAMLEditDialogOpen(true);\n    };\n\n    const copySAMLUrl = (url: string, label: string) => {\n        navigator.clipboard.writeText(url);\n        toast({title: 'Copied', description: `${label} copied to clipboard.`});\n    };\n\n    // Filter SAML providers based on active tab\n    const filteredSAMLProviders = samlData?.providers?.filter((provider: SAMLProvider) => {\n        if (activeTab === 'active') return provider.enabled;\n        if (activeTab === 'disabled') return !provider.enabled;\n        return true; // \"all\" tab\n    });\n\n    // Only allow admins to access this page\n    if (user?.role !== 'ADMIN') {\n        return (\n            <div className=\"flex items-center justify-center min-h-screen\">\n                <Card className=\"w-full max-w-md\">\n                    <CardHeader>\n                        <CardTitle className=\"text-destructive flex items-center gap-2\">\n                            <X className=\"h-5 w-5\"/>\n                            Access Denied\n                        </CardTitle>\n                        <CardDescription>\n                            You do not have permission to access OAuth provider settings.\n                        </CardDescription>\n                    </CardHeader>\n                </Card>\n            </div>\n        );\n    }\n\n    return (\n        <motion.div\n            initial={{opacity: 0, y: 20}}\n            animate={{opacity: 1, y: 0}}\n            className=\"space-y-6\"\n        >\n            {/* Add Provider Dialog */}\n            <Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>\n                <DialogContent className=\"sm:max-w-2xl max-h-[90vh] overflow-y-auto\">\n                    <DialogHeader>\n                        <DialogTitle className=\"flex items-center gap-2\">\n                            <Plus className=\"h-5 w-5\"/>\n                            Add OAuth Provider\n                        </DialogTitle>\n                        <DialogDescription>\n                            Configure a new OAuth provider for single sign-on.\n                        </DialogDescription>\n                    </DialogHeader>\n                    <Form {...createForm}>\n                        <form onSubmit={createForm.handleSubmit(onCreateSubmit)} className=\"space-y-6\">\n                            <Tabs defaultValue=\"basic\" className=\"w-full\">\n                                <TabsList className=\"grid w-full grid-cols-2\">\n                                    <TabsTrigger value=\"basic\" className=\"flex items-center gap-1\">\n                                        <Settings className=\"h-4 w-4\"/>\n                                        <span className=\"hidden sm:inline\">Configuration</span>\n                                        <span className=\"sm:hidden\">Config</span>\n                                    </TabsTrigger>\n                                    <TabsTrigger value=\"security\" className=\"flex items-center gap-1\">\n                                        <Shield className=\"h-4 w-4\"/>\n                                        <span className=\"hidden sm:inline\">Security & Access</span>\n                                        <span className=\"sm:hidden\">Security</span>\n                                    </TabsTrigger>\n                                </TabsList>\n\n                                <TabsContent value=\"basic\" className=\"space-y-4 mt-4\">\n                                    <FormField\n                                        control={createForm.control}\n                                        name=\"name\"\n                                        render={({field}) => (\n                                            <FormItem>\n                                                <FormLabel>Provider Name</FormLabel>\n                                                <FormControl>\n                                                    <Input placeholder=\"Custom Provider\" {...field} />\n                                                </FormControl>\n                                                <FormDescription>\n                                                    A descriptive name for the OAuth provider.\n                                                </FormDescription>\n                                                <FormMessage/>\n                                            </FormItem>\n                                        )}\n                                    />\n\n                                    {/* URL Configuration */}\n                                    <FormField\n                                        control={createForm.control}\n                                        name=\"urlMode\"\n                                        render={({field}) => (\n                                            <FormItem>\n                                                <FormLabel>URL Configuration</FormLabel>\n                                                <Select onValueChange={field.onChange} defaultValue={field.value}>\n                                                    <FormControl>\n                                                        <SelectTrigger>\n                                                            <SelectValue placeholder=\"Choose configuration method\"/>\n                                                        </SelectTrigger>\n                                                    </FormControl>\n                                                    <SelectContent>\n                                                        <SelectItem value=\"preset\">\n                                                            <div className=\"flex items-center gap-2\">\n                                                                <Globe className=\"h-4 w-4\"/>\n                                                                Use Provider Preset\n                                                            </div>\n                                                        </SelectItem>\n                                                        <SelectItem value=\"custom\">\n                                                            <div className=\"flex items-center gap-2\">\n                                                                <Link2 className=\"h-4 w-4\"/>\n                                                                Custom URLs\n                                                            </div>\n                                                        </SelectItem>\n                                                    </SelectContent>\n                                                </Select>\n                                                <FormDescription>\n                                                    Choose whether to use a predefined provider or configure custom URLs.\n                                                </FormDescription>\n                                                <FormMessage/>\n                                            </FormItem>\n                                        )}\n                                    />\n\n                                    {/* Preset Selection */}\n                                    {watchCreateUrlMode === 'preset' && (\n                                        <motion.div\n                                            initial={{opacity: 0, height: 0}}\n                                            animate={{opacity: 1, height: 'auto'}}\n                                            exit={{opacity: 0, height: 0}}\n                                            className=\"space-y-4\"\n                                        >\n                                            <FormField\n                                                control={createForm.control}\n                                                name=\"preset\"\n                                                render={({field}) => (\n                                                    <FormItem>\n                                                        <FormLabel>Select Provider</FormLabel>\n                                                        <Select onValueChange={field.onChange} value={field.value}>\n                                                            <FormControl>\n                                                                <SelectTrigger>\n                                                                    <SelectValue placeholder=\"Choose a provider preset\"/>\n                                                                </SelectTrigger>\n                                                            </FormControl>\n                                                            <SelectContent>\n                                                                {Object.entries(PROVIDER_PRESETS).map(([key, preset]) => (\n                                                                    <SelectItem key={key} value={key}>\n                                                                        <div className=\"flex items-center gap-2\">\n                                                                            <ProviderLogo providerName={preset.name}/>\n                                                                            <span>{preset.name}</span>\n                                                                        </div>\n                                                                    </SelectItem>\n                                                                ))}\n                                                            </SelectContent>\n                                                        </Select>\n                                                        <FormDescription>\n                                                            Select from common OAuth providers with preconfigured URLs.\n                                                        </FormDescription>\n                                                        <FormMessage/>\n                                                    </FormItem>\n                                                )}\n                                            />\n\n                                            {/* Show preset URLs preview */}\n                                            {watchCreatePreset && PROVIDER_PRESETS[watchCreatePreset as keyof typeof PROVIDER_PRESETS] && (\n                                                <div className=\"space-y-2 p-4 bg-muted/30 rounded-md border\">\n                                                    <h4 className=\"text-sm font-medium\">Provider URLs (Auto-configured)</h4>\n                                                    <div className=\"space-y-1 text-xs\">\n                                                        <div>\n                                                            <strong>Authorization:</strong> {PROVIDER_PRESETS[watchCreatePreset as keyof typeof PROVIDER_PRESETS].authorizationUrl}\n                                                        </div>\n                                                        <div>\n                                                            <strong>Token:</strong> {PROVIDER_PRESETS[watchCreatePreset as keyof typeof PROVIDER_PRESETS].tokenUrl}\n                                                        </div>\n                                                        <div><strong>User\n                                                            Info:</strong> {PROVIDER_PRESETS[watchCreatePreset as keyof typeof PROVIDER_PRESETS].userInfoUrl}\n                                                        </div>\n                                                    </div>\n                                                </div>\n                                            )}\n                                        </motion.div>\n                                    )}\n\n                                    {/* Custom URLs */}\n                                    {watchCreateUrlMode === 'custom' && (\n                                        <motion.div\n                                            initial={{opacity: 0, height: 0}}\n                                            animate={{opacity: 1, height: 'auto'}}\n                                            exit={{opacity: 0, height: 0}}\n                                            className=\"space-y-4\"\n                                        >\n                                            <div className=\"grid grid-cols-1 gap-4\">\n                                                <FormField\n                                                    control={createForm.control}\n                                                    name=\"authorizationUrl\"\n                                                    render={({field}) => (\n                                                        <FormItem>\n                                                            <FormLabel>Authorization URL</FormLabel>\n                                                            <FormControl>\n                                                                <Input\n                                                                    placeholder=\"https://provider.com/oauth/authorize\" {...field} />\n                                                            </FormControl>\n                                                            <FormDescription>\n                                                                The OAuth authorization endpoint URL.\n                                                            </FormDescription>\n                                                            <FormMessage/>\n                                                        </FormItem>\n                                                    )}\n                                                />\n\n                                                <FormField\n                                                    control={createForm.control}\n                                                    name=\"tokenUrl\"\n                                                    render={({field}) => (\n                                                        <FormItem>\n                                                            <FormLabel>Token URL</FormLabel>\n                                                            <FormControl>\n                                                                <Input\n                                                                    placeholder=\"https://provider.com/oauth/token\" {...field} />\n                                                            </FormControl>\n                                                            <FormDescription>\n                                                                The OAuth token exchange endpoint URL.\n                                                            </FormDescription>\n                                                            <FormMessage/>\n                                                        </FormItem>\n                                                    )}\n                                                />\n\n                                                <FormField\n                                                    control={createForm.control}\n                                                    name=\"userInfoUrl\"\n                                                    render={({field}) => (\n                                                        <FormItem>\n                                                            <FormLabel>User Info URL</FormLabel>\n                                                            <FormControl>\n                                                                <Input\n                                                                    placeholder=\"https://provider.com/oauth/userinfo\" {...field} />\n                                                            </FormControl>\n                                                            <FormDescription>\n                                                                The endpoint to fetch user information.\n                                                            </FormDescription>\n                                                            <FormMessage/>\n                                                        </FormItem>\n                                                    )}\n                                                />\n                                            </div>\n                                        </motion.div>\n                                    )}\n\n                                    <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n                                        <FormField\n                                            control={createForm.control}\n                                            name=\"clientId\"\n                                            render={({field}) => (\n                                                <FormItem>\n                                                    <FormLabel>Client ID</FormLabel>\n                                                    <FormControl>\n                                                        <Input placeholder=\"client_id\" {...field} />\n                                                    </FormControl>\n                                                    <FormMessage/>\n                                                </FormItem>\n                                            )}\n                                        />\n\n                                        <FormField\n                                            control={createForm.control}\n                                            name=\"clientSecret\"\n                                            render={({field}) => (\n                                                <FormItem>\n                                                    <FormLabel>Client Secret</FormLabel>\n                                                    <FormControl>\n                                                        <Input placeholder=\"client_secret\" type=\"password\" {...field} />\n                                                    </FormControl>\n                                                    <FormMessage/>\n                                                </FormItem>\n                                            )}\n                                        />\n                                    </div>\n\n                                    <FormField\n                                        control={createForm.control}\n                                        name=\"scopes\"\n                                        render={({field}) => (\n                                            <FormItem>\n                                                <FormLabel>Scopes</FormLabel>\n                                                <FormControl>\n                                                    <Input placeholder=\"openid,profile,email\" {...field} />\n                                                </FormControl>\n                                                <FormDescription>\n                                                    Comma-separated list of OAuth scopes.\n                                                </FormDescription>\n                                                <FormMessage/>\n                                            </FormItem>\n                                        )}\n                                    />\n\n                                    {/* Callback URL section */}\n                                    <div className=\"space-y-2 border rounded-md p-4 bg-muted/30\">\n                                        <div className=\"flex items-center justify-between\">\n                                            <FormLabel className=\"text-sm font-medium\">Callback URL</FormLabel>\n                                            <Button\n                                                type=\"button\"\n                                                variant=\"ghost\"\n                                                size=\"sm\"\n                                                onClick={() => {\n                                                    const url = `${window.location.origin}/api/auth/oauth/callback/${createForm.watch('name')?.toLowerCase().replace(/\\s+/g, '-') || 'provider'}`;\n                                                    navigator.clipboard.writeText(url);\n                                                    toast({\n                                                        title: 'Copied to clipboard',\n                                                        description: 'Callback URL has been copied to your clipboard.'\n                                                    });\n                                                }}\n                                            >\n                                                <Copy className=\"h-4 w-4 mr-1\"/>\n                                                Copy\n                                            </Button>\n                                        </div>\n                                        <div className=\"flex items-center gap-2\">\n                                            <code\n                                                className=\"flex-1 p-2 text-xs bg-background rounded border overflow-x-auto text-muted-foreground\">\n                                                {`${window.location.origin}/api/auth/oauth/callback/${createForm.watch('name')?.toLowerCase().replace(/\\s+/g, '-') || 'provider'}`}\n                                            </code>\n                                        </div>\n                                        <FormDescription>\n                                            Use this URL as the callback URL (redirect URI) in your OAuth provider\n                                            configuration.\n                                        </FormDescription>\n                                    </div>\n                                </TabsContent>\n\n                                <TabsContent value=\"security\" className=\"space-y-4 mt-4\">\n                                    <FormField\n                                        control={createForm.control}\n                                        name=\"allowedEmailDomains\"\n                                        render={({field}) => (\n                                            <FormItem>\n                                                <FormLabel>Allowed Email Domains</FormLabel>\n                                                <FormControl>\n                                                    <Input placeholder=\"example.com, company.org\" {...field} />\n                                                </FormControl>\n                                                <FormDescription>\n                                                    Restrict signups to specific email domains. Leave empty to allow all domains. Separate multiple domains with commas.\n                                                </FormDescription>\n                                                <FormMessage/>\n                                            </FormItem>\n                                        )}\n                                    />\n\n                                    <FormField\n                                        control={createForm.control}\n                                        name=\"blockExistingUsers\"\n                                        render={({field}) => (\n                                            <FormItem className=\"flex flex-row items-start justify-between rounded-lg border p-4 space-y-0 gap-3\">\n                                                <div className=\"space-y-1\">\n                                                    <FormLabel className=\"text-destructive font-semibold\">Block Existing Users (Dangerous)</FormLabel>\n                                                    <FormDescription className=\"text-destructive/80\">\n                                                        ⚠️ WARNING: When enabled, users with existing accounts cannot log in with this provider, even if their domain is allowed. Only new user registration will be permitted.\n                                                    </FormDescription>\n                                                </div>\n                                                <FormControl>\n                                                    <Switch\n                                                        checked={field.value}\n                                                        onCheckedChange={field.onChange}\n                                                    />\n                                                </FormControl>\n                                            </FormItem>\n                                        )}\n                                    />\n\n                                    <div className=\"space-y-4 rounded-lg border p-4\">\n                                        <div className=\"space-y-2\">\n                                            <FormLabel>Required Claims / Attributes</FormLabel>\n                                            <FormDescription>\n                                                Validate specific claims from OAuth userInfo or SAML attributes. Users must have matching values to sign in.\n                                                Example: Require users to be in a specific organization or have a certain role.\n                                            </FormDescription>\n                                        </div>\n\n                                        <div className=\"space-y-2\">\n                                            {Object.entries(oauthCreateClaims).map(([key, value], index) => (\n                                                <div key={index} className=\"flex gap-2\">\n                                                    <Input\n                                                        placeholder=\"Claim name (e.g., organization)\"\n                                                        value={key}\n                                                        onChange={(e) => {\n                                                            const newClaims = {...oauthCreateClaims};\n                                                            delete newClaims[key];\n                                                            newClaims[e.target.value] = value;\n                                                            setOauthCreateClaims(newClaims);\n                                                        }}\n                                                        className=\"flex-1\"\n                                                    />\n                                                    <Input\n                                                        placeholder=\"Required value (e.g., my-company)\"\n                                                        value={value}\n                                                        onChange={(e) => {\n                                                            setOauthCreateClaims({...oauthCreateClaims, [key]: e.target.value});\n                                                        }}\n                                                        className=\"flex-1\"\n                                                    />\n                                                    <Button\n                                                        type=\"button\"\n                                                        variant=\"ghost\"\n                                                        size=\"icon\"\n                                                        onClick={() => {\n                                                            const newClaims = {...oauthCreateClaims};\n                                                            delete newClaims[key];\n                                                            setOauthCreateClaims(newClaims);\n                                                        }}\n                                                    >\n                                                        <Trash2 className=\"h-4 w-4\" />\n                                                    </Button>\n                                                </div>\n                                            ))}\n\n                                            <Button\n                                                type=\"button\"\n                                                variant=\"outline\"\n                                                size=\"sm\"\n                                                onClick={() => {\n                                                    const newKey = `claim_${Object.keys(oauthCreateClaims).length + 1}`;\n                                                    setOauthCreateClaims({...oauthCreateClaims, [newKey]: ''});\n                                                }}\n                                            >\n                                                <Plus className=\"h-4 w-4 mr-2\" />\n                                                Add Claim Rule\n                                            </Button>\n                                        </div>\n\n                                        <div className=\"text-xs text-muted-foreground space-y-1\">\n                                            <p><strong>For OAuth:</strong> Claim names like <code>organizations</code>, <code>groups</code>, <code>role</code></p>\n                                            <p><strong>For SAML:</strong> Attribute names from your IdP (e.g., <code>memberOf</code>, <code>department</code>)</p>\n                                            <p><strong>Note:</strong> Matching is case-insensitive. Array claims (groups, etc.) are supported.</p>\n                                        </div>\n                                    </div>\n\n                                    <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n                                        <FormField\n                                            control={createForm.control}\n                                            name=\"enabled\"\n                                            render={({field}) => (\n                                                <FormItem\n                                                    className=\"flex flex-row items-center justify-between rounded-lg border p-3 h-full\">\n                                                    <div className=\"space-y-0.5\">\n                                                        <FormLabel>Enabled</FormLabel>\n                                                        <FormDescription>\n                                                            Allow users to sign in with this provider.\n                                                        </FormDescription>\n                                                    </div>\n                                                    <FormControl>\n                                                        <Switch\n                                                            checked={field.value}\n                                                            onCheckedChange={field.onChange}\n                                                        />\n                                                    </FormControl>\n                                                </FormItem>\n                                            )}\n                                        />\n\n                                        <FormField\n                                            control={createForm.control}\n                                            name=\"isDefault\"\n                                            render={({field}) => (\n                                                <FormItem\n                                                    className=\"flex flex-row items-center justify-between rounded-lg border p-3 h-full\">\n                                                    <div className=\"space-y-0.5\">\n                                                        <FormLabel>Default Provider</FormLabel>\n                                                        <FormDescription>\n                                                            Make this the default sign-in option.\n                                                        </FormDescription>\n                                                    </div>\n                                                    <FormControl>\n                                                        <Switch\n                                                            checked={field.value}\n                                                            onCheckedChange={field.onChange}\n                                                        />\n                                                    </FormControl>\n                                                </FormItem>\n                                            )}\n                                        />\n                                    </div>\n                                </TabsContent>\n                            </Tabs>\n\n                            <DialogFooter>\n                                <Button\n                                    type=\"button\"\n                                    variant=\"outline\"\n                                    onClick={() => setIsAddDialogOpen(false)}\n                                >\n                                    Cancel\n                                </Button>\n                                <Button type=\"submit\" disabled={addProvider.isPending}>\n                                    {addProvider.isPending ? (\n                                        <>\n                                            <Loader2 className=\"mr-2 h-4 w-4 animate-spin\"/>\n                                            Adding...\n                                        </>\n                                    ) : (\n                                        'Add Provider'\n                                    )}\n                                </Button>\n                            </DialogFooter>\n                        </form>\n                    </Form>\n                </DialogContent>\n            </Dialog>\n\n            {/* Edit Provider Dialog */}\n            <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>\n                <DialogContent className=\"sm:max-w-2xl max-h-[90vh] overflow-y-auto\">\n                    <DialogHeader>\n                        <DialogTitle className=\"flex items-center gap-2\">\n                            <Pencil className=\"h-5 w-5\"/>\n                            Edit {selectedProvider?.name || 'OAuth Provider'}\n                        </DialogTitle>\n                        <DialogDescription>\n                            Update the OAuth provider configuration.\n                        </DialogDescription>\n                    </DialogHeader>\n                    <Form {...editForm}>\n                        <form onSubmit={editForm.handleSubmit(onEditSubmit)} className=\"space-y-6\">\n                            <Tabs defaultValue=\"basic\" className=\"w-full\">\n                                <TabsList className=\"grid w-full grid-cols-2\">\n                                    <TabsTrigger value=\"basic\" className=\"flex items-center gap-1\">\n                                        <Settings className=\"h-4 w-4\"/>\n                                        <span className=\"hidden sm:inline\">Configuration</span>\n                                        <span className=\"sm:hidden\">Config</span>\n                                    </TabsTrigger>\n                                    <TabsTrigger value=\"security\" className=\"flex items-center gap-1\">\n                                        <Shield className=\"h-4 w-4\"/>\n                                        <span className=\"hidden sm:inline\">Security & Access</span>\n                                        <span className=\"sm:hidden\">Security</span>\n                                    </TabsTrigger>\n                                </TabsList>\n\n                                <TabsContent value=\"basic\" className=\"space-y-4 mt-4\">\n                                    <FormField\n                                        control={editForm.control}\n                                        name=\"name\"\n                                        render={({field}) => (\n                                            <FormItem>\n                                                <FormLabel>Provider Name</FormLabel>\n                                                <FormControl>\n                                                    <Input placeholder=\"Custom Provider\" {...field} />\n                                                </FormControl>\n                                                <FormDescription>\n                                                    A descriptive name for the OAuth provider.\n                                                </FormDescription>\n                                                <FormMessage/>\n                                            </FormItem>\n                                        )}\n                                    />\n\n                                    {/* URL Configuration */}\n                                    <FormField\n                                        control={editForm.control}\n                                        name=\"urlMode\"\n                                        render={({field}) => (\n                                            <FormItem>\n                                                <FormLabel>URL Configuration</FormLabel>\n                                                <Select onValueChange={field.onChange} value={field.value}>\n                                                    <FormControl>\n                                                        <SelectTrigger>\n                                                            <SelectValue placeholder=\"Choose configuration method\"/>\n                                                        </SelectTrigger>\n                                                    </FormControl>\n                                                    <SelectContent>\n                                                        <SelectItem value=\"preset\">\n                                                            <div className=\"flex items-center gap-2\">\n                                                                <Globe className=\"h-4 w-4\"/>\n                                                                Use Provider Preset\n                                                            </div>\n                                                        </SelectItem>\n                                                        <SelectItem value=\"custom\">\n                                                            <div className=\"flex items-center gap-2\">\n                                                                <Link2 className=\"h-4 w-4\"/>\n                                                                Custom URLs\n                                                            </div>\n                                                        </SelectItem>\n                                                    </SelectContent>\n                                                </Select>\n                                                <FormDescription>\n                                                    Choose whether to use a predefined provider or configure custom URLs.\n                                                </FormDescription>\n                                                <FormMessage/>\n                                            </FormItem>\n                                        )}\n                                    />\n\n                                    {/* Preset Selection */}\n                                    {watchEditUrlMode === 'preset' && (\n                                        <motion.div\n                                            initial={{opacity: 0, height: 0}}\n                                            animate={{opacity: 1, height: 'auto'}}\n                                            exit={{opacity: 0, height: 0}}\n                                            className=\"space-y-4\"\n                                        >\n                                            <FormField\n                                                control={editForm.control}\n                                                name=\"preset\"\n                                                render={({field}) => (\n                                                    <FormItem>\n                                                        <FormLabel>Select Provider</FormLabel>\n                                                        <Select onValueChange={field.onChange} value={field.value}>\n                                                            <FormControl>\n                                                                <SelectTrigger>\n                                                                    <SelectValue placeholder=\"Choose a provider preset\"/>\n                                                                </SelectTrigger>\n                                                            </FormControl>\n                                                            <SelectContent>\n                                                                {Object.entries(PROVIDER_PRESETS).map(([key, preset]) => (\n                                                                    <SelectItem key={key} value={key}>\n                                                                        <div className=\"flex items-center gap-2\">\n                                                                            <ProviderLogo providerName={preset.name}/>\n                                                                            <span>{preset.name}</span>\n                                                                        </div>\n                                                                    </SelectItem>\n                                                                ))}\n                                                            </SelectContent>\n                                                        </Select>\n                                                        <FormDescription>\n                                                            Select from common OAuth providers with preconfigured URLs.\n                                                        </FormDescription>\n                                                        <FormMessage/>\n                                                    </FormItem>\n                                                )}\n                                            />\n\n                                            {/* Show preset URLs preview */}\n                                            {watchEditPreset && PROVIDER_PRESETS[watchEditPreset as keyof typeof PROVIDER_PRESETS] && (\n                                                <div className=\"space-y-2 p-4 bg-muted/30 rounded-md border\">\n                                                    <h4 className=\"text-sm font-medium\">Provider URLs (Auto-configured)</h4>\n                                                    <div className=\"space-y-1 text-xs\">\n                                                        <div>\n                                                            <strong>Authorization:</strong> {PROVIDER_PRESETS[watchEditPreset as keyof typeof PROVIDER_PRESETS].authorizationUrl}\n                                                        </div>\n                                                        <div>\n                                                            <strong>Token:</strong> {PROVIDER_PRESETS[watchEditPreset as keyof typeof PROVIDER_PRESETS].tokenUrl}\n                                                        </div>\n                                                        <div><strong>User\n                                                            Info:</strong> {PROVIDER_PRESETS[watchEditPreset as keyof typeof PROVIDER_PRESETS].userInfoUrl}\n                                                        </div>\n                                                    </div>\n                                                </div>\n                                            )}\n                                        </motion.div>\n                                    )}\n\n                                    {/* Custom URLs */}\n                                    {watchEditUrlMode === 'custom' && (\n                                        <motion.div\n                                            initial={{opacity: 0, height: 0}}\n                                            animate={{opacity: 1, height: 'auto'}}\n                                            exit={{opacity: 0, height: 0}}\n                                            className=\"space-y-4\"\n                                        >\n                                            <div className=\"grid grid-cols-1 gap-4\">\n                                                <FormField\n                                                    control={editForm.control}\n                                                    name=\"authorizationUrl\"\n                                                    render={({field}) => (\n                                                        <FormItem>\n                                                            <FormLabel>Authorization URL</FormLabel>\n                                                            <FormControl>\n                                                                <Input\n                                                                    placeholder=\"https://provider.com/oauth/authorize\" {...field} />\n                                                            </FormControl>\n                                                            <FormDescription>\n                                                                The OAuth authorization endpoint URL.\n                                                            </FormDescription>\n                                                            <FormMessage/>\n                                                        </FormItem>\n                                                    )}\n                                                />\n\n                                                <FormField\n                                                    control={editForm.control}\n                                                    name=\"tokenUrl\"\n                                                    render={({field}) => (\n                                                        <FormItem>\n                                                            <FormLabel>Token URL</FormLabel>\n                                                            <FormControl>\n                                                                <Input\n                                                                    placeholder=\"https://provider.com/oauth/token\" {...field} />\n                                                            </FormControl>\n                                                            <FormDescription>\n                                                                The OAuth token exchange endpoint URL.\n                                                            </FormDescription>\n                                                            <FormMessage/>\n                                                        </FormItem>\n                                                    )}\n                                                />\n\n                                                <FormField\n                                                    control={editForm.control}\n                                                    name=\"userInfoUrl\"\n                                                    render={({field}) => (\n                                                        <FormItem>\n                                                            <FormLabel>User Info URL</FormLabel>\n                                                            <FormControl>\n                                                                <Input\n                                                                    placeholder=\"https://provider.com/oauth/userinfo\" {...field} />\n                                                            </FormControl>\n                                                            <FormDescription>\n                                                                The endpoint to fetch user information.\n                                                            </FormDescription>\n                                                            <FormMessage/>\n                                                        </FormItem>\n                                                    )}\n                                                />\n                                            </div>\n                                        </motion.div>\n                                    )}\n\n                                    <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n                                        <FormField\n                                            control={editForm.control}\n                                            name=\"clientId\"\n                                            render={({field}) => (\n                                                <FormItem>\n                                                    <FormLabel>Client ID</FormLabel>\n                                                    <FormControl>\n                                                        <Input placeholder=\"client_id\" {...field} />\n                                                    </FormControl>\n                                                    <FormMessage/>\n                                                </FormItem>\n                                            )}\n                                        />\n\n                                        <FormField\n                                            control={editForm.control}\n                                            name=\"clientSecret\"\n                                            render={({field}) => (\n                                                <FormItem>\n                                                    <FormLabel>Client Secret</FormLabel>\n                                                    <FormControl>\n                                                        <Input placeholder=\"client_secret\" type=\"password\" {...field} />\n                                                    </FormControl>\n                                                    <FormMessage/>\n                                                </FormItem>\n                                            )}\n                                        />\n                                    </div>\n\n                                    <FormField\n                                        control={editForm.control}\n                                        name=\"scopes\"\n                                        render={({field}) => (\n                                            <FormItem>\n                                                <FormLabel>Scopes</FormLabel>\n                                                <FormControl>\n                                                    <Input placeholder=\"openid,profile,email\" {...field} />\n                                                </FormControl>\n                                                <FormDescription>\n                                                    Comma-separated list of OAuth scopes.\n                                                </FormDescription>\n                                                <FormMessage/>\n                                            </FormItem>\n                                        )}\n                                    />\n\n                                    {/* Callback URL section */}\n                                    <div className=\"space-y-2 border rounded-md p-4 bg-muted/30\">\n                                        <div className=\"flex items-center justify-between\">\n                                            <FormLabel className=\"text-sm font-medium\">Callback URL</FormLabel>\n                                            <Button\n                                                type=\"button\"\n                                                variant=\"ghost\"\n                                                size=\"sm\"\n                                                onClick={() => {\n                                                    const url = `${window.location.origin}/api/auth/oauth/callback/${editForm.watch('name')?.toLowerCase().replace(/\\s+/g, '-') || 'provider'}`;\n                                                    navigator.clipboard.writeText(url);\n                                                    toast({\n                                                        title: 'Copied to clipboard',\n                                                        description: 'Callback URL has been copied to your clipboard.'\n                                                    });\n                                                }}\n                                            >\n                                                <Copy className=\"h-4 w-4 mr-1\"/>\n                                                Copy\n                                            </Button>\n                                        </div>\n                                        <div className=\"flex items-center gap-2\">\n                                            <code\n                                                className=\"flex-1 p-2 text-xs bg-background rounded border overflow-x-auto text-muted-foreground\">\n                                                {`${window.location.origin}/api/auth/oauth/callback/${editForm.watch('name')?.toLowerCase().replace(/\\s+/g, '-') || 'provider'}`}\n                                            </code>\n                                        </div>\n                                        <FormDescription>\n                                            Use this URL as the callback URL (redirect URI) in your OAuth provider\n                                            configuration.\n                                        </FormDescription>\n                                    </div>\n                                </TabsContent>\n\n                                <TabsContent value=\"security\" className=\"space-y-4 mt-4\">\n                                    <FormField\n                                        control={editForm.control}\n                                        name=\"allowedEmailDomains\"\n                                        render={({field}) => (\n                                            <FormItem>\n                                                <FormLabel>Allowed Email Domains</FormLabel>\n                                                <FormControl>\n                                                    <Input placeholder=\"example.com, company.org\" {...field} />\n                                                </FormControl>\n                                                <FormDescription>\n                                                    Restrict signups to specific email domains. Leave empty to allow all domains. Separate multiple domains with commas.\n                                                </FormDescription>\n                                                <FormMessage/>\n                                            </FormItem>\n                                        )}\n                                    />\n\n                                    <FormField\n                                        control={editForm.control}\n                                        name=\"blockExistingUsers\"\n                                        render={({field}) => (\n                                            <FormItem className=\"flex flex-row items-start justify-between rounded-lg border p-4 space-y-0 gap-3\">\n                                                <div className=\"space-y-1\">\n                                                    <FormLabel className=\"text-destructive font-semibold\">Block Existing Users (Dangerous)</FormLabel>\n                                                    <FormDescription className=\"text-destructive/80\">\n                                                        ⚠️ WARNING: When enabled, users with existing accounts cannot log in with this provider, even if their domain is allowed. Only new user registration will be permitted.\n                                                    </FormDescription>\n                                                </div>\n                                                <FormControl>\n                                                    <Switch\n                                                        checked={field.value}\n                                                        onCheckedChange={field.onChange}\n                                                    />\n                                                </FormControl>\n                                            </FormItem>\n                                        )}\n                                    />\n\n                                    <div className=\"space-y-4 rounded-lg border p-4\">\n                                        <div className=\"space-y-2\">\n                                            <FormLabel>Required Claims / Attributes</FormLabel>\n                                            <FormDescription>\n                                                Validate specific claims from OAuth userInfo or SAML attributes. Users must have matching values to sign in.\n                                                Example: Require users to be in a specific organization or have a certain role.\n                                            </FormDescription>\n                                        </div>\n\n                                        <div className=\"space-y-2\">\n                                            {Object.entries(oauthEditClaims).map(([key, value], index) => (\n                                                <div key={index} className=\"flex gap-2\">\n                                                    <Input\n                                                        placeholder=\"Claim name (e.g., organization)\"\n                                                        value={key}\n                                                        onChange={(e) => {\n                                                            const newClaims = {...oauthEditClaims};\n                                                            delete newClaims[key];\n                                                            newClaims[e.target.value] = value;\n                                                            setOauthEditClaims(newClaims);\n                                                        }}\n                                                        className=\"flex-1\"\n                                                    />\n                                                    <Input\n                                                        placeholder=\"Required value (e.g., my-company)\"\n                                                        value={value}\n                                                        onChange={(e) => {\n                                                            setOauthEditClaims({...oauthEditClaims, [key]: e.target.value});\n                                                        }}\n                                                        className=\"flex-1\"\n                                                    />\n                                                    <Button\n                                                        type=\"button\"\n                                                        variant=\"ghost\"\n                                                        size=\"icon\"\n                                                        onClick={() => {\n                                                            const newClaims = {...oauthEditClaims};\n                                                            delete newClaims[key];\n                                                            setOauthEditClaims(newClaims);\n                                                        }}\n                                                    >\n                                                        <Trash2 className=\"h-4 w-4\" />\n                                                    </Button>\n                                                </div>\n                                            ))}\n\n                                            <Button\n                                                type=\"button\"\n                                                variant=\"outline\"\n                                                size=\"sm\"\n                                                onClick={() => {\n                                                    const newKey = `claim_${Object.keys(oauthEditClaims).length + 1}`;\n                                                    setOauthEditClaims({...oauthEditClaims, [newKey]: ''});\n                                                }}\n                                            >\n                                                <Plus className=\"h-4 w-4 mr-2\" />\n                                                Add Claim Rule\n                                            </Button>\n                                        </div>\n\n                                        <div className=\"text-xs text-muted-foreground space-y-1\">\n                                            <p><strong>For OAuth:</strong> Claim names like <code>organizations</code>, <code>groups</code>, <code>role</code></p>\n                                            <p><strong>For SAML:</strong> Attribute names from your IdP (e.g., <code>memberOf</code>, <code>department</code>)</p>\n                                            <p><strong>Note:</strong> Matching is case-insensitive. Array claims (groups, etc.) are supported.</p>\n                                        </div>\n                                    </div>\n\n                                    <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n                                        <FormField\n                                            control={editForm.control}\n                                            name=\"enabled\"\n                                            render={({field}) => (\n                                                <FormItem\n                                                    className=\"flex flex-row items-center justify-between rounded-lg border p-3 h-full\">\n                                                    <div className=\"space-y-0.5\">\n                                                        <FormLabel>Enabled</FormLabel>\n                                                        <FormDescription>\n                                                            Allow users to sign in with this provider.\n                                                        </FormDescription>\n                                                    </div>\n                                                    <FormControl>\n                                                        <Switch\n                                                            checked={field.value}\n                                                            onCheckedChange={field.onChange}\n                                                        />\n                                                    </FormControl>\n                                                </FormItem>\n                                            )}\n                                        />\n\n                                        <FormField\n                                            control={editForm.control}\n                                            name=\"isDefault\"\n                                            render={({field}) => (\n                                                <FormItem\n                                                    className=\"flex flex-row items-center justify-between rounded-lg border p-3 h-full\">\n                                                    <div className=\"space-y-0.5\">\n                                                        <FormLabel>Default Provider</FormLabel>\n                                                        <FormDescription>\n                                                            Make this the default sign-in option.\n                                                        </FormDescription>\n                                                    </div>\n                                                    <FormControl>\n                                                        <Switch\n                                                            checked={field.value}\n                                                            onCheckedChange={field.onChange}\n                                                        />\n                                                    </FormControl>\n                                                </FormItem>\n                                            )}\n                                        />\n                                    </div>\n                                </TabsContent>\n                            </Tabs>\n\n                            <DialogFooter>\n                                <Button\n                                    type=\"button\"\n                                    variant=\"outline\"\n                                    onClick={() => setIsEditDialogOpen(false)}\n                                >\n                                    Cancel\n                                </Button>\n                                <Button type=\"submit\" disabled={updateProvider.isPending}>\n                                    {updateProvider.isPending ? (\n                                        <>\n                                            <Loader2 className=\"mr-2 h-4 w-4 animate-spin\"/>\n                                            Updating...\n                                        </>\n                                    ) : (\n                                        'Update Provider'\n                                    )}\n                                </Button>\n                            </DialogFooter>\n                        </form>\n                    </Form>\n                </DialogContent>\n            </Dialog>\n            {/* Delete Provider Dialog */}\n            <AlertDialog open={!!providerToDelete} onOpenChange={(isOpen) => !isOpen && setProviderToDelete(null)}>\n                <AlertDialogContent>\n                    <AlertDialogHeader>\n                        <AlertDialogTitle className=\"flex items-center gap-2\">\n                            <Trash2 className=\"h-5 w-5 text-destructive\"/>\n                            Delete {providerToDelete?.name || 'OAuth Provider'}\n                        </AlertDialogTitle>\n                        <AlertDialogDescription>\n                            Are you sure you want to delete this OAuth provider? This action cannot be undone.\n                            Users who previously signed in with this provider may lose access.\n                        </AlertDialogDescription>\n                    </AlertDialogHeader>\n                    <AlertDialogFooter>\n                        <AlertDialogCancel>Cancel</AlertDialogCancel>\n                        <AlertDialogAction\n                            onClick={() => providerToDelete && deleteProvider.mutate(providerToDelete.id)}\n                            className=\"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\n                        >\n                            {deleteProvider.isPending ? (\n                                <Loader2 className=\"mr-2 h-4 w-4 animate-spin\"/>\n                            ) : (\n                                <Trash2 className=\"mr-2 h-4 w-4\"/>\n                            )}\n                            Delete\n                        </AlertDialogAction>\n                    </AlertDialogFooter>\n                </AlertDialogContent>\n            </AlertDialog>\n\n            {/* Page Header */}\n            <div>\n                <h1 className=\"text-3xl font-bold tracking-tight\">SSO Providers</h1>\n                <p className=\"text-muted-foreground mt-2\">\n                    Manage OAuth/OIDC and SAML single sign-on providers for Changerawr\n                </p>\n            </div>\n\n            {/* Outer SSO type tabs */}\n            <Tabs value={ssoType} onValueChange={(v) => setSsoType(v as 'oauth' | 'saml')} className=\"w-full\">\n                <TabsList className=\"grid w-full sm:w-auto grid-cols-2 mb-4\">\n                    <TabsTrigger value=\"oauth\" className=\"flex items-center gap-1\">\n                        <KeyRound className=\"h-4 w-4\"/>\n                        <span>OAuth / OIDC</span>\n                    </TabsTrigger>\n                    <TabsTrigger value=\"saml\" className=\"flex items-center gap-1\">\n                        <Fingerprint className=\"h-4 w-4\"/>\n                        <span>SAML</span>\n                    </TabsTrigger>\n                </TabsList>\n\n                {/* OAuth Tab Content */}\n                <TabsContent value=\"oauth\" className=\"space-y-4\">\n                    <div className=\"flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4\">\n                        <div>\n                            <h2 className=\"text-xl font-semibold\">OAuth / OpenID Connect</h2>\n                            <p className=\"text-sm text-muted-foreground\">Manage OAuth 2.0 and OpenID Connect providers</p>\n                        </div>\n                        <Button onClick={() => setIsAddDialogOpen(true)} className=\"sm:self-start\">\n                            <Plus className=\"mr-2 h-4 w-4\"/>\n                            Add OAuth Provider\n                        </Button>\n                    </div>\n\n                {/* Filter tabs */}\n                <Tabs defaultValue=\"active\" value={activeTab} onValueChange={setActiveTab} className=\"w-full\">\n                <TabsList className=\"grid w-full sm:w-auto grid-cols-3\">\n                    <TabsTrigger value=\"active\" className=\"flex items-center gap-1\">\n                        <Check className=\"h-4 w-4\"/>\n                        <span>Active</span>\n                    </TabsTrigger>\n                    <TabsTrigger value=\"disabled\" className=\"flex items-center gap-1\">\n                        <X className=\"h-4 w-4\"/>\n                        <span>Disabled</span>\n                    </TabsTrigger>\n                    <TabsTrigger value=\"all\" className=\"flex items-center gap-1\">\n                        <Settings className=\"h-4 w-4\"/>\n                        <span>All</span>\n                    </TabsTrigger>\n                </TabsList>\n\n                <TabsContent value=\"active\" className=\"mt-4\">\n                    <ProvidersList\n                        providers={filteredProviders}\n                        isLoading={isLoading}\n                        onEdit={handleEditProvider}\n                        onDelete={setProviderToDelete}\n                        setIsAddDialogOpen={setIsAddDialogOpen}\n                    />\n                </TabsContent>\n\n                <TabsContent value=\"disabled\" className=\"mt-4\">\n                    <ProvidersList\n                        providers={filteredProviders}\n                        isLoading={isLoading}\n                        onEdit={handleEditProvider}\n                        onDelete={setProviderToDelete}\n                        setIsAddDialogOpen={setIsAddDialogOpen}\n                    />\n                </TabsContent>\n\n                <TabsContent value=\"all\" className=\"mt-4\">\n                    <ProvidersList\n                        providers={filteredProviders}\n                        isLoading={isLoading}\n                        onEdit={handleEditProvider}\n                        onDelete={setProviderToDelete}\n                        setIsAddDialogOpen={setIsAddDialogOpen}\n                    />\n                </TabsContent>\n            </Tabs>\n                </TabsContent>\n\n                {/* SAML Tab Content */}\n                <TabsContent value=\"saml\" className=\"space-y-4\">\n                    {/* SAML Add Dialog */}\n                    <Dialog open={isSAMLAddDialogOpen} onOpenChange={setIsSAMLAddDialogOpen}>\n                        <DialogContent className=\"sm:max-w-2xl max-h-[90vh] overflow-y-auto\">\n                            <DialogHeader>\n                                <DialogTitle className=\"flex items-center gap-2\">\n                                    <Plus className=\"h-5 w-5\"/>\n                                    Add SAML Provider\n                                </DialogTitle>\n                                <DialogDescription>\n                                    Configure a new SAML 2.0 identity provider for enterprise SSO.\n                                </DialogDescription>\n                            </DialogHeader>\n                            <Form {...samlCreateForm}>\n                                <form onSubmit={samlCreateForm.handleSubmit((d) => addSAMLProvider.mutate(d))} className=\"space-y-6\">\n                                    <Tabs defaultValue=\"basic\" className=\"w-full\">\n                                        <TabsList className=\"grid w-full grid-cols-3\">\n                                            <TabsTrigger value=\"basic\" className=\"flex items-center gap-1\">\n                                                <Settings className=\"h-4 w-4\"/>\n                                                <span className=\"hidden sm:inline\">Configuration</span>\n                                                <span className=\"sm:hidden\">Config</span>\n                                            </TabsTrigger>\n                                            <TabsTrigger value=\"security\" className=\"flex items-center gap-1\">\n                                                <Shield className=\"h-4 w-4\"/>\n                                                <span className=\"hidden sm:inline\">Security & Access</span>\n                                                <span className=\"sm:hidden\">Security</span>\n                                            </TabsTrigger>\n                                            <TabsTrigger value=\"advanced\" className=\"flex items-center gap-1\">\n                                                <User className=\"h-4 w-4\"/>\n                                                <span className=\"hidden sm:inline\">Attribute Mapping</span>\n                                                <span className=\"sm:hidden\">Attributes</span>\n                                            </TabsTrigger>\n                                        </TabsList>\n\n                                        <TabsContent value=\"basic\" className=\"space-y-4 mt-4\">\n                                            <FormField control={samlCreateForm.control} name=\"name\" render={({field}) => (\n                                                <FormItem>\n                                                    <FormLabel>Provider Name</FormLabel>\n                                                    <FormControl><Input placeholder=\"Okta SAML\" {...field}/></FormControl>\n                                                    <FormDescription>Unique display name for this provider.</FormDescription>\n                                                    <FormMessage/>\n                                                </FormItem>\n                                            )}/>\n                                            <FormField control={samlCreateForm.control} name=\"entityId\" render={({field}) => (\n                                                <FormItem>\n                                                    <FormLabel>IdP Entity ID / Issuer</FormLabel>\n                                                    <FormControl><Input placeholder=\"https://idp.example.com/entity\" {...field}/></FormControl>\n                                                    <FormDescription>The Entity ID or Issuer of the Identity Provider.</FormDescription>\n                                                    <FormMessage/>\n                                                </FormItem>\n                                            )}/>\n                                            <FormField control={samlCreateForm.control} name=\"ssoUrl\" render={({field}) => (\n                                                <FormItem>\n                                                    <FormLabel>IdP SSO URL</FormLabel>\n                                                    <FormControl><Input placeholder=\"https://idp.example.com/sso/saml\" {...field}/></FormControl>\n                                                    <FormDescription>The IdP single sign-on URL (HTTP-Redirect binding).</FormDescription>\n                                                    <FormMessage/>\n                                                </FormItem>\n                                            )}/>\n                                            <FormField control={samlCreateForm.control} name=\"certificate\" render={({field}) => (\n                                                <FormItem>\n                                                    <FormLabel>IdP Signing Certificate (PEM)</FormLabel>\n                                                    <FormControl>\n                                                        <textarea\n                                                            className=\"flex min-h-[120px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 font-mono\"\n                                                            placeholder=\"-----BEGIN CERTIFICATE-----&#10;...&#10;-----END CERTIFICATE-----\"\n                                                            {...field}\n                                                        />\n                                                    </FormControl>\n                                                    <FormDescription>Paste the full PEM certificate from your IdP.</FormDescription>\n                                                    <FormMessage/>\n                                                </FormItem>\n                                            )}/>\n                                            <FormField control={samlCreateForm.control} name=\"spEntityId\" render={({field}) => (\n                                                <FormItem>\n                                                    <FormLabel>SP Entity ID Override (optional)</FormLabel>\n                                                    <FormControl><Input placeholder=\"Defaults to metadata URL\" {...field}/></FormControl>\n                                                    <FormMessage/>\n                                                </FormItem>\n                                            )}/>\n\n                                            {/* SP URLs section */}\n                                            <div className=\"space-y-2 border rounded-md p-4 bg-muted/30\">\n                                                <p className=\"text-sm font-medium\">SP URLs (register these with your IdP)</p>\n                                                <div className=\"space-y-3\">\n                                                    <div className=\"space-y-1\">\n                                                        <div className=\"flex items-center justify-between\">\n                                                            <FormLabel className=\"text-xs text-muted-foreground\">ACS URL</FormLabel>\n                                                            <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => copySAMLUrl(`${window.location.origin}/api/auth/saml/callback/${samlCreateForm.watch('name')?.toLowerCase().replace(/\\s+/g, '-') || 'provider'}`, 'ACS URL')}>\n                                                                <Copy className=\"h-3 w-3 mr-1\"/>\n                                                                Copy\n                                                            </Button>\n                                                        </div>\n                                                        <code className=\"block p-2 text-xs bg-background rounded border overflow-x-auto text-muted-foreground\">\n                                                            {`${typeof window !== 'undefined' ? window.location.origin : ''}/api/auth/saml/callback/${samlCreateForm.watch('name')?.toLowerCase().replace(/\\s+/g, '-') || 'provider'}`}\n                                                        </code>\n                                                    </div>\n                                                    <div className=\"space-y-1\">\n                                                        <div className=\"flex items-center justify-between\">\n                                                            <FormLabel className=\"text-xs text-muted-foreground\">Metadata URL</FormLabel>\n                                                            <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => copySAMLUrl(`${window.location.origin}/api/auth/saml/metadata/${samlCreateForm.watch('name')?.toLowerCase().replace(/\\s+/g, '-') || 'provider'}`, 'Metadata URL')}>\n                                                                <Copy className=\"h-3 w-3 mr-1\"/>\n                                                                Copy\n                                                            </Button>\n                                                        </div>\n                                                        <code className=\"block p-2 text-xs bg-background rounded border overflow-x-auto text-muted-foreground\">\n                                                            {`${typeof window !== 'undefined' ? window.location.origin : ''}/api/auth/saml/metadata/${samlCreateForm.watch('name')?.toLowerCase().replace(/\\s+/g, '-') || 'provider'}`}\n                                                        </code>\n                                                    </div>\n                                                </div>\n                                            </div>\n                                        </TabsContent>\n\n                                        <TabsContent value=\"security\" className=\"space-y-4 mt-4\">\n                                            <FormField control={samlCreateForm.control} name=\"allowedEmailDomains\" render={({field}) => (\n                                                <FormItem>\n                                                    <FormLabel>Allowed Email Domains</FormLabel>\n                                                    <FormControl><Input placeholder=\"example.com, company.org\" {...field}/></FormControl>\n                                                    <FormDescription>\n                                                        Restrict signups to specific email domains. Leave empty to allow all domains. Separate multiple domains with commas.\n                                                    </FormDescription>\n                                                    <FormMessage/>\n                                                </FormItem>\n                                            )}/>\n\n                                            <FormField control={samlCreateForm.control} name=\"blockExistingUsers\" render={({field}) => (\n                                                <FormItem className=\"flex flex-row items-start justify-between rounded-lg border p-4 space-y-0 gap-3\">\n                                                    <div className=\"space-y-1\">\n                                                        <FormLabel className=\"text-destructive font-semibold\">Block Existing Users (Dangerous)</FormLabel>\n                                                        <FormDescription className=\"text-destructive/80\">\n                                                            ⚠️ WARNING: When enabled, users with existing accounts cannot log in with this provider, even if their domain is allowed. Only new user registration will be permitted.\n                                                        </FormDescription>\n                                                    </div>\n                                                    <FormControl>\n                                                        <Switch checked={field.value} onCheckedChange={field.onChange}/>\n                                                    </FormControl>\n                                                </FormItem>\n                                            )}/>\n\n                                            <div className=\"space-y-4 rounded-lg border p-4\">\n                                                <div className=\"space-y-2\">\n                                                    <FormLabel>Required Claims / Attributes</FormLabel>\n                                                    <FormDescription>\n                                                        Validate specific claims from OAuth userInfo or SAML attributes. Users must have matching values to sign in.\n                                                        Example: Require users to be in a specific organization or have a certain role.\n                                                    </FormDescription>\n                                                </div>\n\n                                                <div className=\"space-y-2\">\n                                                    {Object.entries(samlCreateClaims).map(([key, value], index) => (\n                                                        <div key={index} className=\"flex gap-2\">\n                                                            <Input\n                                                                placeholder=\"Claim name (e.g., organization)\"\n                                                                value={key}\n                                                                onChange={(e) => {\n                                                                    const newClaims = {...samlCreateClaims};\n                                                                    delete newClaims[key];\n                                                                    newClaims[e.target.value] = value;\n                                                                    setSamlCreateClaims(newClaims);\n                                                                }}\n                                                                className=\"flex-1\"\n                                                            />\n                                                            <Input\n                                                                placeholder=\"Required value (e.g., my-company)\"\n                                                                value={value}\n                                                                onChange={(e) => {\n                                                                    setSamlCreateClaims({...samlCreateClaims, [key]: e.target.value});\n                                                                }}\n                                                                className=\"flex-1\"\n                                                            />\n                                                            <Button\n                                                                type=\"button\"\n                                                                variant=\"ghost\"\n                                                                size=\"icon\"\n                                                                onClick={() => {\n                                                                    const newClaims = {...samlCreateClaims};\n                                                                    delete newClaims[key];\n                                                                    setSamlCreateClaims(newClaims);\n                                                                }}\n                                                            >\n                                                                <Trash2 className=\"h-4 w-4\" />\n                                                            </Button>\n                                                        </div>\n                                                    ))}\n\n                                                    <Button\n                                                        type=\"button\"\n                                                        variant=\"outline\"\n                                                        size=\"sm\"\n                                                        onClick={() => {\n                                                            const newKey = `claim_${Object.keys(samlCreateClaims).length + 1}`;\n                                                            setSamlCreateClaims({...samlCreateClaims, [newKey]: ''});\n                                                        }}\n                                                    >\n                                                        <Plus className=\"h-4 w-4 mr-2\" />\n                                                        Add Claim Rule\n                                                    </Button>\n                                                </div>\n\n                                                <div className=\"text-xs text-muted-foreground space-y-1\">\n                                                    <p><strong>For OAuth:</strong> Claim names like <code>organizations</code>, <code>groups</code>, <code>role</code></p>\n                                                    <p><strong>For SAML:</strong> Attribute names from your IdP (e.g., <code>memberOf</code>, <code>department</code>)</p>\n                                                    <p><strong>Note:</strong> Matching is case-insensitive. Array claims (groups, etc.) are supported.</p>\n                                                </div>\n                                            </div>\n\n                                            <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n                                                <FormField control={samlCreateForm.control} name=\"enabled\" render={({field}) => (\n                                                    <FormItem className=\"flex flex-row items-center justify-between rounded-lg border p-3 h-full\">\n                                                        <div className=\"space-y-0.5\">\n                                                            <FormLabel>Enabled</FormLabel>\n                                                            <FormDescription>Show on login page</FormDescription>\n                                                        </div>\n                                                        <FormControl><Switch checked={field.value} onCheckedChange={field.onChange}/></FormControl>\n                                                    </FormItem>\n                                                )}/>\n                                                <FormField control={samlCreateForm.control} name=\"isDefault\" render={({field}) => (\n                                                    <FormItem className=\"flex flex-row items-center justify-between rounded-lg border p-3 h-full\">\n                                                        <div className=\"space-y-0.5\">\n                                                            <FormLabel>Default Provider</FormLabel>\n                                                            <FormDescription>Primary SAML provider</FormDescription>\n                                                        </div>\n                                                        <FormControl><Switch checked={field.value} onCheckedChange={field.onChange}/></FormControl>\n                                                    </FormItem>\n                                                )}/>\n                                            </div>\n                                        </TabsContent>\n\n                                        <TabsContent value=\"advanced\" className=\"space-y-4 mt-4\">\n                                            <FormField control={samlCreateForm.control} name=\"nameIdFormat\" render={({field}) => (\n                                                <FormItem>\n                                                    <FormLabel>Name ID Format</FormLabel>\n                                                    <FormControl><Input placeholder=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\" {...field}/></FormControl>\n                                                    <FormDescription>The SAML NameID format to request from the IdP.</FormDescription>\n                                                    <FormMessage/>\n                                                </FormItem>\n                                            )}/>\n                                            <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n                                                <FormField control={samlCreateForm.control} name=\"emailAttribute\" render={({field}) => (\n                                                    <FormItem>\n                                                        <FormLabel>Email Attribute</FormLabel>\n                                                        <FormControl><Input placeholder=\"email\" {...field}/></FormControl>\n                                                        <FormDescription>SAML attribute containing user email</FormDescription>\n                                                        <FormMessage/>\n                                                    </FormItem>\n                                                )}/>\n                                                <FormField control={samlCreateForm.control} name=\"nameAttribute\" render={({field}) => (\n                                                    <FormItem>\n                                                        <FormLabel>Name Attribute</FormLabel>\n                                                        <FormControl><Input placeholder=\"name\" {...field}/></FormControl>\n                                                        <FormDescription>SAML attribute containing user name</FormDescription>\n                                                        <FormMessage/>\n                                                    </FormItem>\n                                                )}/>\n                                            </div>\n                                        </TabsContent>\n                                    </Tabs>\n\n                                    <DialogFooter>\n                                        <Button type=\"button\" variant=\"outline\" onClick={() => setIsSAMLAddDialogOpen(false)}>Cancel</Button>\n                                        <Button type=\"submit\" disabled={addSAMLProvider.isPending}>\n                                            {addSAMLProvider.isPending ? <><Loader2 className=\"mr-2 h-4 w-4 animate-spin\"/>Adding...</> : 'Add Provider'}\n                                        </Button>\n                                    </DialogFooter>\n                                </form>\n                            </Form>\n                        </DialogContent>\n                    </Dialog>\n\n                    {/* SAML Edit Dialog */}\n                    <Dialog open={isSAMLEditDialogOpen} onOpenChange={setIsSAMLEditDialogOpen}>\n                        <DialogContent className=\"sm:max-w-2xl max-h-[90vh] overflow-y-auto\">\n                            <DialogHeader>\n                                <DialogTitle className=\"flex items-center gap-2\">\n                                    <Pencil className=\"h-5 w-5\"/>\n                                    Edit SAML Provider\n                                </DialogTitle>\n                                <DialogDescription>Update the SAML identity provider configuration.</DialogDescription>\n                            </DialogHeader>\n                            <Form {...samlEditForm}>\n                                <form onSubmit={samlEditForm.handleSubmit((d) => selectedSAMLProvider && updateSAMLProvider.mutate({...d, id: selectedSAMLProvider.id}))} className=\"space-y-6\">\n                                    <Tabs defaultValue=\"basic\" className=\"w-full\">\n                                        <TabsList className=\"grid w-full grid-cols-3\">\n                                            <TabsTrigger value=\"basic\" className=\"flex items-center gap-1\">\n                                                <Settings className=\"h-4 w-4\"/>\n                                                <span className=\"hidden sm:inline\">Configuration</span>\n                                                <span className=\"sm:hidden\">Config</span>\n                                            </TabsTrigger>\n                                            <TabsTrigger value=\"security\" className=\"flex items-center gap-1\">\n                                                <Shield className=\"h-4 w-4\"/>\n                                                <span className=\"hidden sm:inline\">Security & Access</span>\n                                                <span className=\"sm:hidden\">Security</span>\n                                            </TabsTrigger>\n                                            <TabsTrigger value=\"advanced\" className=\"flex items-center gap-1\">\n                                                <User className=\"h-4 w-4\"/>\n                                                <span className=\"hidden sm:inline\">Attribute Mapping</span>\n                                                <span className=\"sm:hidden\">Attributes</span>\n                                            </TabsTrigger>\n                                        </TabsList>\n\n                                        <TabsContent value=\"basic\" className=\"space-y-4 mt-4\">\n                                            <FormField control={samlEditForm.control} name=\"name\" render={({field}) => (\n                                                <FormItem>\n                                                    <FormLabel>Provider Name</FormLabel>\n                                                    <FormControl><Input {...field}/></FormControl>\n                                                    <FormDescription>Unique display name for this provider.</FormDescription>\n                                                    <FormMessage/>\n                                                </FormItem>\n                                            )}/>\n                                            <FormField control={samlEditForm.control} name=\"entityId\" render={({field}) => (\n                                                <FormItem>\n                                                    <FormLabel>IdP Entity ID / Issuer</FormLabel>\n                                                    <FormControl><Input {...field}/></FormControl>\n                                                    <FormDescription>The Entity ID or Issuer of the Identity Provider.</FormDescription>\n                                                    <FormMessage/>\n                                                </FormItem>\n                                            )}/>\n                                            <FormField control={samlEditForm.control} name=\"ssoUrl\" render={({field}) => (\n                                                <FormItem>\n                                                    <FormLabel>IdP SSO URL</FormLabel>\n                                                    <FormControl><Input {...field}/></FormControl>\n                                                    <FormDescription>The IdP single sign-on URL (HTTP-Redirect binding).</FormDescription>\n                                                    <FormMessage/>\n                                                </FormItem>\n                                            )}/>\n                                            <FormField control={samlEditForm.control} name=\"certificate\" render={({field}) => (\n                                                <FormItem>\n                                                    <FormLabel>IdP Signing Certificate (PEM)</FormLabel>\n                                                    <FormControl>\n                                                        <textarea\n                                                            className=\"flex min-h-[120px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 font-mono\"\n                                                            {...field}\n                                                        />\n                                                    </FormControl>\n                                                    <FormDescription>Paste the full PEM certificate from your IdP.</FormDescription>\n                                                    <FormMessage/>\n                                                </FormItem>\n                                            )}/>\n                                            <FormField control={samlEditForm.control} name=\"spEntityId\" render={({field}) => (\n                                                <FormItem>\n                                                    <FormLabel>SP Entity ID Override (optional)</FormLabel>\n                                                    <FormControl><Input placeholder=\"Defaults to metadata URL\" {...field}/></FormControl>\n                                                    <FormMessage/>\n                                                </FormItem>\n                                            )}/>\n\n                                            {/* SP URLs section */}\n                                            <div className=\"space-y-2 border rounded-md p-4 bg-muted/30\">\n                                                <p className=\"text-sm font-medium\">SP URLs (register these with your IdP)</p>\n                                                <div className=\"space-y-3\">\n                                                    <div className=\"space-y-1\">\n                                                        <div className=\"flex items-center justify-between\">\n                                                            <FormLabel className=\"text-xs text-muted-foreground\">ACS URL</FormLabel>\n                                                            <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => copySAMLUrl(`${window.location.origin}/api/auth/saml/callback/${samlEditForm.watch('name')?.toLowerCase().replace(/\\s+/g, '-') || 'provider'}`, 'ACS URL')}>\n                                                                <Copy className=\"h-3 w-3 mr-1\"/>\n                                                                Copy\n                                                            </Button>\n                                                        </div>\n                                                        <code className=\"block p-2 text-xs bg-background rounded border overflow-x-auto text-muted-foreground\">\n                                                            {`${typeof window !== 'undefined' ? window.location.origin : ''}/api/auth/saml/callback/${samlEditForm.watch('name')?.toLowerCase().replace(/\\s+/g, '-') || 'provider'}`}\n                                                        </code>\n                                                    </div>\n                                                    <div className=\"space-y-1\">\n                                                        <div className=\"flex items-center justify-between\">\n                                                            <FormLabel className=\"text-xs text-muted-foreground\">Metadata URL</FormLabel>\n                                                            <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => copySAMLUrl(`${window.location.origin}/api/auth/saml/metadata/${samlEditForm.watch('name')?.toLowerCase().replace(/\\s+/g, '-') || 'provider'}`, 'Metadata URL')}>\n                                                                <Copy className=\"h-3 w-3 mr-1\"/>\n                                                                Copy\n                                                            </Button>\n                                                        </div>\n                                                        <code className=\"block p-2 text-xs bg-background rounded border overflow-x-auto text-muted-foreground\">\n                                                            {`${typeof window !== 'undefined' ? window.location.origin : ''}/api/auth/saml/metadata/${samlEditForm.watch('name')?.toLowerCase().replace(/\\s+/g, '-') || 'provider'}`}\n                                                        </code>\n                                                    </div>\n                                                </div>\n                                            </div>\n                                        </TabsContent>\n\n                                        <TabsContent value=\"security\" className=\"space-y-4 mt-4\">\n                                            <FormField control={samlEditForm.control} name=\"allowedEmailDomains\" render={({field}) => (\n                                                <FormItem>\n                                                    <FormLabel>Allowed Email Domains</FormLabel>\n                                                    <FormControl><Input placeholder=\"example.com, company.org\" {...field}/></FormControl>\n                                                    <FormDescription>\n                                                        Restrict signups to specific email domains. Leave empty to allow all domains. Separate multiple domains with commas.\n                                                    </FormDescription>\n                                                    <FormMessage/>\n                                                </FormItem>\n                                            )}/>\n\n                                            <FormField control={samlEditForm.control} name=\"blockExistingUsers\" render={({field}) => (\n                                                <FormItem className=\"flex flex-row items-start justify-between rounded-lg border p-4 space-y-0 gap-3\">\n                                                    <div className=\"space-y-1\">\n                                                        <FormLabel className=\"text-destructive font-semibold\">Block Existing Users (Dangerous)</FormLabel>\n                                                        <FormDescription className=\"text-destructive/80\">\n                                                            ⚠️ WARNING: When enabled, users with existing accounts cannot log in with this provider, even if their domain is allowed. Only new user registration will be permitted.\n                                                        </FormDescription>\n                                                    </div>\n                                                    <FormControl>\n                                                        <Switch checked={field.value} onCheckedChange={field.onChange}/>\n                                                    </FormControl>\n                                                </FormItem>\n                                            )}/>\n\n                                            <div className=\"space-y-4 rounded-lg border p-4\">\n                                                <div className=\"space-y-2\">\n                                                    <FormLabel>Required Claims / Attributes</FormLabel>\n                                                    <FormDescription>\n                                                        Validate specific claims from OAuth userInfo or SAML attributes. Users must have matching values to sign in.\n                                                        Example: Require users to be in a specific organization or have a certain role.\n                                                    </FormDescription>\n                                                </div>\n\n                                                <div className=\"space-y-2\">\n                                                    {Object.entries(samlEditClaims).map(([key, value], index) => (\n                                                        <div key={index} className=\"flex gap-2\">\n                                                            <Input\n                                                                placeholder=\"Claim name (e.g., organization)\"\n                                                                value={key}\n                                                                onChange={(e) => {\n                                                                    const newClaims = {...samlEditClaims};\n                                                                    delete newClaims[key];\n                                                                    newClaims[e.target.value] = value;\n                                                                    setSamlEditClaims(newClaims);\n                                                                }}\n                                                                className=\"flex-1\"\n                                                            />\n                                                            <Input\n                                                                placeholder=\"Required value (e.g., my-company)\"\n                                                                value={value}\n                                                                onChange={(e) => {\n                                                                    setSamlEditClaims({...samlEditClaims, [key]: e.target.value});\n                                                                }}\n                                                                className=\"flex-1\"\n                                                            />\n                                                            <Button\n                                                                type=\"button\"\n                                                                variant=\"ghost\"\n                                                                size=\"icon\"\n                                                                onClick={() => {\n                                                                    const newClaims = {...samlEditClaims};\n                                                                    delete newClaims[key];\n                                                                    setSamlEditClaims(newClaims);\n                                                                }}\n                                                            >\n                                                                <Trash2 className=\"h-4 w-4\" />\n                                                            </Button>\n                                                        </div>\n                                                    ))}\n\n                                                    <Button\n                                                        type=\"button\"\n                                                        variant=\"outline\"\n                                                        size=\"sm\"\n                                                        onClick={() => {\n                                                            const newKey = `claim_${Object.keys(samlEditClaims).length + 1}`;\n                                                            setSamlEditClaims({...samlEditClaims, [newKey]: ''});\n                                                        }}\n                                                    >\n                                                        <Plus className=\"h-4 w-4 mr-2\" />\n                                                        Add Claim Rule\n                                                    </Button>\n                                                </div>\n\n                                                <div className=\"text-xs text-muted-foreground space-y-1\">\n                                                    <p><strong>For OAuth:</strong> Claim names like <code>organizations</code>, <code>groups</code>, <code>role</code></p>\n                                                    <p><strong>For SAML:</strong> Attribute names from your IdP (e.g., <code>memberOf</code>, <code>department</code>)</p>\n                                                    <p><strong>Note:</strong> Matching is case-insensitive. Array claims (groups, etc.) are supported.</p>\n                                                </div>\n                                            </div>\n\n                                            <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n                                                <FormField control={samlEditForm.control} name=\"enabled\" render={({field}) => (\n                                                    <FormItem className=\"flex flex-row items-center justify-between rounded-lg border p-3 h-full\">\n                                                        <div className=\"space-y-0.5\">\n                                                            <FormLabel>Enabled</FormLabel>\n                                                            <FormDescription>Show on login page</FormDescription>\n                                                        </div>\n                                                        <FormControl><Switch checked={field.value} onCheckedChange={field.onChange}/></FormControl>\n                                                    </FormItem>\n                                                )}/>\n                                                <FormField control={samlEditForm.control} name=\"isDefault\" render={({field}) => (\n                                                    <FormItem className=\"flex flex-row items-center justify-between rounded-lg border p-3 h-full\">\n                                                        <div className=\"space-y-0.5\">\n                                                            <FormLabel>Default Provider</FormLabel>\n                                                            <FormDescription>Primary SAML provider</FormDescription>\n                                                        </div>\n                                                        <FormControl><Switch checked={field.value} onCheckedChange={field.onChange}/></FormControl>\n                                                    </FormItem>\n                                                )}/>\n                                            </div>\n                                        </TabsContent>\n\n                                        <TabsContent value=\"advanced\" className=\"space-y-4 mt-4\">\n                                            <FormField control={samlEditForm.control} name=\"nameIdFormat\" render={({field}) => (\n                                                <FormItem>\n                                                    <FormLabel>Name ID Format</FormLabel>\n                                                    <FormControl><Input placeholder=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\" {...field}/></FormControl>\n                                                    <FormDescription>The SAML NameID format to request from the IdP.</FormDescription>\n                                                    <FormMessage/>\n                                                </FormItem>\n                                            )}/>\n                                            <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n                                                <FormField control={samlEditForm.control} name=\"emailAttribute\" render={({field}) => (\n                                                    <FormItem>\n                                                        <FormLabel>Email Attribute</FormLabel>\n                                                        <FormControl><Input {...field}/></FormControl>\n                                                        <FormDescription>SAML attribute containing user email</FormDescription>\n                                                        <FormMessage/>\n                                                    </FormItem>\n                                                )}/>\n                                                <FormField control={samlEditForm.control} name=\"nameAttribute\" render={({field}) => (\n                                                    <FormItem>\n                                                        <FormLabel>Name Attribute</FormLabel>\n                                                        <FormControl><Input {...field}/></FormControl>\n                                                        <FormDescription>SAML attribute containing user name</FormDescription>\n                                                        <FormMessage/>\n                                                    </FormItem>\n                                                )}/>\n                                            </div>\n                                        </TabsContent>\n                                    </Tabs>\n\n                                    <DialogFooter>\n                                        <Button type=\"button\" variant=\"outline\" onClick={() => setIsSAMLEditDialogOpen(false)}>Cancel</Button>\n                                        <Button type=\"submit\" disabled={updateSAMLProvider.isPending}>\n                                            {updateSAMLProvider.isPending ? <><Loader2 className=\"mr-2 h-4 w-4 animate-spin\"/>Updating...</> : 'Update Provider'}\n                                        </Button>\n                                    </DialogFooter>\n                                </form>\n                            </Form>\n                        </DialogContent>\n                    </Dialog>\n\n                    {/* SAML Delete Dialog */}\n                    <AlertDialog open={!!samlProviderToDelete} onOpenChange={(open) => !open && setSAMLProviderToDelete(null)}>\n                        <AlertDialogContent>\n                            <AlertDialogHeader>\n                                <AlertDialogTitle className=\"flex items-center gap-2\">\n                                    <Trash2 className=\"h-5 w-5 text-destructive\"/>\n                                    Delete {samlProviderToDelete?.name || 'SAML Provider'}\n                                </AlertDialogTitle>\n                                <AlertDialogDescription>\n                                    Are you sure you want to delete this SAML provider? This action cannot be undone.\n                                    Users who previously signed in with this provider may lose access.\n                                </AlertDialogDescription>\n                            </AlertDialogHeader>\n                            <AlertDialogFooter>\n                                <AlertDialogCancel>Cancel</AlertDialogCancel>\n                                <AlertDialogAction\n                                    onClick={() => samlProviderToDelete && deleteSAMLProvider.mutate(samlProviderToDelete.id)}\n                                    className=\"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\n                                >\n                                    {deleteSAMLProvider.isPending ? <Loader2 className=\"mr-2 h-4 w-4 animate-spin\"/> : <Trash2 className=\"mr-2 h-4 w-4\"/>}\n                                    Delete\n                                </AlertDialogAction>\n                            </AlertDialogFooter>\n                        </AlertDialogContent>\n                    </AlertDialog>\n\n                    {/* SAML Providers list */}\n                    <div className=\"flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4\">\n                        <div>\n                            <h2 className=\"text-xl font-semibold\">SAML 2.0</h2>\n                            <p className=\"text-sm text-muted-foreground\">Configure SAML identity providers for enterprise SSO</p>\n                        </div>\n                        <Button onClick={() => setIsSAMLAddDialogOpen(true)} className=\"sm:self-start\">\n                            <Plus className=\"mr-2 h-4 w-4\"/>\n                            Add SAML Provider\n                        </Button>\n                    </div>\n\n                    {/* Filter tabs */}\n                    <Tabs defaultValue=\"active\" value={activeTab} onValueChange={setActiveTab} className=\"w-full\">\n                        <TabsList className=\"grid w-full sm:w-auto grid-cols-3\">\n                            <TabsTrigger value=\"active\" className=\"flex items-center gap-1\">\n                                <Check className=\"h-4 w-4\"/>\n                                <span>Active</span>\n                            </TabsTrigger>\n                            <TabsTrigger value=\"disabled\" className=\"flex items-center gap-1\">\n                                <X className=\"h-4 w-4\"/>\n                                <span>Disabled</span>\n                            </TabsTrigger>\n                            <TabsTrigger value=\"all\" className=\"flex items-center gap-1\">\n                                <Settings className=\"h-4 w-4\"/>\n                                <span>All</span>\n                            </TabsTrigger>\n                        </TabsList>\n\n                        <TabsContent value=\"active\" className=\"mt-4\">\n                            <SAMLProvidersList\n                                providers={filteredSAMLProviders}\n                                isLoading={isSAMLLoading}\n                                onEdit={handleEditSAMLProvider}\n                                onDelete={setSAMLProviderToDelete}\n                                setIsAddDialogOpen={setIsSAMLAddDialogOpen}\n                                copySAMLUrl={copySAMLUrl}\n                            />\n                        </TabsContent>\n\n                        <TabsContent value=\"disabled\" className=\"mt-4\">\n                            <SAMLProvidersList\n                                providers={filteredSAMLProviders}\n                                isLoading={isSAMLLoading}\n                                onEdit={handleEditSAMLProvider}\n                                onDelete={setSAMLProviderToDelete}\n                                setIsAddDialogOpen={setIsSAMLAddDialogOpen}\n                                copySAMLUrl={copySAMLUrl}\n                            />\n                        </TabsContent>\n\n                        <TabsContent value=\"all\" className=\"mt-4\">\n                            <SAMLProvidersList\n                                providers={filteredSAMLProviders}\n                                isLoading={isSAMLLoading}\n                                onEdit={handleEditSAMLProvider}\n                                onDelete={setSAMLProviderToDelete}\n                                setIsAddDialogOpen={setIsSAMLAddDialogOpen}\n                                copySAMLUrl={copySAMLUrl}\n                            />\n                        </TabsContent>\n                    </Tabs>\n                </TabsContent>\n            </Tabs>\n        </motion.div>\n    );\n}\n\ninterface ProvidersListProps {\n    providers: OAuthProvider[];\n    isLoading: boolean;\n    onEdit: (provider: OAuthProvider) => void;\n    onDelete: (provider: OAuthProvider) => void;\n    setIsAddDialogOpen: (open: boolean) => void;\n}\n\nconst ProvidersList: React.FC<ProvidersListProps> = ({\n                                                         providers,\n                                                         isLoading,\n                                                         onEdit,\n                                                         onDelete,\n                                                         setIsAddDialogOpen\n                                                     }) => {\n    if (isLoading) {\n        return (\n            <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n                {Array.from({length: 3}).map((_, i) => (\n                    <Card key={i} className=\"animate-pulse\">\n                        <CardHeader className=\"pb-2\">\n                            <div className=\"flex items-center gap-3\">\n                                <div className=\"w-10 h-10 rounded-md bg-muted\"></div>\n                                <div>\n                                    <div className=\"h-6 w-24 bg-muted rounded\"></div>\n                                    <div className=\"h-4 w-32 bg-muted rounded mt-1\"></div>\n                                </div>\n                            </div>\n                        </CardHeader>\n                        <CardContent>\n                            <div className=\"h-4 w-full bg-muted rounded my-2\"></div>\n                            <div className=\"h-4 w-3/4 bg-muted rounded my-2\"></div>\n                        </CardContent>\n                        <CardFooter className=\"flex justify-between\">\n                            <div className=\"h-9 w-20 bg-muted rounded\"></div>\n                            <div className=\"h-9 w-20 bg-muted rounded\"></div>\n                        </CardFooter>\n                    </Card>\n                ))}\n            </div>\n        );\n    }\n\n    if (!providers?.length) {\n        return (\n            <Card className=\"col-span-full\">\n                <CardHeader>\n                    <CardTitle>No OAuth Providers</CardTitle>\n                    <CardDescription>\n                        Add an OAuth provider to enable single sign-on for your users with custom URL support.\n                    </CardDescription>\n                </CardHeader>\n                <CardContent className=\"flex justify-center py-8\">\n                    <div className=\"text-center\">\n                        <Fingerprint className=\"h-16 w-16 text-muted-foreground mx-auto mb-4\"/>\n                        <p className=\"text-sm text-muted-foreground max-w-md mb-6\">\n                            OAuth providers allow your users to sign in using their existing accounts from services like\n                            Microsoft, Google, GitHub, or any custom identity provider with configurable URLs.\n                        </p>\n                    </div>\n                </CardContent>\n                <CardFooter>\n                    <Button\n                        className=\"w-full\"\n                        onClick={() => setIsAddDialogOpen(true)}\n                    >\n                        <Plus className=\"mr-2 h-4 w-4\"/>\n                        Add Provider\n                    </Button>\n                </CardFooter>\n            </Card>\n        );\n    }\n\n    return (\n        <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n            <AnimatePresence>\n                {providers.map((provider: OAuthProvider) => (\n                    <motion.div\n                        key={provider.id}\n                        initial={{opacity: 0, scale: 0.95}}\n                        animate={{opacity: 1, scale: 1}}\n                        exit={{opacity: 0, scale: 0.95}}\n                        transition={{duration: 0.2}}\n                    >\n                        <Card className={!provider.enabled ? \"opacity-80 border-dashed\" : \"\"}>\n                            <CardHeader>\n                                <div className=\"flex items-center gap-3\">\n                                    <ProviderLogo providerName={provider.name}/>\n                                    <div>\n                                        <CardTitle className=\"flex items-center text-lg\">\n                                            {provider.name}\n                                            {provider.isDefault && (\n                                                <Badge variant=\"secondary\" className=\"ml-2 text-xs\">\n                                                    Default\n                                                </Badge>\n                                            )}\n                                        </CardTitle>\n                                        <CardDescription>\n                                            {provider.enabled ? (\n                                                <span className=\"flex items-center text-green-600 text-xs\">\n                                                    <Check className=\"mr-1 h-3 w-3\"/>\n                                                    Active\n                                                </span>\n                                            ) : (\n                                                <span className=\"flex items-center text-muted-foreground text-xs\">\n                                                    <X className=\"mr-1 h-3 w-3\"/>\n                                                    Disabled\n                                                </span>\n                                            )}\n                                        </CardDescription>\n                                    </div>\n                                </div>\n                            </CardHeader>\n                            <CardContent>\n                                <div className=\"space-y-3 text-sm\">\n                                    <div className=\"flex items-center\">\n                                        <KeyRound className=\"h-4 w-4 mr-2 text-muted-foreground\"/>\n                                        <span className=\"font-medium\">Client ID:</span>\n                                        <span className=\"ml-2 truncate text-muted-foreground\">\n                                            {provider.clientId.substring(0, 12)}...\n                                        </span>\n                                    </div>\n\n                                    {/* Debug URL Display */}\n                                    {/*<div className=\"space-y-1\">*/}\n                                    {/*    <div className=\"flex items-start\">*/}\n                                    {/*        <Globe className=\"h-4 w-4 mr-2 text-muted-foreground mt-0.5\" />*/}\n                                    {/*        <div className=\"flex-1 min-w-0\">*/}\n                                    {/*            <div className=\"text-xs text-muted-foreground\">Authorization URL:</div>*/}\n                                    {/*            <div className=\"text-xs font-mono bg-muted px-1 py-0.5 rounded truncate\">*/}\n                                    {/*                {provider.authorizationUrl}*/}\n                                    {/*            </div>*/}\n                                    {/*        </div>*/}\n                                    {/*    </div>*/}\n\n                                    {/*    <div className=\"flex items-start ml-6\">*/}\n                                    {/*        <div className=\"flex-1 min-w-0\">*/}\n                                    {/*            <div className=\"text-xs text-muted-foreground\">Token URL:</div>*/}\n                                    {/*            <div className=\"text-xs font-mono bg-muted px-1 py-0.5 rounded truncate\">*/}\n                                    {/*                {provider.tokenUrl}*/}\n                                    {/*            </div>*/}\n                                    {/*        </div>*/}\n                                    {/*    </div>*/}\n\n                                    {/*    <div className=\"flex items-start ml-6\">*/}\n                                    {/*        <div className=\"flex-1 min-w-0\">*/}\n                                    {/*            <div className=\"text-xs text-muted-foreground\">User Info URL:</div>*/}\n                                    {/*            <div className=\"text-xs font-mono bg-muted px-1 py-0.5 rounded truncate\">*/}\n                                    {/*                {provider.userInfoUrl}*/}\n                                    {/*            </div>*/}\n                                    {/*        </div>*/}\n                                    {/*    </div>*/}\n                                    {/*</div>*/}\n\n                                    <div className=\"flex flex-wrap gap-1\">\n                                        {provider.scopes.map((scope, index) => (\n                                            <Badge key={index} variant=\"outline\" className=\"text-xs font-normal\">\n                                                {scope}\n                                            </Badge>\n                                        ))}\n                                    </div>\n\n                                    <div className=\"pt-2\">\n                                        <button\n                                            onClick={() => {\n                                                try {\n                                                    const url = new URL(provider.authorizationUrl);\n                                                    window.open(url.origin, '_blank', 'noopener,noreferrer');\n                                                } catch {\n                                                    // Fallback if URL parsing fails\n                                                    window.open(provider.authorizationUrl, '_blank', 'noopener,noreferrer');\n                                                }\n                                            }}\n                                            className=\"inline-flex items-center text-xs text-muted-foreground hover:text-foreground transition-colors\"\n                                        >\n                                            <ExternalLink className=\"h-3 w-3 mr-1\"/>\n                                            View Provider\n                                        </button>\n                                    </div>\n                                </div>\n                            </CardContent>\n                            <CardFooter className=\"flex justify-between\">\n                                <Button\n                                    variant=\"outline\"\n                                    size=\"sm\"\n                                    onClick={() => onEdit(provider)}\n                                >\n                                    <Pencil className=\"h-4 w-4 mr-2\"/>\n                                    Edit\n                                </Button>\n                                <Button\n                                    variant=\"destructive\"\n                                    size=\"sm\"\n                                    onClick={() => onDelete(provider)}\n                                >\n                                    <Trash2 className=\"h-4 w-4 mr-2\"/>\n                                    Delete\n                                </Button>\n                            </CardFooter>\n                        </Card>\n                    </motion.div>\n                ))}\n            </AnimatePresence>\n        </div>\n    );\n};\n\ninterface SAMLProvidersListProps {\n    providers: SAMLProvider[];\n    isLoading: boolean;\n    onEdit: (provider: SAMLProvider) => void;\n    onDelete: (provider: SAMLProvider) => void;\n    setIsAddDialogOpen: (open: boolean) => void;\n    copySAMLUrl: (url: string, label: string) => void;\n}\n\nconst SAMLProvidersList: React.FC<SAMLProvidersListProps> = ({\n                                                                 providers,\n                                                                 isLoading,\n                                                                 onEdit,\n                                                                 onDelete,\n                                                                 setIsAddDialogOpen,\n                                                                 copySAMLUrl\n                                                             }) => {\n    if (isLoading) {\n        return (\n            <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n                {Array.from({length: 2}).map((_, i) => (\n                    <Card key={i} className=\"animate-pulse\">\n                        <CardHeader className=\"pb-2\">\n                            <div className=\"flex items-center gap-3\">\n                                <div className=\"w-10 h-10 rounded-md bg-muted\"></div>\n                                <div>\n                                    <div className=\"h-6 w-24 bg-muted rounded\"></div>\n                                    <div className=\"h-4 w-32 bg-muted rounded mt-1\"></div>\n                                </div>\n                            </div>\n                        </CardHeader>\n                    </Card>\n                ))}\n            </div>\n        );\n    }\n\n    if (!providers?.length) {\n        return (\n            <Card className=\"border-dashed\">\n                <CardContent className=\"pt-6 pb-6 flex flex-col items-center justify-center text-center\">\n                    <div className=\"w-16 h-16 rounded-full bg-muted/50 flex items-center justify-center mb-4\">\n                        <Fingerprint className=\"h-8 w-8 text-muted-foreground\"/>\n                    </div>\n                    <h3 className=\"text-lg font-medium mb-2\">No SAML Providers</h3>\n                    <p className=\"text-sm text-muted-foreground mb-4 max-w-sm\">\n                        Add a SAML identity provider to enable enterprise single sign-on.\n                    </p>\n                    <Button onClick={() => setIsAddDialogOpen(true)}>\n                        <Plus className=\"mr-2 h-4 w-4\"/>\n                        Add SAML Provider\n                    </Button>\n                </CardContent>\n            </Card>\n        );\n    }\n\n    return (\n        <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n            <AnimatePresence>\n                {providers.map((provider: SAMLProvider, index: number) => (\n                    <motion.div\n                        key={provider.id}\n                        initial={{opacity: 0, y: 20}}\n                        animate={{opacity: 1, y: 0}}\n                        exit={{opacity: 0, y: -20}}\n                        transition={{duration: 0.2, delay: index * 0.05}}\n                    >\n                        <Card className={!provider.enabled ? \"h-full flex flex-col opacity-80 border-dashed\" : \"h-full flex flex-col\"}>\n                            <CardHeader className=\"pb-2\">\n                                <div className=\"flex items-center gap-3\">\n                                    <ProviderLogo providerName={provider.name}/>\n                                    <div>\n                                        <CardTitle className=\"flex items-center text-base\">\n                                            {provider.name}\n                                            {provider.isDefault && (\n                                                <Badge variant=\"secondary\" className=\"ml-2 text-xs\">\n                                                    Default\n                                                </Badge>\n                                            )}\n                                        </CardTitle>\n                                        <CardDescription>\n                                            {provider.enabled ? (\n                                                <span className=\"flex items-center text-green-600 text-xs\">\n                                                    <Check className=\"mr-1 h-3 w-3\"/>\n                                                    Active\n                                                </span>\n                                            ) : (\n                                                <span className=\"flex items-center text-muted-foreground text-xs\">\n                                                    <X className=\"mr-1 h-3 w-3\"/>\n                                                    Disabled\n                                                </span>\n                                            )}\n                                        </CardDescription>\n                                    </div>\n                                </div>\n                            </CardHeader>\n                            <CardContent className=\"flex-1 space-y-2\">\n                                <div className=\"text-xs text-muted-foreground space-y-1\">\n                                    <div className=\"truncate\"><span className=\"font-medium\">SSO URL: </span>{provider.ssoUrl}</div>\n                                    <div className=\"truncate\"><span className=\"font-medium\">Entity ID: </span>{provider.entityId}</div>\n                                </div>\n                                <div className=\"flex items-center gap-2 pt-1\">\n                                    <Button\n                                        type=\"button\" variant=\"ghost\" size=\"sm\" className=\"h-6 px-2 text-xs\"\n                                        onClick={() => copySAMLUrl(`${window.location.origin}/api/auth/saml/callback/${provider.name.toLowerCase().replace(/\\s+/g, '-')}`, 'ACS URL')}\n                                    >\n                                        <Copy className=\"h-3 w-3 mr-1\"/>ACS URL\n                                    </Button>\n                                    <Button\n                                        type=\"button\" variant=\"ghost\" size=\"sm\" className=\"h-6 px-2 text-xs\"\n                                        onClick={() => copySAMLUrl(`${window.location.origin}/api/auth/saml/metadata/${provider.name.toLowerCase().replace(/\\s+/g, '-')}`, 'Metadata URL')}\n                                    >\n                                        <Copy className=\"h-3 w-3 mr-1\"/>Metadata\n                                    </Button>\n                                </div>\n                            </CardContent>\n                            <CardFooter className=\"flex justify-between\">\n                                <Button variant=\"outline\" size=\"sm\" onClick={() => onEdit(provider)}>\n                                    <Pencil className=\"h-4 w-4 mr-2\"/>Edit\n                                </Button>\n                                <Button variant=\"destructive\" size=\"sm\" onClick={() => onDelete(provider)}>\n                                    <Trash2 className=\"h-4 w-4 mr-2\"/>Delete\n                                </Button>\n                            </CardFooter>\n                        </Card>\n                    </motion.div>\n                ))}\n            </AnimatePresence>\n        </div>\n    );\n};"
  },
  {
    "path": "app/dashboard/admin/page.tsx",
    "content": "'use client'\n\nimport React from 'react'\nimport { useQuery } from '@tanstack/react-query'\nimport {\n    Users,\n    FileText,\n    Link as LinkIcon,\n    Database,\n    Activity,\n    ChevronRight\n} from 'lucide-react'\n\nimport {\n    Card,\n    CardContent,\n    CardHeader,\n    CardTitle\n} from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\n\n// Types for our dashboard data\ninterface AdminDashboardData {\n    userCount: {\n        total: number\n        admins: number\n        staff: number\n    }\n    systemHealth: {\n        databaseConnected: boolean\n        lastDataSync: string\n    }\n    changelog: {\n        totalEntries: number\n        entriesThisMonth: number\n    }\n    invitations: {\n        total: number\n        pending: number\n    }\n}\n\nexport default function AdminOverviewPage() {\n    // Fetch admin dashboard data\n    const { data: dashboardData, isLoading } = useQuery<AdminDashboardData>({\n        queryKey: ['admin-dashboard'],\n        queryFn: async () => {\n            const response = await fetch('/api/admin/dashboard')\n            if (!response.ok) throw new Error('Failed to fetch dashboard data')\n            return response.json()\n        }\n    })\n\n    // Quick Action items\n    const quickActions = [\n        {\n            title: 'Manage Users',\n            icon: Users,\n            href: '/dashboard/admin/users',\n            description: 'Add or modify user accounts'\n        },\n        {\n            title: 'Audit Logs',\n            icon: Activity,\n            href: '/dashboard/admin/audit-logs',\n            description: 'Review system activities'\n        },\n        {\n            title: 'API Keys',\n            icon: LinkIcon,\n            href: '/dashboard/admin/api-keys',\n            description: 'Manage API access'\n        }\n    ]\n\n    if (isLoading) {\n        return (\n            <div className=\"container mx-auto p-4 space-y-4\">\n                <div className=\"grid gap-4 md:grid-cols-2\">\n                    {[1, 2, 3, 4].map((_, index) => (\n                        <div\n                            key={index}\n                            className=\"h-24 bg-muted rounded-lg animate-pulse\"\n                        />\n                    ))}\n                </div>\n            </div>\n        )\n    }\n\n    return (\n        <div className=\"container mx-auto p-4 space-y-6\">\n            <div className=\"space-y-4\">\n                <h1 className=\"text-2xl font-bold\">Dashboard</h1>\n\n                {/* Key Metrics Grid */}\n                <div className=\"grid gap-4 md:grid-cols-2\">\n                    {/* Users Metric */}\n                    <Card className=\"hover:shadow-md transition-shadow\">\n                        <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n                            <CardTitle className=\"text-sm font-medium flex items-center\">\n                                <Users className=\"h-5 w-5 mr-2 text-muted-foreground\" />\n                                Users\n                            </CardTitle>\n                            <Badge variant=\"secondary\">Total</Badge>\n                        </CardHeader>\n                        <CardContent>\n                            <div className=\"text-2xl font-bold\">\n                                {dashboardData?.userCount.total ?? 0}\n                            </div>\n                            <div className=\"text-xs text-muted-foreground mt-1\">\n                                Admins: {dashboardData?.userCount.admins ?? 0}\n                                <span className=\"mx-1\">|</span>\n                                Staff: {dashboardData?.userCount.staff ?? 0}\n                            </div>\n                        </CardContent>\n                    </Card>\n\n                    {/* Invitations Metric */}\n                    <Card className=\"hover:shadow-md transition-shadow\">\n                        <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n                            <CardTitle className=\"text-sm font-medium flex items-center\">\n                                <LinkIcon className=\"h-5 w-5 mr-2 text-muted-foreground\" />\n                                Invitations\n                            </CardTitle>\n                            <Badge variant=\"secondary\">Pending</Badge>\n                        </CardHeader>\n                        <CardContent>\n                            <div className=\"text-2xl font-bold\">\n                                {dashboardData?.invitations.pending ?? 0}\n                            </div>\n                            <div className=\"text-xs text-muted-foreground mt-1\">\n                                Total: {dashboardData?.invitations.total ?? 0}\n                            </div>\n                        </CardContent>\n                    </Card>\n\n                    {/* Changelog Metric */}\n                    <Card className=\"hover:shadow-md transition-shadow\">\n                        <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n                            <CardTitle className=\"text-sm font-medium flex items-center\">\n                                <FileText className=\"h-5 w-5 mr-2 text-muted-foreground\" />\n                                Changelogs\n                            </CardTitle>\n                            <Badge variant=\"secondary\">This Month</Badge>\n                        </CardHeader>\n                        <CardContent>\n                            <div className=\"text-2xl font-bold\">\n                                {dashboardData?.changelog.entriesThisMonth ?? 0}\n                            </div>\n                            <div className=\"text-xs text-muted-foreground mt-1\">\n                                Total: {dashboardData?.changelog.totalEntries ?? 0}\n                            </div>\n                        </CardContent>\n                    </Card>\n\n                    {/* System Health Metric */}\n                    <Card className=\"hover:shadow-md transition-shadow\">\n                        <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n                            <CardTitle className=\"text-sm font-medium flex items-center\">\n                                <Database className=\"h-5 w-5 mr-2 text-muted-foreground\" />\n                                System Health\n                            </CardTitle>\n                            <Badge\n                                variant={dashboardData?.systemHealth.databaseConnected ? \"default\" : \"destructive\"}\n                            >\n                                Status\n                            </Badge>\n                        </CardHeader>\n                        <CardContent>\n                            <div className=\"text-sm font-medium\">\n                                {dashboardData?.systemHealth.databaseConnected\n                                    ? \"Operational\"\n                                    : \"Connection Issues\"}\n                            </div>\n                            <div className=\"text-xs text-muted-foreground mt-1\">\n                                Last Sync: {dashboardData?.systemHealth.lastDataSync ?? 'N/A'}\n                            </div>\n                        </CardContent>\n                    </Card>\n                </div>\n\n                {/* Quick Actions */}\n                <div className=\"space-y-4\">\n                    <h2 className=\"text-lg font-semibold\">Quick Actions</h2>\n                    <div className=\"grid gap-4 md:grid-cols-3\">\n                        {quickActions.map((action) => (\n                            <Button\n                                key={action.title}\n                                variant=\"outline\"\n                                className=\"w-full justify-between px-4 py-6 h-auto\"\n                                asChild\n                            >\n                                <a href={action.href}>\n                                    <div className=\"flex items-center\">\n                                        <action.icon className=\"h-5 w-5 mr-3 text-muted-foreground\" />\n                                        <div className=\"text-left\">\n                                            <div className=\"font-medium\">{action.title}</div>\n                                            <div className=\"text-xs text-muted-foreground\">\n                                                {action.description}\n                                            </div>\n                                        </div>\n                                    </div>\n                                    <ChevronRight className=\"h-5 w-5 text-muted-foreground\" />\n                                </a>\n                            </Button>\n                        ))}\n                    </div>\n                </div>\n            </div>\n        </div>\n    )\n}"
  },
  {
    "path": "app/dashboard/admin/requests/page.tsx",
    "content": "import { RequestManagement } from '@/components/admin/requests/Management'\n\nexport default function AdminRequestsPage() {\n    return (\n        <div className=\"container max-w-7xl p-6\">\n            <div className=\"mb-8\">\n                <h1 className=\"text-3xl font-bold tracking-tight\">Pending Requests</h1>\n                <p className=\"text-muted-foreground\">\n                    Review and manage requests for destructive actions\n                </p>\n            </div>\n            <RequestManagement />\n        </div>\n    )\n}"
  },
  {
    "path": "app/dashboard/admin/system/email/page.tsx",
    "content": "// app/dashboard/admin/system/email/page.tsx\n'use client'\n\nimport { useEffect, useState } from 'react'\nimport { useForm } from 'react-hook-form'\nimport { zodResolver } from '@hookform/resolvers/zod'\nimport { z } from 'zod'\nimport { useAuth } from '@/context/auth'\nimport { useToast } from '@/hooks/use-toast'\nimport { useRouter } from 'next/navigation'\nimport { motion } from 'framer-motion'\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n    CardFooter,\n} from '@/components/ui/card'\nimport {\n    Form,\n    FormControl,\n    FormDescription,\n    FormField,\n    FormItem,\n    FormLabel,\n    FormMessage,\n} from '@/components/ui/form'\nimport { Input } from '@/components/ui/input'\nimport { Button } from '@/components/ui/button'\nimport { Switch } from '@/components/ui/switch'\nimport {\n    AlertTriangle,\n    ArrowLeft,\n    Check,\n    ExternalLink,\n    Eye,\n    EyeOff,\n    Loader2,\n    Lock,\n    Send,\n} from 'lucide-react'\nimport {\n    Tooltip,\n    TooltipContent,\n    TooltipProvider,\n    TooltipTrigger,\n} from '@/components/ui/tooltip'\nimport {\n    Alert,\n    AlertDescription,\n    AlertTitle,\n} from '@/components/ui/alert'\n\n// Define the schema for system email configuration\nconst systemEmailSchema = z.object({\n    enablePasswordReset: z.boolean(),\n    smtpHost: z.string().min(1, 'SMTP host is required'),\n    smtpPort: z.coerce.number().int().min(1).max(65535),\n    smtpUser: z.string().default(''),\n    smtpPassword: z.string().default(''),\n    smtpSecure: z.boolean().default(true),\n    systemEmail: z.string().email('Invalid email address'),\n    testEmail: z.string().email('Invalid email address').optional().default(''),\n});\n\ntype SystemEmailConfig = z.infer<typeof systemEmailSchema>;\n\nexport default function SystemEmailConfigPage() {\n    const { user } = useAuth();\n    const { toast } = useToast();\n    const router = useRouter();\n    const [isTesting, setIsTesting] = useState(false);\n    const [isSaving, setIsSaving] = useState(false);\n    const [showPassword, setShowPassword] = useState(false);\n    const [testState, setTestState] = useState<'idle' | 'success' | 'error'>('idle');\n    const [testError, setTestError] = useState('');\n    const [isLoading, setIsLoading] = useState(true);\n\n    // Initialize form\n    const form = useForm<SystemEmailConfig>({\n        resolver: zodResolver(systemEmailSchema),\n        defaultValues: {\n            enablePasswordReset: false,\n            smtpHost: '',\n            smtpPort: 587,\n            smtpUser: '',\n            smtpPassword: '',\n            smtpSecure: true,\n            systemEmail: '',\n            testEmail: user?.email || '',\n        },\n    });\n\n    // Fetch current configuration\n    useEffect(() => {\n        const fetchConfig = async () => {\n            if (!user || user.role !== 'ADMIN') {\n                setIsLoading(false);\n                return;\n            }\n\n            setIsLoading(true);\n\n            try {\n                const response = await fetch('/api/admin/config/system-email');\n                if (!response.ok) {\n                    throw new Error('Failed to fetch system email configuration');\n                }\n\n                const data = await response.json();\n\n                // Update form with fetched data\n                form.reset({\n                    enablePasswordReset: data.enablePasswordReset || false,\n                    smtpHost: data.smtpHost || '',\n                    smtpPort: data.smtpPort || 587,\n                    smtpUser: data.smtpUser || '',\n                    smtpPassword: '', // Don't fill password from API\n                    smtpSecure: data.smtpSecure ?? true,\n                    systemEmail: data.systemEmail || '',\n                    testEmail: user?.email || '',\n                });\n\n            } catch (error) {\n                toast({\n                    title: 'Error',\n                    description: error instanceof Error ? error.message : 'Failed to load configuration',\n                    variant: 'destructive',\n                });\n            } finally {\n                setIsLoading(false);\n            }\n        };\n\n        fetchConfig();\n    }, [user, form, toast]);\n\n    // Toggle password visibility\n    const togglePasswordVisibility = () => {\n        setShowPassword(!showPassword);\n    };\n\n    // Test email configuration\n    const handleTestEmail = async () => {\n        try {\n            setIsTesting(true);\n            setTestState('idle');\n            setTestError('');\n\n            // Validate form\n            const isValid = await form.trigger(['smtpHost', 'smtpPort', 'systemEmail', 'testEmail']);\n            if (!isValid) {\n                setIsTesting(false);\n                return;\n            }\n\n            const values = form.getValues();\n\n            // Send test email\n            const response = await fetch('/api/admin/config/system-email', {\n                method: 'PATCH',\n                headers: {\n                    'Content-Type': 'application/json',\n                },\n                body: JSON.stringify({\n                    ...values,\n                }),\n            });\n\n            const data = await response.json();\n\n            if (!response.ok) {\n                setTestState('error');\n                setTestError(data.error || data.message || 'Failed to send test email');\n                toast({\n                    title: 'Test failed',\n                    description: data.error || data.message || 'Failed to send test email',\n                    variant: 'destructive',\n                });\n            } else {\n                setTestState('success');\n                toast({\n                    title: 'Test successful',\n                    description: 'Test email sent successfully',\n                });\n            }\n        } catch (error) {\n            setTestState('error');\n            setTestError(error instanceof Error ? error.message : 'Unknown error');\n            toast({\n                title: 'Test failed',\n                description: error instanceof Error ? error.message : 'Failed to send test email',\n                variant: 'destructive',\n            });\n        } finally {\n            setIsTesting(false);\n        }\n    };\n\n    // Save configuration\n    const onSubmit = async (data: SystemEmailConfig) => {\n        try {\n            setIsSaving(true);\n\n            const response = await fetch('/api/admin/config/system-email', {\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json',\n                },\n                body: JSON.stringify(data),\n            });\n\n            if (!response.ok) {\n                const errorData = await response.json();\n                throw new Error(errorData.error || 'Failed to update system email configuration');\n            }\n\n            toast({\n                title: 'Configuration updated',\n                description: 'System email configuration has been updated successfully',\n            });\n        } catch (error) {\n            toast({\n                title: 'Update failed',\n                description: error instanceof Error ? error.message : 'Failed to update configuration',\n                variant: 'destructive',\n            });\n        } finally {\n            setIsSaving(false);\n        }\n    };\n\n    // Access control\n    if (!user || user.role !== 'ADMIN') {\n        return (\n            <div className=\"flex items-center justify-center min-h-screen\">\n                <Card className=\"w-full max-w-md\">\n                    <CardHeader>\n                        <CardTitle className=\"text-destructive flex items-center gap-2\">\n                            <AlertTriangle className=\"h-5 w-5\" />\n                            Access Denied\n                        </CardTitle>\n                        <CardDescription>\n                            You do not have permission to access system email configuration.\n                        </CardDescription>\n                    </CardHeader>\n                </Card>\n            </div>\n        );\n    }\n\n    // Show loading state\n    if (isLoading) {\n        return (\n            <div className=\"container max-w-4xl px-4 md:px-6 py-8 flex items-center justify-center\">\n                <div className=\"text-center\">\n                    <Loader2 className=\"h-8 w-8 animate-spin mx-auto mb-4 text-muted-foreground\" />\n                    <p className=\"text-muted-foreground\">Loading configuration...</p>\n                </div>\n            </div>\n        );\n    }\n\n    return (\n        <div className=\"pb-16 md:pb-0\">\n            <div className=\"container max-w-4xl px-4 md:px-6 space-y-6 md:space-y-8\">\n                {/* Header section */}\n                <div className=\"sticky top-0 z-10 bg-background pt-4 pb-2 mb-4 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4\">\n                    <div className=\"flex items-center\">\n                        <Button\n                            type=\"button\"\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            className=\"mr-2\"\n                            onClick={() => router.push('/dashboard/admin/system')}\n                        >\n                            <ArrowLeft className=\"h-5 w-5\" />\n                        </Button>\n                        <div>\n                            <h1 className=\"text-2xl md:text-3xl font-bold tracking-tight\">System Email</h1>\n                            <p className=\"text-sm text-muted-foreground mt-1\">\n                                Configure system email settings\n                            </p>\n                        </div>\n                    </div>\n                </div>\n\n                {/* Main form */}\n                <motion.div\n                    initial={{ opacity: 0, y: 20 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    className=\"w-full\"\n                >\n                    <Card>\n                        <CardHeader>\n                            <CardTitle>Email Configuration</CardTitle>\n                            <CardDescription>\n                                Configure SMTP settings for system emails like password reset\n                            </CardDescription>\n                        </CardHeader>\n                        <CardContent>\n                            <Form {...form}>\n                                <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-6\">\n                                    {/* Password Reset Toggle */}\n                                    <FormField\n                                        control={form.control}\n                                        name=\"enablePasswordReset\"\n                                        render={({ field }) => (\n                                            <FormItem className=\"flex flex-row items-center justify-between rounded-lg border p-4\">\n                                                <div className=\"space-y-0.5\">\n                                                    <FormLabel className=\"text-base\">\n                                                        Enable Password Reset\n                                                    </FormLabel>\n                                                    <FormDescription>\n                                                        Allow users to reset their passwords via email\n                                                    </FormDescription>\n                                                </div>\n                                                <FormControl>\n                                                    <Switch\n                                                        checked={field.value}\n                                                        onCheckedChange={field.onChange}\n                                                    />\n                                                </FormControl>\n                                            </FormItem>\n                                        )}\n                                    />\n\n                                    {/* SMTP Server */}\n                                    <div className=\"grid gap-6\">\n                                        <div className=\"grid md:grid-cols-2 gap-4\">\n                                            <FormField\n                                                control={form.control}\n                                                name=\"smtpHost\"\n                                                render={({ field }) => (\n                                                    <FormItem>\n                                                        <FormLabel>SMTP Host</FormLabel>\n                                                        <FormControl>\n                                                            <Input placeholder=\"smtp.example.com\" {...field} />\n                                                        </FormControl>\n                                                        <FormMessage />\n                                                    </FormItem>\n                                                )}\n                                            />\n\n                                            <FormField\n                                                control={form.control}\n                                                name=\"smtpPort\"\n                                                render={({ field }) => (\n                                                    <FormItem>\n                                                        <FormLabel>SMTP Port</FormLabel>\n                                                        <FormControl>\n                                                            <Input\n                                                                type=\"number\"\n                                                                placeholder=\"587\"\n                                                                {...field}\n                                                            />\n                                                        </FormControl>\n                                                        <FormMessage />\n                                                    </FormItem>\n                                                )}\n                                            />\n                                        </div>\n\n                                        <div className=\"grid md:grid-cols-2 gap-4\">\n                                            <FormField\n                                                control={form.control}\n                                                name=\"smtpUser\"\n                                                render={({ field }) => (\n                                                    <FormItem>\n                                                        <FormLabel>SMTP Username</FormLabel>\n                                                        <FormControl>\n                                                            <Input\n                                                                placeholder=\"e.g. smtp_778as78dnasy\"\n                                                                {...field}\n                                                            />\n                                                        </FormControl>\n                                                        <FormDescription>\n                                                            Optional if your SMTP server doesn&apos;t require authentication\n                                                        </FormDescription>\n                                                        <FormMessage />\n                                                    </FormItem>\n                                                )}\n                                            />\n\n                                            <FormField\n                                                control={form.control}\n                                                name=\"smtpPassword\"\n                                                render={({ field }) => (\n                                                    <FormItem>\n                                                        <FormLabel>SMTP Password</FormLabel>\n                                                        <div className=\"relative\">\n                                                            <FormControl>\n                                                                <Input\n                                                                    type={showPassword ? \"text\" : \"password\"}\n                                                                    placeholder={field.value ? \"••••••••\" : \"Enter password\"}\n                                                                    {...field}\n                                                                    className=\"pr-10\"\n                                                                />\n                                                            </FormControl>\n                                                            <Button\n                                                                type=\"button\"\n                                                                variant=\"ghost\"\n                                                                size=\"sm\"\n                                                                className=\"absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent\"\n                                                                onClick={togglePasswordVisibility}\n                                                            >\n                                                                {showPassword ? (\n                                                                    <EyeOff className=\"h-4 w-4 text-muted-foreground\" />\n                                                                ) : (\n                                                                    <Eye className=\"h-4 w-4 text-muted-foreground\" />\n                                                                )}\n                                                            </Button>\n                                                        </div>\n                                                        <FormDescription>\n                                                            <span className=\"flex items-center gap-1\">\n                                                                <Lock className=\"h-3 w-3\" />\n                                                                Stored securely\n                                                            </span>\n                                                        </FormDescription>\n                                                        <FormMessage />\n                                                    </FormItem>\n                                                )}\n                                            />\n                                        </div>\n\n                                        <FormField\n                                            control={form.control}\n                                            name=\"smtpSecure\"\n                                            render={({ field }) => (\n                                                <FormItem className=\"flex flex-row items-center gap-3 space-y-0 rounded-md border p-4\">\n                                                    <FormControl>\n                                                        <Switch\n                                                            checked={field.value}\n                                                            onCheckedChange={field.onChange}\n                                                        />\n                                                    </FormControl>\n                                                    <div className=\"space-y-0.5\">\n                                                        <FormLabel className=\"text-base\">\n                                                            Use SSL/TLS\n                                                        </FormLabel>\n                                                        <FormDescription>\n                                                            Secure connection to mail server (recommended)\n                                                        </FormDescription>\n                                                    </div>\n                                                </FormItem>\n                                            )}\n                                        />\n\n                                        <FormField\n                                            control={form.control}\n                                            name=\"systemEmail\"\n                                            render={({ field }) => (\n                                                <FormItem>\n                                                    <FormLabel>From Email Address</FormLabel>\n                                                    <FormControl>\n                                                        <Input\n                                                            placeholder=\"system@yourdomain.com\"\n                                                            {...field}\n                                                        />\n                                                    </FormControl>\n                                                    <FormDescription>\n                                                        The email address that system emails will be sent from\n                                                    </FormDescription>\n                                                    <FormMessage />\n                                                </FormItem>\n                                            )}\n                                        />\n\n                                        {/* Test Email Field */}\n                                        <div className=\"border rounded-md p-4 space-y-4\">\n                                            <div className=\"flex flex-col sm:flex-row sm:items-center justify-between gap-2\">\n                                                <div>\n                                                    <h3 className=\"text-sm font-medium\">Test Configuration</h3>\n                                                    <p className=\"text-sm text-muted-foreground\">\n                                                        Send a test email to verify your configuration\n                                                    </p>\n                                                </div>\n\n                                                <div className=\"flex gap-2\">\n                                                    <FormField\n                                                        control={form.control}\n                                                        name=\"testEmail\"\n                                                        render={({ field }) => (\n                                                            <FormItem className=\"w-full mb-0\">\n                                                                <FormControl>\n                                                                    <Input\n                                                                        placeholder=\"test@example.com\"\n                                                                        {...field}\n                                                                        className=\"w-full sm:w-[200px]\"\n                                                                    />\n                                                                </FormControl>\n                                                                <FormMessage />\n                                                            </FormItem>\n                                                        )}\n                                                    />\n                                                    <TooltipProvider>\n                                                        <Tooltip>\n                                                            <TooltipTrigger asChild>\n                                                                <Button\n                                                                    type=\"button\"\n                                                                    variant=\"outline\"\n                                                                    onClick={handleTestEmail}\n                                                                    disabled={isTesting}\n                                                                >\n                                                                    {isTesting ? (\n                                                                        <Loader2 className=\"h-4 w-4 animate-spin\" />\n                                                                    ) : (\n                                                                        <Send className=\"h-4 w-4\" />\n                                                                    )}\n                                                                </Button>\n                                                            </TooltipTrigger>\n                                                            <TooltipContent>\n                                                                <p>Send test email</p>\n                                                            </TooltipContent>\n                                                        </Tooltip>\n                                                    </TooltipProvider>\n                                                </div>\n                                            </div>\n\n                                            {/* Test result message */}\n                                            {testState === 'success' && (\n                                                <Alert variant=\"success\" className=\"bg-green-50 dark:bg-green-950/20 border-green-200 dark:border-green-900/50\">\n                                                    <AlertTitle className=\"text-green-800 dark:text-green-400\">Success</AlertTitle>\n                                                    <AlertDescription className=\"text-green-700 dark:text-green-500\">\n                                                        Test email sent successfully. Please check your inbox.\n                                                    </AlertDescription>\n                                                </Alert>\n                                            )}\n\n                                            {testState === 'error' && (\n                                                <Alert variant=\"destructive\">\n                                                    <AlertTriangle className=\"h-4 w-4\" />\n                                                    <AlertTitle>Failed to send test email</AlertTitle>\n                                                    <AlertDescription>\n                                                        {testError || 'There was an error sending the test email. Please check your configuration.'}\n                                                    </AlertDescription>\n                                                </Alert>\n                                            )}\n                                        </div>\n                                    </div>\n\n                                    <Alert hasIcon={false} className=\"bg-amber-50 dark:bg-amber-950/20 border-amber-200 dark:border-amber-900/50\">\n                                        <AlertTitle className=\"text-amber-800 dark:text-amber-400\">Important</AlertTitle>\n                                        <AlertDescription className=\"text-amber-700 dark:text-amber-500\">\n                                            Email configuration is required for password reset functionality. It&apos;s recommended to test your configuration before enabling this feature.\n                                        </AlertDescription>\n                                    </Alert>\n\n                                    <div className=\"flex justify-end\">\n                                        <Button\n                                            type=\"submit\"\n                                            disabled={isSaving || !form.formState.isDirty}\n                                        >\n                                            {isSaving ? (\n                                                <>\n                                                    <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                                                    Saving...\n                                                </>\n                                            ) : (\n                                                <>\n                                                    <Check className=\"mr-2 h-4 w-4\" />\n                                                    Save Configuration\n                                                </>\n                                            )}\n                                        </Button>\n                                    </div>\n                                </form>\n                            </Form>\n                        </CardContent>\n                        <CardFooter className=\"flex flex-col items-start border-t bg-muted/50 px-6 py-4\">\n                            <h3 className=\"text-sm font-medium\">Resources</h3>\n                            <ul className=\"text-sm text-muted-foreground mt-2 space-y-1\">\n                                <li>\n                                    <a\n                                        href=\"https://nodemailer.com/smtp/\"\n                                        target=\"_blank\"\n                                        rel=\"noopener noreferrer\"\n                                        className=\"flex items-center hover:text-primary\"\n                                    >\n                                        Nodemailer SMTP Configuration\n                                        <ExternalLink className=\"ml-1 h-3 w-3\" />\n                                    </a>\n                                </li>\n                                <li>\n                                    <a\n                                        href=\"https://support.google.com/mail/answer/7126229\"\n                                        target=\"_blank\"\n                                        rel=\"noopener noreferrer\"\n                                        className=\"flex items-center hover:text-primary\"\n                                    >\n                                        Gmail SMTP Settings\n                                        <ExternalLink className=\"ml-1 h-3 w-3\" />\n                                    </a>\n                                </li>\n                            </ul>\n                        </CardFooter>\n                    </Card>\n                </motion.div>\n            </div>\n        </div>\n    );\n}"
  },
  {
    "path": "app/dashboard/admin/system/page.tsx",
    "content": "'use client'\n\nimport React, {useState} from 'react'\nimport {useQuery, useMutation} from '@tanstack/react-query'\nimport {useAuth} from '@/context/auth'\nimport {useToast} from '@/hooks/use-toast'\nimport {motion} from 'framer-motion'\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card'\nimport {\n    Tabs,\n    TabsContent,\n    TabsList,\n    TabsTrigger,\n} from '@/components/ui/tabs'\nimport {\n    Form,\n    FormControl,\n    FormDescription,\n    FormField,\n    FormItem,\n    FormLabel,\n    FormMessage,\n} from '@/components/ui/form'\nimport {Input} from '@/components/ui/input'\nimport {Button} from '@/components/ui/button'\nimport {Switch} from '@/components/ui/switch'\nimport {Separator} from '@/components/ui/separator'\nimport {\n    AlertTriangle,\n    Check,\n    Loader2,\n    Settings,\n    Mail,\n    Bell,\n    BarChart4,\n    Activity,\n    Shield,\n    CheckCircle,\n    XCircle,\n    Key,\n    KeyRound,\n    ExternalLink,\n    RefreshCw,\n    AlertCircle,\n    Copy,\n    ArrowRight,\n    ArrowLeft,\n    ShieldCheck,\n    Globe,\n    Calendar,\n} from 'lucide-react'\nimport {zodResolver} from '@hookform/resolvers/zod'\nimport {useForm} from 'react-hook-form'\nimport * as z from 'zod'\nimport Link from \"next/link\"\nimport {appInfo} from \"@/lib/app-info\";\nimport {RadioGroup, RadioGroupItem} from '@/components/ui/radio-group'\nimport {SearchableSelect} from '@/components/ui/searchable-select'\nimport {Badge} from '@/components/ui/badge'\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogFooter,\n    DialogHeader,\n    DialogTitle,\n} from '@/components/ui/dialog'\nimport {SlackLogo} from \"@/lib/services/slack/logo\";\nimport {TIMEZONES, getTimezonesByRegion} from \"@/lib/constants/timezones\";\n\nfunction buildConfigSchema(sponsored: boolean) {\n    return z.object({\n        defaultInvitationExpiry: z.number().min(1).max(30),\n        requireApprovalForChangelogs: z.boolean(),\n        maxChangelogEntriesPerProject: z.number().min(10).max(sponsored ? 999999 : 10000),\n        enableAnalytics: z.boolean(),\n        enableNotifications: z.boolean(),\n        allowTelemetry: z.enum(['prompt', 'enabled', 'disabled']),\n        adminOnlyApiKeyCreation: z.boolean(),\n        timezone: z.string().min(1).max(100),\n        allowUserTimezone: z.boolean(),\n        panelIpWhitelistEnabled: z.boolean(),\n        panelIpWhitelist: z.array(z.string()),\n    })\n}\n\ntype SystemConfig = {\n    defaultInvitationExpiry: number\n    requireApprovalForChangelogs: boolean\n    maxChangelogEntriesPerProject: number\n    enableAnalytics: boolean\n    enableNotifications: boolean\n    allowTelemetry: 'prompt' | 'enabled' | 'disabled'\n    adminOnlyApiKeyCreation: boolean\n    timezone: string\n    allowUserTimezone: boolean\n    customDateTemplates?: { format: string; label: string }[] | null\n    sponsorActive?: boolean\n    telemetryInstanceId?: string\n    panelIpWhitelistEnabled: boolean\n    panelIpWhitelist: string[]\n    nginxAgentConfigured?: boolean\n}\n\nexport default function SystemConfigPage() {\n    const {user} = useAuth()\n    const {toast} = useToast()\n    const [licenseKeyInput, setLicenseKeyInput] = useState('')\n    const [licenseLoading, setLicenseLoading] = useState(false)\n    const [showNameModal, setShowNameModal] = useState(false)\n    const [showDeviceLimitModal, setShowDeviceLimitModal] = useState(false)\n    const [instanceNameInput, setInstanceNameInput] = useState('')\n    const [deviceLimitRefreshing, setDeviceLimitRefreshing] = useState(false)\n    const [activationStep, setActivationStep] = useState<'key' | 'challenge' | 'confirm'>('key')\n    const [challengeCode, setChallengeCode] = useState('')\n    const [challengeId, setChallengeId] = useState('')\n    const [responseCodeInput, setResponseCodeInput] = useState('')\n\n    const {data: config, isLoading, refetch} = useQuery<SystemConfig>({\n        queryKey: ['system-config'],\n        queryFn: async () => {\n            const response = await fetch('/api/admin/config')\n            if (!response.ok) throw new Error('Failed to fetch system configuration')\n            return response.json()\n        },\n    })\n\n    const {data: licenseStatus, refetch: refetchLicense} = useQuery<{\n        active: boolean,\n        features: string[],\n        connectionFailed?: boolean\n    }>({\n        queryKey: ['license-status'],\n        queryFn: async () => {\n            const response = await fetch('/api/admin/sponsor')\n            if (!response.ok) return {active: false, features: []}\n            return response.json()\n        },\n    })\n\n    const isLicensed = licenseStatus?.active === true\n    const connectionFailed = licenseStatus?.connectionFailed === true\n    const currentSchema = buildConfigSchema(isLicensed)\n    const form = useForm<SystemConfig>({\n        resolver: zodResolver(currentSchema),\n        defaultValues: {\n            defaultInvitationExpiry: 7,\n            requireApprovalForChangelogs: true,\n            maxChangelogEntriesPerProject: 100,\n            enableAnalytics: true,\n            enableNotifications: true,\n            allowTelemetry: 'prompt' as const,\n            timezone: 'UTC',\n            allowUserTimezone: true,\n            panelIpWhitelistEnabled: false,\n            panelIpWhitelist: [],\n        },\n        values: config ? {\n            defaultInvitationExpiry: config.defaultInvitationExpiry,\n            requireApprovalForChangelogs: config.requireApprovalForChangelogs,\n            maxChangelogEntriesPerProject: config.maxChangelogEntriesPerProject,\n            enableAnalytics: config.enableAnalytics,\n            enableNotifications: config.enableNotifications,\n            allowTelemetry: config.allowTelemetry,\n            adminOnlyApiKeyCreation: config.adminOnlyApiKeyCreation,\n            timezone: config.timezone,\n            allowUserTimezone: config.allowUserTimezone,\n            panelIpWhitelistEnabled: config.panelIpWhitelistEnabled ?? false,\n            panelIpWhitelist: config.panelIpWhitelist ?? [],\n        } : undefined,\n    })\n\n    const updateConfig = useMutation({\n        mutationFn: async (data: SystemConfig) => {\n            const response = await fetch('/api/admin/config', {\n                method: 'PATCH',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify(data),\n            })\n            if (!response.ok) throw new Error('Failed to update system configuration')\n            return response.json()\n        },\n        onSuccess: () => {\n            toast({\n                title: 'Configuration Updated',\n                description: 'System configuration has been successfully updated.',\n            })\n            refetch()\n        },\n        onError: (error) => {\n            toast({\n                title: 'Update Failed',\n                description: error.message,\n                variant: 'destructive',\n            })\n        },\n    })\n\n    const handleActivateClick = async () => {\n        if (!licenseKeyInput.trim()) return\n        setLicenseLoading(true)\n        try {\n            const response = await fetch('/api/admin/sponsor', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({\n                    licenseKey: licenseKeyInput.trim(),\n                    mode: 'challenge',\n                }),\n            })\n            const data = await response.json()\n            if (!response.ok) {\n                toast({\n                    title: 'Challenge Failed',\n                    description: data.error || 'Could not initiate activation',\n                    variant: 'destructive'\n                })\n            } else {\n                setChallengeId(data.challenge_id)\n                setChallengeCode(data.challenge_code)\n                setActivationStep('challenge')\n            }\n        } catch {\n            toast({title: 'Error', description: 'Unable to reach the licensing server.', variant: 'destructive'})\n        } finally {\n            setLicenseLoading(false)\n        }\n    }\n\n    const handleConfirmChallenge = () => {\n        if (!responseCodeInput.trim()) return\n        setShowNameModal(true)\n    }\n\n    const handleActivateLicense = async () => {\n        setShowNameModal(false)\n        setLicenseLoading(true)\n        try {\n            const response = await fetch('/api/admin/sponsor', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({\n                    licenseKey: licenseKeyInput.trim(),\n                    challengeId,\n                    responseCode: responseCodeInput.trim().toUpperCase(),\n                    instanceName: instanceNameInput.trim() || undefined,\n                    mode: 'confirm',\n                }),\n            })\n            const data = await response.json()\n            if (!response.ok) {\n                if (response.status === 400 && data.error?.includes('Activation limit')) {\n                    setShowDeviceLimitModal(true)\n                } else {\n                    toast({\n                        title: 'Activation Failed',\n                        description: data.error || 'Could not activate license',\n                        variant: 'destructive'\n                    })\n                }\n            } else {\n                toast({title: 'Activated', description: 'Successfully activated. Thanks for your support!'})\n                setLicenseKeyInput('')\n                setInstanceNameInput('')\n                setActivationStep('key')\n                setChallengeCode('')\n                setChallengeId('')\n                setResponseCodeInput('')\n                refetchLicense()\n                refetch()\n            }\n        } catch {\n            toast({\n                title: 'Activation Failed',\n                description: 'Unable to reach the licensing server.',\n                variant: 'destructive'\n            })\n        } finally {\n            setLicenseLoading(false)\n        }\n    }\n\n    const handleRefreshDeviceStatus = async () => {\n        setDeviceLimitRefreshing(true)\n        try {\n            // Re-attempt activation — if a slot was freed, it will succeed\n            const response = await fetch('/api/admin/sponsor', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({\n                    licenseKey: licenseKeyInput.trim(),\n                    instanceName: instanceNameInput.trim() || undefined,\n                }),\n            })\n            const data = await response.json()\n            if (response.ok) {\n                setShowDeviceLimitModal(false)\n                toast({title: 'Activated', description: 'Successfully activated. Thanks for your support!'})\n                setLicenseKeyInput('')\n                setInstanceNameInput('')\n                refetchLicense()\n                refetch()\n            } else {\n                toast({\n                    title: 'Still at limit',\n                    description: 'No slots have been freed yet. Remove an instance from the license portal first.',\n                    variant: 'destructive'\n                })\n            }\n        } catch {\n            toast({title: 'Error', description: 'Unable to reach the licensing server.', variant: 'destructive'})\n        } finally {\n            setDeviceLimitRefreshing(false)\n        }\n    }\n\n    const handleDeactivateLicense = async () => {\n        setLicenseLoading(true)\n        try {\n            const response = await fetch('/api/admin/sponsor', {method: 'DELETE'})\n            if (response.ok) {\n                toast({title: 'Deactivated', description: 'Successfully deactivated.'})\n                refetchLicense()\n                refetch()\n            }\n        } catch {\n            toast({title: 'Error', description: 'Failed to deactivate license.', variant: 'destructive'})\n        } finally {\n            setLicenseLoading(false)\n        }\n    }\n\n    if (!user || user.role !== 'ADMIN') {\n        return (\n            <div className=\"flex items-center justify-center min-h-screen\">\n                <Card className=\"w-full max-w-md\">\n                    <CardHeader>\n                        <CardTitle className=\"text-destructive flex items-center gap-2\">\n                            <AlertTriangle className=\"h-5 w-5\"/>\n                            Access Denied\n                        </CardTitle>\n                        <CardDescription>\n                            You do not have permission to access system configuration.\n                        </CardDescription>\n                    </CardHeader>\n                </Card>\n            </div>\n        )\n    }\n\n    const cardVariants = {\n        hidden: {opacity: 0, y: 20},\n        visible: {opacity: 1, y: 0}\n    }\n\n    const getTelemetryDescription = (value: string) => {\n        switch (value) {\n            case 'enabled':\n                return 'Anonymous usage data is being collected to help improve Changerawr'\n            case 'disabled':\n                return 'No usage data is being collected'\n            case 'prompt':\n            default:\n                return 'Users will be prompted to choose their telemetry preference'\n        }\n    }\n\n    const currentTelemetryValue = form.watch('allowTelemetry')\n\n    return (\n        <>\n            <motion.div\n                initial={{opacity: 0, y: 20}}\n                animate={{opacity: 1, y: 0}}\n                className=\"container max-w-5xl p-6\"\n            >\n                <Card className=\"shadow-md\">\n                    <CardHeader>\n                        <div className=\"flex items-center justify-between\">\n                            <div className=\"flex items-center gap-2\">\n                                <Settings className=\"h-6 w-6 text-primary\"/>\n                                <CardTitle>System Configuration</CardTitle>\n                            </div>\n                        </div>\n                        <CardDescription>\n                            Manage global system settings and defaults\n                        </CardDescription>\n                    </CardHeader>\n                    <CardContent>\n                        {isLoading ? (\n                            <div className=\"flex items-center justify-center py-6\">\n                                <Loader2 className=\"h-6 w-6 animate-spin\"/>\n                            </div>\n                        ) : (\n                            <Tabs defaultValue=\"general\" className=\"w-full\">\n                                <TabsList className=\"grid grid-cols-6 mb-6\">\n                                    <TabsTrigger value=\"general\">General</TabsTrigger>\n                                    <TabsTrigger value=\"features\">Features</TabsTrigger>\n                                    <TabsTrigger value=\"privacy\">Privacy</TabsTrigger>\n                                    <TabsTrigger value=\"integrations\">Integrations</TabsTrigger>\n                                    <TabsTrigger value=\"security\">Security</TabsTrigger>\n                                    <TabsTrigger value=\"license\">License</TabsTrigger>\n                                </TabsList>\n\n                                <Form {...form}>\n                                    <form onSubmit={form.handleSubmit((data) => updateConfig.mutate(data))}>\n                                        <TabsContent value=\"general\" className=\"space-y-6\">\n                                            <div className=\"grid gap-6\">\n                                                <FormField\n                                                    control={form.control}\n                                                    name=\"defaultInvitationExpiry\"\n                                                    render={({field}) => (\n                                                        <FormItem>\n                                                            <FormLabel>Default Invitation Expiry (days)</FormLabel>\n                                                            <FormControl>\n                                                                <Input\n                                                                    type=\"number\"\n                                                                    {...field}\n                                                                    onChange={(e) => field.onChange(Number(e.target.value))}\n                                                                />\n                                                            </FormControl>\n                                                            <FormDescription>\n                                                                Number of days before invitation links expire\n                                                            </FormDescription>\n                                                            <FormMessage/>\n                                                        </FormItem>\n                                                    )}\n                                                />\n\n                                                <FormField\n                                                    control={form.control}\n                                                    name=\"maxChangelogEntriesPerProject\"\n                                                    render={({field}) => (\n                                                        <FormItem>\n                                                            <FormLabel className=\"flex items-center gap-2\">\n                                                                Max Changelog Entries per Project\n                                                                {isLicensed && (\n                                                                    <Badge variant=\"default\"\n                                                                           className=\"text-xs\">Unlimited</Badge>\n                                                                )}\n                                                            </FormLabel>\n                                                            <FormControl>\n                                                                <Input\n                                                                    type=\"number\"\n                                                                    {...field}\n                                                                    onChange={(e) => field.onChange(Number(e.target.value))}\n                                                                />\n                                                            </FormControl>\n                                                            <FormDescription>\n                                                                {isLicensed\n                                                                    ? 'Unlimited entries enabled. This value is used as a soft guideline.'\n                                                                    : 'Maximum number of changelog entries allowed per project (10 - 10,000)'}\n                                                            </FormDescription>\n                                                            <FormMessage/>\n                                                        </FormItem>\n                                                    )}\n                                                />\n                                                <Separator/>\n\n                                                <FormField\n                                                    control={form.control}\n                                                    name=\"timezone\"\n                                                    render={({field}) => (\n                                                        <FormItem>\n                                                            <FormLabel className=\"flex items-center gap-2\">\n                                                                <Globe className=\"h-4 w-4 text-muted-foreground\"/>\n                                                                Timezone\n                                                            </FormLabel>\n                                                            <SearchableSelect\n                                                                value={field.value}\n                                                                onValueChange={field.onChange}\n                                                                placeholder=\"Select timezone\"\n                                                                searchPlaceholder=\"Search timezones...\"\n                                                                groups={Object.entries(getTimezonesByRegion()).map(([region, tzs]) => ({\n                                                                    heading: region,\n                                                                    items: tzs.map(tz => ({\n                                                                        value: tz.value,\n                                                                        label: `${tz.label} (${tz.value})`,\n                                                                        searchValue: `${tz.label} ${tz.value} ${region}`,\n                                                                    })),\n                                                                }))}\n                                                            />\n                                                            <FormDescription>\n                                                                Timezone used for date-based version templates and\n                                                                scheduling\n                                                            </FormDescription>\n                                                            <FormMessage/>\n                                                        </FormItem>\n                                                    )}\n                                                />\n\n                                                <FormField\n                                                    control={form.control}\n                                                    name=\"allowUserTimezone\"\n                                                    render={({field}) => (\n                                                        <FormItem\n                                                            className=\"flex flex-row items-center justify-between rounded-lg border p-4\">\n                                                            <div className=\"space-y-0.5\">\n                                                                <FormLabel className=\"text-base\">\n                                                                    Allow User Timezone Override\n                                                                </FormLabel>\n                                                                <FormDescription>\n                                                                    Let users set their own timezone in their profile\n                                                                    settings\n                                                                </FormDescription>\n                                                            </div>\n                                                            <FormControl>\n                                                                <Switch\n                                                                    checked={field.value}\n                                                                    onCheckedChange={field.onChange}\n                                                                />\n                                                            </FormControl>\n                                                        </FormItem>\n                                                    )}\n                                                />\n\n                                                <Separator/>\n\n                                                {/* Version Templates Link */}\n                                                <Link\n                                                    href=\"/dashboard/admin/system/templates\"\n                                                    className=\"flex items-center justify-between rounded-lg border p-4 hover:bg-accent/50 transition-colors\"\n                                                >\n                                                    <div className=\"space-y-0.5\">\n                                                        <div className=\"flex items-center gap-2\">\n                                                            <Calendar className=\"h-4 w-4 text-muted-foreground\"/>\n                                                            <span\n                                                                className=\"text-sm font-medium\">Version Templates</span>\n                                                        </div>\n                                                        <p className=\"text-xs text-muted-foreground\">\n                                                            Configure custom version format templates with date and\n                                                            version variables\n                                                        </p>\n                                                    </div>\n                                                    <ArrowRight className=\"h-4 w-4 text-muted-foreground\"/>\n                                                </Link>\n                                            </div>\n                                        </TabsContent>\n\n                                        <TabsContent value=\"features\" className=\"space-y-6\">\n                                            <motion.div\n                                                variants={cardVariants}\n                                                initial=\"hidden\"\n                                                animate=\"visible\"\n                                                transition={{staggerChildren: 0.1}}\n                                                className=\"space-y-4\"\n                                            >\n                                                <FormField\n                                                    control={form.control}\n                                                    name=\"requireApprovalForChangelogs\"\n                                                    render={({field}) => (\n                                                        <motion.div variants={cardVariants}>\n                                                            <FormItem\n                                                                className=\"flex flex-row items-center justify-between rounded-lg border p-4\">\n                                                                <div className=\"space-y-0.5\">\n                                                                    <FormLabel className=\"text-base\">\n                                                                        Require Approval for Changelogs\n                                                                    </FormLabel>\n                                                                    <FormDescription>\n                                                                        Require admin approval before publishing\n                                                                        changelog\n                                                                        entries\n                                                                    </FormDescription>\n                                                                </div>\n                                                                <FormControl>\n                                                                    <Switch\n                                                                        checked={field.value}\n                                                                        onCheckedChange={field.onChange}\n                                                                    />\n                                                                </FormControl>\n                                                            </FormItem>\n                                                        </motion.div>\n                                                    )}\n                                                />\n\n                                                <FormField\n                                                    control={form.control}\n                                                    name=\"enableAnalytics\"\n                                                    render={({field}) => (\n                                                        <motion.div variants={cardVariants}>\n                                                            <FormItem\n                                                                className=\"flex flex-row items-center justify-between rounded-lg border p-4\">\n                                                                <div className=\"flex gap-2\">\n                                                                    <BarChart4\n                                                                        className=\"h-5 w-5 text-muted-foreground mt-0.5\"/>\n                                                                    <div className=\"space-y-0.5\">\n                                                                        <FormLabel className=\"text-base\">\n                                                                            Enable Analytics\n                                                                        </FormLabel>\n                                                                        <FormDescription>\n                                                                            Collect and display analytics for changelog\n                                                                            entries\n                                                                        </FormDescription>\n                                                                    </div>\n                                                                </div>\n                                                                <FormControl>\n                                                                    <Switch\n                                                                        checked={field.value}\n                                                                        onCheckedChange={field.onChange}\n                                                                    />\n                                                                </FormControl>\n                                                            </FormItem>\n                                                        </motion.div>\n                                                    )}\n                                                />\n\n                                                <FormField\n                                                    control={form.control}\n                                                    name=\"enableNotifications\"\n                                                    render={({field}) => (\n                                                        <motion.div variants={cardVariants}>\n                                                            <FormItem\n                                                                className=\"flex flex-row items-center justify-between rounded-lg border p-4\">\n                                                                <div className=\"flex gap-2\">\n                                                                    <Bell\n                                                                        className=\"h-5 w-5 text-muted-foreground mt-0.5\"/>\n                                                                    <div className=\"space-y-0.5\">\n                                                                        <FormLabel className=\"text-base\">\n                                                                            Enable Notifications\n                                                                        </FormLabel>\n                                                                        <FormDescription>\n                                                                            Send notifications for internal actions (\n                                                                            e.g.\n                                                                            approvals )\n                                                                        </FormDescription>\n                                                                    </div>\n                                                                </div>\n                                                                <FormControl>\n                                                                    <Switch\n                                                                        checked={field.value}\n                                                                        onCheckedChange={field.onChange}\n                                                                    />\n                                                                </FormControl>\n                                                            </FormItem>\n                                                        </motion.div>\n                                                    )}\n                                                />\n\n                                                <FormField\n                                                    control={form.control}\n                                                    name=\"adminOnlyApiKeyCreation\"\n                                                    render={({field}) => (\n                                                        <motion.div variants={cardVariants}>\n                                                            <FormItem\n                                                                className=\"flex flex-row items-center justify-between rounded-lg border p-4\">\n                                                                <div className=\"flex gap-2\">\n                                                                    <Key\n                                                                        className=\"h-5 w-5 text-muted-foreground mt-0.5\"/>\n                                                                    <div className=\"space-y-0.5\">\n                                                                        <FormLabel className=\"text-base\">\n                                                                            Admin-Only API Key Creation\n                                                                        </FormLabel>\n                                                                        <FormDescription>\n                                                                            Restrict API key creation to administrators\n                                                                            only\n                                                                        </FormDescription>\n                                                                    </div>\n                                                                </div>\n                                                                <FormControl>\n                                                                    <Switch\n                                                                        checked={field.value}\n                                                                        onCheckedChange={field.onChange}\n                                                                    />\n                                                                </FormControl>\n                                                            </FormItem>\n                                                        </motion.div>\n                                                    )}\n                                                />\n                                            </motion.div>\n                                        </TabsContent>\n\n                                        <TabsContent value=\"privacy\" className=\"space-y-6\">\n                                            <div className=\"space-y-4\">\n                                                <FormField\n                                                    control={form.control}\n                                                    name=\"allowTelemetry\"\n                                                    render={({field}) => (\n                                                        <Card className=\"border-2\">\n                                                            <CardHeader>\n                                                                <div className=\"flex items-center gap-3\">\n                                                                    <div className=\"p-2 bg-primary/10 rounded-lg\">\n                                                                        <Activity className=\"h-5 w-5 text-primary\"/>\n                                                                    </div>\n                                                                    <div>\n                                                                        <CardTitle className=\"text-lg\">Telemetry\n                                                                            Settings</CardTitle>\n                                                                        <CardDescription>\n                                                                            Configure how usage data is collected from\n                                                                            your\n                                                                            Changerawr instance\n                                                                        </CardDescription>\n                                                                    </div>\n                                                                </div>\n                                                            </CardHeader>\n                                                            <CardContent className=\"space-y-6\">\n                                                                <FormItem>\n                                                                    <FormLabel className=\"text-base font-semibold\">Telemetry\n                                                                        Mode</FormLabel>\n                                                                    <FormControl>\n                                                                        <RadioGroup\n                                                                            value={field.value}\n                                                                            onValueChange={field.onChange}\n                                                                            className=\"space-y-4\"\n                                                                        >\n                                                                            {process.env.NODE_ENV === 'development' && (\n                                                                                <div className=\"relative\">\n                                                                                    <div\n                                                                                        className=\"flex items-start space-x-3 p-4 border rounded-lg hover:bg-muted/50 transition-colors\">\n                                                                                        <RadioGroupItem value=\"prompt\"\n                                                                                                        id=\"telemetry-prompt\"\n                                                                                                        className=\"mt-1\"/>\n                                                                                        <div\n                                                                                            className=\"flex-1 space-y-2\">\n                                                                                            <div\n                                                                                                className=\"flex items-center gap-2\">\n                                                                                                <label\n                                                                                                    htmlFor=\"telemetry-prompt\"\n                                                                                                    className=\"text-sm font-medium cursor-pointer\">\n                                                                                                    Prompt users\n                                                                                                </label>\n                                                                                                <Badge\n                                                                                                    variant=\"secondary\"\n                                                                                                    className=\"text-xs\">\n                                                                                                    Development Only\n                                                                                                </Badge>\n                                                                                            </div>\n                                                                                            <p className=\"text-xs text-muted-foreground\">\n                                                                                                Show a modal asking\n                                                                                                users to\n                                                                                                choose whether to enable\n                                                                                                telemetry\n                                                                                            </p>\n                                                                                        </div>\n                                                                                    </div>\n                                                                                </div>\n                                                                            )}\n\n                                                                            <div className=\"relative\">\n                                                                                <div\n                                                                                    className=\"flex items-start space-x-3 p-4 border rounded-lg hover:bg-muted/50 transition-colors\">\n                                                                                    <RadioGroupItem value=\"enabled\"\n                                                                                                    id=\"telemetry-enabled\"\n                                                                                                    className=\"mt-1\"/>\n                                                                                    <div className=\"flex-1 space-y-2\">\n                                                                                        <div\n                                                                                            className=\"flex items-center gap-2\">\n                                                                                            <CheckCircle\n                                                                                                className=\"w-4 h-4 text-green-600\"/>\n                                                                                            <label\n                                                                                                htmlFor=\"telemetry-enabled\"\n                                                                                                className=\"text-sm font-medium cursor-pointer\">\n                                                                                                Always enabled\n                                                                                            </label>\n                                                                                            <Badge variant=\"default\"\n                                                                                                   className=\"text-xs\">\n                                                                                                Recommended\n                                                                                            </Badge>\n                                                                                        </div>\n                                                                                        <p className=\"text-xs text-muted-foreground\">\n                                                                                            Automatically collect\n                                                                                            anonymous\n                                                                                            usage data to help improve\n                                                                                            the\n                                                                                            product\n                                                                                        </p>\n                                                                                    </div>\n                                                                                </div>\n                                                                            </div>\n\n                                                                            <div className=\"relative\">\n                                                                                <div\n                                                                                    className=\"flex items-start space-x-3 p-4 border rounded-lg hover:bg-muted/50 transition-colors\">\n                                                                                    <RadioGroupItem value=\"disabled\"\n                                                                                                    id=\"telemetry-disabled\"\n                                                                                                    className=\"mt-1\"/>\n                                                                                    <div className=\"flex-1 space-y-2\">\n                                                                                        <div\n                                                                                            className=\"flex items-center gap-2\">\n                                                                                            <XCircle\n                                                                                                className=\"w-4 h-4 text-red-600\"/>\n                                                                                            <label\n                                                                                                htmlFor=\"telemetry-disabled\"\n                                                                                                className=\"text-sm font-medium cursor-pointer\">\n                                                                                                Always disabled\n                                                                                            </label>\n                                                                                        </div>\n                                                                                        <p className=\"text-xs text-muted-foreground\">\n                                                                                            Completely disable telemetry\n                                                                                            data collection\n                                                                                        </p>\n                                                                                    </div>\n                                                                                </div>\n                                                                            </div>\n                                                                        </RadioGroup>\n                                                                    </FormControl>\n                                                                    <FormDescription className=\"mt-4\">\n                                                                        {getTelemetryDescription(currentTelemetryValue)}\n                                                                    </FormDescription>\n                                                                    <FormMessage/>\n                                                                </FormItem>\n\n                                                                <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n                                                                    <Card\n                                                                        className=\"border-green-200 bg-green-50/50 dark:border-green-800 dark:bg-green-950/20\">\n                                                                        <CardContent className=\"pt-4\">\n                                                                            <div className=\"flex items-start gap-3\">\n                                                                                <CheckCircle\n                                                                                    className=\"w-5 h-5 text-green-600 dark:text-green-400 mt-0.5 flex-shrink-0\"/>\n                                                                                <div className=\"space-y-3\">\n                                                                                    <h4 className=\"font-medium text-green-900 dark:text-green-100\">\n                                                                                        What we collect\n                                                                                    </h4>\n                                                                                    <div className=\"space-y-2\">\n                                                                                        <Badge variant=\"outline\"\n                                                                                               className=\"text-xs bg-green-100 border-green-300 text-green-800 dark:bg-green-950 dark:border-green-700 dark:text-green-200\">\n                                                                                            Version: {appInfo.version}\n                                                                                        </Badge>\n                                                                                        <Badge variant=\"outline\"\n                                                                                               className=\"text-xs bg-green-100 border-green-300 text-green-800 dark:bg-green-950 dark:border-green-700 dark:text-green-200\">\n                                                                                            Environment: {appInfo.environment}\n                                                                                        </Badge>\n                                                                                        <Badge variant=\"outline\"\n                                                                                               className=\"text-xs bg-green-100 border-green-300 text-green-800 dark:bg-green-950 dark:border-green-700 dark:text-green-200\">\n                                                                                            Anonymous instance ID\n                                                                                        </Badge>\n                                                                                    </div>\n                                                                                </div>\n                                                                            </div>\n                                                                        </CardContent>\n                                                                    </Card>\n\n                                                                    <Card\n                                                                        className=\"border-red-200 bg-red-50/50 dark:border-red-800 dark:bg-red-950/20\">\n                                                                        <CardContent className=\"pt-4\">\n                                                                            <div className=\"flex items-start gap-3\">\n                                                                                <XCircle\n                                                                                    className=\"w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0\"/>\n                                                                                <div className=\"space-y-3\">\n                                                                                    <h4 className=\"font-medium text-red-900 dark:text-red-100\">\n                                                                                        What we don&apos;t collect\n                                                                                    </h4>\n                                                                                    <div\n                                                                                        className=\"space-y-1 text-xs text-red-700 dark:text-red-300\">\n                                                                                        <div>• Personal data or user\n                                                                                            information\n                                                                                        </div>\n                                                                                        <div>• Application logs or\n                                                                                            sensitive\n                                                                                            data\n                                                                                        </div>\n                                                                                        <div>• Project content or\n                                                                                            configurations\n                                                                                        </div>\n                                                                                        <div>• IP addresses or tracking\n                                                                                            data\n                                                                                        </div>\n                                                                                    </div>\n                                                                                </div>\n                                                                            </div>\n                                                                        </CardContent>\n                                                                    </Card>\n                                                                </div>\n\n                                                                <div\n                                                                    className=\"flex items-start gap-3 p-4 bg-muted/50 rounded-lg border\">\n                                                                    <Shield\n                                                                        className=\"w-5 h-5 text-muted-foreground mt-0.5 flex-shrink-0\"/>\n                                                                    <div className=\"space-y-2\">\n                                                                        <h4 className=\"text-sm font-medium\">Privacy &\n                                                                            Security</h4>\n                                                                        <p className=\"text-xs text-muted-foreground leading-relaxed\">\n                                                                            All telemetry data is anonymized, encrypted\n                                                                            in\n                                                                            transit, and used solely for product\n                                                                            improvement.\n                                                                            We follow GDPR guidelines and never sell or\n                                                                            share your data with third parties.\n                                                                        </p>\n                                                                    </div>\n                                                                </div>\n\n                                                                {config?.telemetryInstanceId && (\n                                                                    <div\n                                                                        className=\"flex items-center gap-3 p-3 rounded-lg border bg-muted/30\">\n                                                                        <div className=\"flex-1 min-w-0\">\n                                                                            <p className=\"text-xs text-muted-foreground mb-1\">Instance\n                                                                                ID</p>\n                                                                            <code\n                                                                                className=\"text-xs font-mono text-foreground/70 break-all select-all\">\n                                                                                {config.telemetryInstanceId}\n                                                                            </code>\n                                                                        </div>\n                                                                        <Button\n                                                                            type=\"button\"\n                                                                            variant=\"ghost\"\n                                                                            size=\"sm\"\n                                                                            className=\"h-8 w-8 p-0 flex-shrink-0\"\n                                                                            onClick={() => {\n                                                                                navigator.clipboard.writeText(config.telemetryInstanceId!)\n                                                                                toast({\n                                                                                    title: 'Copied',\n                                                                                    description: 'Instance ID copied to clipboard.'\n                                                                                })\n                                                                            }}\n                                                                        >\n                                                                            <Copy className=\"h-3.5 w-3.5\"/>\n                                                                        </Button>\n                                                                    </div>\n                                                                )}\n                                                            </CardContent>\n                                                        </Card>\n                                                    )}\n                                                />\n                                            </div>\n                                        </TabsContent>\n\n                                        <TabsContent value=\"integrations\" className=\"space-y-4\">\n                                            <motion.div\n                                                variants={cardVariants}\n                                                initial=\"hidden\"\n                                                animate=\"visible\"\n                                                className=\"flex flex-row items-center justify-between rounded-lg border p-4\"\n                                            >\n                                                <div className=\"flex gap-2\">\n                                                    <Mail className=\"h-5 w-5 text-muted-foreground mt-0.5\"/>\n                                                    <div className=\"space-y-1\">\n                                                        <h3 className=\"text-base font-medium\">System Email</h3>\n                                                        <p className=\"text-sm text-muted-foreground\">\n                                                            Configure SMTP settings for internal system usage.\n                                                        </p>\n                                                    </div>\n                                                </div>\n                                                <Button asChild variant=\"outline\" size=\"sm\">\n                                                    <Link href=\"/dashboard/admin/system/email\">\n                                                        Configure\n                                                    </Link>\n                                                </Button>\n                                            </motion.div>\n\n                                            <motion.div\n                                                variants={cardVariants}\n                                                initial=\"hidden\"\n                                                animate=\"visible\"\n                                                transition={{delay: 0.1}}\n                                                className=\"flex flex-row items-center justify-between rounded-lg border p-4\"\n                                            >\n                                                <div className=\"flex gap-2\">\n                                                    <SlackLogo className=\"h-10 w-10  mt-0.5\"/>\n                                                    <div className=\"space-y-1\">\n                                                        <h3 className=\"text-base font-medium\">Slack Integration</h3>\n                                                        <p className=\"text-sm text-muted-foreground\">\n                                                            Configure OAuth credentials for Slack workspace integration.\n                                                        </p>\n                                                    </div>\n                                                </div>\n                                                <Button asChild variant=\"outline\" size=\"sm\">\n                                                    <Link href=\"/dashboard/admin/system/slack\">\n                                                        Configure\n                                                    </Link>\n                                                </Button>\n                                            </motion.div>\n                                        </TabsContent>\n\n                                        <TabsContent value=\"security\" className=\"space-y-6\">\n                                            <Card>\n                                                <CardHeader>\n                                                    <CardTitle className=\"flex items-center gap-2\">\n                                                        <Shield className=\"h-5 w-5\"/>\n                                                        Panel IP Whitelist\n                                                    </CardTitle>\n                                                    <CardDescription>\n                                                        Restrict access to the dashboard and API to specific IP\n                                                        addresses or CIDR ranges. When enabled, requests from\n                                                        unlisted IPs receive a 403 response. Public changelog pages\n                                                        are never affected.\n                                                    </CardDescription>\n                                                </CardHeader>\n                                                <CardContent className=\"space-y-6\">\n                                                    {config && !config.nginxAgentConfigured && (\n                                                        <div className=\"flex gap-3 rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm dark:border-amber-900 dark:bg-amber-950/30\">\n                                                            <AlertTriangle className=\"h-4 w-4 mt-0.5 shrink-0 text-amber-600 dark:text-amber-400\"/>\n                                                            <div className=\"space-y-1\">\n                                                                <p className=\"font-medium text-amber-800 dark:text-amber-300\">nginx-agent not configured</p>\n                                                                <p className=\"text-amber-700 dark:text-amber-400\">\n                                                                    IP whitelisting requires the nginx-agent to be set up. Add the following to your <code className=\"bg-amber-100 dark:bg-amber-900 px-1 rounded\">.env</code>:\n                                                                </p>\n                                                                <pre className=\"mt-2 rounded bg-amber-100 dark:bg-amber-900 px-3 py-2 font-mono text-xs text-amber-900 dark:text-amber-200 whitespace-pre-wrap\">{`NGINX_AGENT_URL=http://localhost:7842\\nNGINX_AGENT_SECRET=your-agent-secret\\nINTERNAL_API_SECRET=your-internal-secret`}</pre>\n                                                                <p className=\"text-amber-700 dark:text-amber-400\">\n                                                                    See the <a href=\"https://github.com/changerawr/nginx-agent\" target=\"_blank\" rel=\"noopener noreferrer\" className=\"underline underline-offset-2\">nginx-agent docs</a> for setup instructions.\n                                                                </p>\n                                                            </div>\n                                                        </div>\n                                                    )}\n\n                                                    <FormField\n                                                        control={form.control}\n                                                        name=\"panelIpWhitelistEnabled\"\n                                                        render={({field}) => (\n                                                            <FormItem\n                                                                className=\"flex items-center justify-between rounded-lg border p-4\">\n                                                                <div className=\"space-y-0.5\">\n                                                                    <FormLabel className=\"text-base\">\n                                                                        Enable IP Whitelist\n                                                                    </FormLabel>\n                                                                    <FormDescription>\n                                                                        Only allow listed IPs to access the panel.\n                                                                        Make sure your own IP is listed before enabling.\n                                                                    </FormDescription>\n                                                                </div>\n                                                                <FormControl>\n                                                                    <Switch\n                                                                        checked={field.value}\n                                                                        onCheckedChange={field.onChange}\n                                                                        disabled={!config?.nginxAgentConfigured}\n                                                                    />\n                                                                </FormControl>\n                                                            </FormItem>\n                                                        )}\n                                                    />\n\n                                                    <FormField\n                                                        control={form.control}\n                                                        name=\"panelIpWhitelist\"\n                                                        render={({field}) => (\n                                                            <FormItem>\n                                                                <FormLabel>Allowed IPs / CIDR Ranges</FormLabel>\n                                                                <FormControl>\n                                                                    <textarea\n                                                                        className=\"flex min-h-[120px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 font-mono\"\n                                                                        placeholder={\"192.168.1.1\\n10.0.0.0/8\\n172.16.0.0/12\"}\n                                                                        value={field.value.join('\\n')}\n                                                                        disabled={!config?.nginxAgentConfigured}\n                                                                        onChange={(e) => {\n                                                                            const lines = e.target.value\n                                                                                .split('\\n')\n                                                                                .map(l => l.trim())\n                                                                                .filter(Boolean)\n                                                                            field.onChange(lines)\n                                                                        }}\n                                                                    />\n                                                                </FormControl>\n                                                                <FormDescription>\n                                                                    One IP address or CIDR range per line.\n                                                                    Examples: <code className=\"text-xs bg-muted px-1 rounded\">203.0.113.5</code>,{' '}\n                                                                    <code className=\"text-xs bg-muted px-1 rounded\">10.0.0.0/8</code>.\n                                                                    Changes take effect within 30 seconds.\n                                                                </FormDescription>\n                                                                <FormMessage/>\n                                                            </FormItem>\n                                                        )}\n                                                    />\n                                                </CardContent>\n                                            </Card>\n                                        </TabsContent>\n\n                                        <TabsContent value=\"license\" className=\"space-y-6\">\n                                            <Card className=\"border-2\">\n                                                <CardHeader>\n                                                    <div className=\"flex items-center gap-3\">\n                                                        <div className=\"p-2 bg-primary/10 rounded-lg\">\n                                                            <KeyRound className=\"h-5 w-5 text-primary\"/>\n                                                        </div>\n                                                        <div>\n                                                            <CardTitle className=\"text-lg\">License</CardTitle>\n                                                            <CardDescription>\n                                                                Activate a license key to unlock extended features\n                                                            </CardDescription>\n                                                        </div>\n                                                    </div>\n                                                </CardHeader>\n                                                <CardContent className=\"space-y-6\">\n                                                    {connectionFailed && (\n                                                        <div\n                                                            className=\"flex items-center gap-3 p-4 rounded-lg border border-amber-500/30 bg-amber-500/5\">\n                                                            <AlertCircle\n                                                                className=\"w-5 h-5 text-amber-500 flex-shrink-0\"/>\n                                                            <div className=\"flex-1\">\n                                                                <p className=\"text-sm font-medium text-amber-600 dark:text-amber-400\">Connection\n                                                                    Failed</p>\n                                                                <p className=\"text-xs text-muted-foreground\">\n                                                                    Unable to reach the licensing server. Please\n                                                                    re-enter your license key to reactivate.\n                                                                </p>\n                                                            </div>\n                                                        </div>\n                                                    )}\n\n                                                    {!connectionFailed && (\n                                                        <div className=\"flex items-center gap-3 p-4 rounded-lg border\">\n                                                            <div\n                                                                className={`w-3 h-3 rounded-full ${isLicensed ? 'bg-green-500' : 'bg-muted-foreground/30'}`}/>\n                                                            <div className=\"flex-1\">\n                                                                <p className=\"text-sm font-medium\">\n                                                                    {isLicensed ? 'Active' : 'Inactive'}\n                                                                </p>\n                                                                <p className=\"text-xs text-muted-foreground\">\n                                                                    {isLicensed\n                                                                        ? 'Extended features are enabled for this instance.'\n                                                                        : 'Enter a license key below to activate extended features.'}\n                                                                </p>\n                                                            </div>\n                                                            {isLicensed && (\n                                                                <Badge variant=\"default\"\n                                                                       className=\"text-xs\">Licensed</Badge>\n                                                            )}\n                                                        </div>\n                                                    )}\n\n                                                    {(!isLicensed || connectionFailed) ? (\n                                                        <div className=\"space-y-5\">\n                                                            {/* Step indicator */}\n                                                            {activationStep !== 'key' && (\n                                                                <div\n                                                                    className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n                                                                    <div className=\"flex items-center gap-1.5\">\n                                                                        <div\n                                                                            className=\"w-5 h-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-[10px] font-bold\">\n                                                                            <Check className=\"w-3 h-3\"/>\n                                                                        </div>\n                                                                        <span\n                                                                            className=\"font-medium text-foreground\">Key</span>\n                                                                    </div>\n                                                                    <div className=\"w-6 h-px bg-primary\"/>\n                                                                    <div className=\"flex items-center gap-1.5\">\n                                                                        <div\n                                                                            className={`w-5 h-5 rounded-full flex items-center justify-center text-[10px] font-bold ${\n                                                                                activationStep === 'challenge'\n                                                                                    ? 'bg-primary text-primary-foreground'\n                                                                                    : 'bg-primary text-primary-foreground'\n                                                                            }`}>\n                                                                            {activationStep === 'confirm' ?\n                                                                                <Check className=\"w-3 h-3\"/> : '2'}\n                                                                        </div>\n                                                                        <span\n                                                                            className={activationStep === 'challenge' ? 'font-medium text-foreground' : 'font-medium text-foreground'}>Verify</span>\n                                                                    </div>\n                                                                    <div\n                                                                        className={`w-6 h-px ${activationStep === 'confirm' ? 'bg-primary' : 'bg-border'}`}/>\n                                                                    <div className=\"flex items-center gap-1.5\">\n                                                                        <div\n                                                                            className={`w-5 h-5 rounded-full flex items-center justify-center text-[10px] font-bold ${\n                                                                                activationStep === 'confirm'\n                                                                                    ? 'bg-primary text-primary-foreground'\n                                                                                    : 'bg-muted text-muted-foreground'\n                                                                            }`}>3\n                                                                        </div>\n                                                                        <span\n                                                                            className={activationStep === 'confirm' ? 'font-medium text-foreground' : ''}>Confirm</span>\n                                                                    </div>\n                                                                </div>\n                                                            )}\n\n                                                            {activationStep === 'key' && (\n                                                                <>\n                                                                    <div className=\"space-y-2\">\n                                                                        <label className=\"text-sm font-medium\">License\n                                                                            Key</label>\n                                                                        <Input\n                                                                            type=\"text\"\n                                                                            placeholder=\"chr_sp_...\"\n                                                                            value={licenseKeyInput}\n                                                                            onChange={(e) => setLicenseKeyInput(e.target.value)}\n                                                                            disabled={licenseLoading}\n                                                                        />\n                                                                    </div>\n                                                                    <Button\n                                                                        type=\"button\"\n                                                                        onClick={handleActivateClick}\n                                                                        disabled={licenseLoading || !licenseKeyInput.trim()}\n                                                                        className=\"w-full\"\n                                                                    >\n                                                                        {licenseLoading ? (\n                                                                            <><Loader2\n                                                                                className=\"mr-2 h-4 w-4 animate-spin\"/> Connecting...</>\n                                                                        ) : (\n                                                                            <><ArrowRight\n                                                                                className=\"mr-2 h-4 w-4\"/> Continue</>\n                                                                        )}\n                                                                    </Button>\n                                                                </>\n                                                            )}\n\n                                                            {activationStep === 'challenge' && (\n                                                                <div className=\"space-y-4\">\n                                                                    {/* Challenge code card */}\n                                                                    <div\n                                                                        className=\"rounded-lg border-2 border-primary/20 bg-primary/5 p-5 space-y-4\">\n                                                                        <div className=\"flex items-start gap-3\">\n                                                                            <div\n                                                                                className=\"p-1.5 rounded-md bg-primary/10 mt-0.5\">\n                                                                                <ShieldCheck\n                                                                                    className=\"w-4 h-4 text-primary\"/>\n                                                                            </div>\n                                                                            <div className=\"flex-1 space-y-1\">\n                                                                                <p className=\"text-sm font-semibold\">Your\n                                                                                    verification code</p>\n                                                                                <p className=\"text-xs text-muted-foreground\">\n                                                                                    Enter this code in your license\n                                                                                    dashboard to verify ownership\n                                                                                </p>\n                                                                            </div>\n                                                                        </div>\n\n                                                                        <div\n                                                                            className=\"flex items-center justify-between p-3 rounded-md bg-background border\">\n                                                                            <code\n                                                                                className=\"text-2xl font-mono font-bold tracking-[0.3em] select-all text-primary\">\n                                                                                {challengeCode}\n                                                                            </code>\n                                                                            <Button\n                                                                                type=\"button\"\n                                                                                variant=\"ghost\"\n                                                                                size=\"sm\"\n                                                                                className=\"h-8 px-2 text-muted-foreground hover:text-foreground\"\n                                                                                onClick={() => {\n                                                                                    navigator.clipboard.writeText(challengeCode)\n                                                                                    toast({\n                                                                                        title: 'Copied',\n                                                                                        description: 'Verification code copied to clipboard.'\n                                                                                    })\n                                                                                }}\n                                                                            >\n                                                                                <Copy className=\"h-3.5 w-3.5 mr-1.5\"/>\n                                                                                <span className=\"text-xs\">Copy</span>\n                                                                            </Button>\n                                                                        </div>\n\n                                                                        <Button\n                                                                            type=\"button\"\n                                                                            variant=\"outline\"\n                                                                            size=\"sm\"\n                                                                            className=\"w-full\"\n                                                                            onClick={() => window.open('https://dl.supers0ft.us/changerawr/sponsor/dashboard', '_blank')}\n                                                                        >\n                                                                            <ExternalLink\n                                                                                className=\"mr-2 h-3.5 w-3.5\"/> Open\n                                                                            License Dashboard\n                                                                        </Button>\n                                                                    </div>\n\n                                                                    {/* Response code input */}\n                                                                    <div className=\"space-y-2\">\n                                                                        <label\n                                                                            className=\"text-sm font-medium flex items-center gap-2\">\n                                                                            Response Code\n                                                                            <Badge variant=\"outline\"\n                                                                                   className=\"text-[10px] font-normal\">From\n                                                                                dashboard</Badge>\n                                                                        </label>\n                                                                        <p className=\"text-xs text-muted-foreground\">\n                                                                            After entering the code above in your\n                                                                            dashboard, you&apos;ll receive a 6-character\n                                                                            response code.\n                                                                        </p>\n                                                                        <Input\n                                                                            type=\"text\"\n                                                                            placeholder=\"ABC123\"\n                                                                            value={responseCodeInput}\n                                                                            onChange={(e) => setResponseCodeInput(e.target.value.toUpperCase().replace(/[^A-F0-9]/g, ''))}\n                                                                            maxLength={6}\n                                                                            className=\"font-mono tracking-[0.3em] text-center text-lg h-12\"\n                                                                            disabled={licenseLoading}\n                                                                            autoFocus\n                                                                        />\n                                                                    </div>\n\n                                                                    <div className=\"flex gap-2 pt-1\">\n                                                                        <Button\n                                                                            type=\"button\"\n                                                                            variant=\"ghost\"\n                                                                            size=\"sm\"\n                                                                            onClick={() => {\n                                                                                setActivationStep('key')\n                                                                                setChallengeCode('')\n                                                                                setChallengeId('')\n                                                                                setResponseCodeInput('')\n                                                                            }}\n                                                                        >\n                                                                            <ArrowLeft\n                                                                                className=\"mr-1.5 h-3.5 w-3.5\"/> Back\n                                                                        </Button>\n                                                                        <Button\n                                                                            type=\"button\"\n                                                                            onClick={handleConfirmChallenge}\n                                                                            disabled={licenseLoading || responseCodeInput.trim().length !== 6}\n                                                                            className=\"flex-1\"\n                                                                        >\n                                                                            {licenseLoading ? (\n                                                                                <><Loader2\n                                                                                    className=\"mr-2 h-4 w-4 animate-spin\"/> Verifying...</>\n                                                                            ) : (\n                                                                                <><ShieldCheck\n                                                                                    className=\"mr-2 h-4 w-4\"/> Complete\n                                                                                    Activation</>\n                                                                            )}\n                                                                        </Button>\n                                                                    </div>\n                                                                </div>\n                                                            )}\n                                                        </div>\n                                                    ) : (\n                                                        <Button\n                                                            type=\"button\"\n                                                            variant=\"outline\"\n                                                            onClick={handleDeactivateLicense}\n                                                            disabled={licenseLoading}\n                                                        >\n                                                            {licenseLoading ? (\n                                                                <><Loader2\n                                                                    className=\"mr-2 h-4 w-4 animate-spin\"/> Deactivating...</>\n                                                            ) : (\n                                                                'Deactivate License'\n                                                            )}\n                                                        </Button>\n                                                    )}\n\n                                                    <div\n                                                        className=\"flex items-start gap-3 p-4 bg-muted/50 rounded-lg border\">\n                                                        <Shield\n                                                            className=\"w-5 h-5 text-muted-foreground mt-0.5 flex-shrink-0\"/>\n                                                        <div className=\"space-y-1\">\n                                                            <p className=\"text-xs text-muted-foreground leading-relaxed\">\n                                                                {activationStep === 'challenge'\n                                                                    ? 'This two-step verification ensures your license key can only be activated by its owner.'\n                                                                    : 'Having trouble activating? Check if your firewall allows connections to our licensing server.'}\n                                                            </p>\n                                                            {activationStep === 'key' && (\n                                                                <Button\n                                                                    type=\"button\"\n                                                                    variant=\"link\"\n                                                                    size=\"sm\"\n                                                                    className=\"h-auto p-0 text-xs text-primary\"\n                                                                    onClick={() => window.open('https://dl.supers0ft.us/changerawr/sponsor/auth/github', '_blank')}\n                                                                >\n                                                                    Get a license key\n                                                                </Button>\n                                                            )}\n                                                        </div>\n                                                    </div>\n                                                </CardContent>\n                                            </Card>\n                                        </TabsContent>\n\n                                        <Separator className=\"my-6\"/>\n\n                                        <div className=\"flex justify-end\">\n                                            <Button\n                                                type=\"submit\"\n                                                disabled={updateConfig.isPending}\n                                                className=\"px-8\"\n                                            >\n                                                {updateConfig.isPending ? (\n                                                    <>\n                                                        <Loader2 className=\"mr-2 h-4 w-4 animate-spin\"/>\n                                                        Updating...\n                                                    </>\n                                                ) : (\n                                                    <>\n                                                        <Check className=\"mr-2 h-4 w-4\"/>\n                                                        Save Changes\n                                                    </>\n                                                )}\n                                            </Button>\n                                        </div>\n                                    </form>\n                                </Form>\n                            </Tabs>\n                        )}\n                    </CardContent>\n                </Card>\n            </motion.div>\n\n            <Dialog open={showNameModal} onOpenChange={setShowNameModal}>\n                <DialogContent className=\"sm:max-w-md\">\n                    <DialogHeader>\n                        <DialogTitle>Name This Instance</DialogTitle>\n                        <DialogDescription>\n                            Give this server a name so you can identify it later in the license portal.\n                        </DialogDescription>\n                    </DialogHeader>\n                    <div className=\"space-y-4 py-4\">\n                        <Input\n                            placeholder=\"e.g. Production Server, Dev Environment...\"\n                            value={instanceNameInput}\n                            onChange={(e) => setInstanceNameInput(e.target.value)}\n                            onKeyDown={(e) => {\n                                if (e.key === 'Enter') handleActivateLicense()\n                            }}\n                            autoFocus\n                        />\n                    </div>\n                    <DialogFooter>\n                        <Button variant=\"outline\" onClick={() => setShowNameModal(false)}>Cancel</Button>\n                        <Button onClick={handleActivateLicense}>\n                            <Key className=\"mr-2 h-4 w-4\"/> Activate\n                        </Button>\n                    </DialogFooter>\n                </DialogContent>\n            </Dialog>\n\n            <Dialog open={showDeviceLimitModal} onOpenChange={setShowDeviceLimitModal}>\n                <DialogContent className=\"sm:max-w-md\">\n                    <DialogHeader>\n                        <DialogTitle>Device Limit Reached</DialogTitle>\n                        <DialogDescription>\n                            All activation slots for this license are in use. Free up a slot from the license portal, or\n                            refresh to check if one has been freed.\n                        </DialogDescription>\n                    </DialogHeader>\n                    <div className=\"flex flex-col gap-3 pt-4\">\n                        <Button\n                            variant=\"outline\"\n                            onClick={() => window.open('https://dl.supers0ft.us/changerawr/sponsor/dashboard', '_blank')}\n                        >\n                            <ExternalLink className=\"mr-2 h-4 w-4\"/> Open License Portal\n                        </Button>\n                        <Button\n                            onClick={handleRefreshDeviceStatus}\n                            disabled={deviceLimitRefreshing}\n                        >\n                            {deviceLimitRefreshing ? (\n                                <><Loader2 className=\"mr-2 h-4 w-4 animate-spin\"/> Checking...</>\n                            ) : (\n                                <><RefreshCw className=\"mr-2 h-4 w-4\"/> Refresh &amp; Retry</>\n                            )}\n                        </Button>\n                    </div>\n                    <DialogFooter>\n                        <Button variant=\"ghost\" onClick={() => setShowDeviceLimitModal(false)}>Cancel</Button>\n                    </DialogFooter>\n                </DialogContent>\n            </Dialog>\n        </>\n    )\n}\n"
  },
  {
    "path": "app/dashboard/admin/system/slack/page.tsx",
    "content": "// app/dashboard/admin/system/slack/page.tsx\n'use client'\n\nimport {useEffect, useState} from 'react'\nimport {useForm} from 'react-hook-form'\nimport {zodResolver} from '@hookform/resolvers/zod'\nimport {z} from 'zod'\nimport {useAuth} from '@/context/auth'\nimport {useToast} from '@/hooks/use-toast'\nimport {useRouter} from 'next/navigation'\nimport {motion} from 'framer-motion'\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n    CardFooter,\n} from '@/components/ui/card'\nimport {\n    Form,\n    FormControl,\n    FormDescription,\n    FormField,\n    FormItem,\n    FormLabel,\n    FormMessage,\n} from '@/components/ui/form'\nimport {Input} from '@/components/ui/input'\nimport {Button} from '@/components/ui/button'\nimport {Switch} from '@/components/ui/switch'\nimport {\n    ArrowLeft,\n    Eye,\n    EyeOff,\n    ExternalLink,\n    Loader2,\n    Lock,\n    Download,\n    Copy,\n    Check,\n} from 'lucide-react'\nimport {\n    Alert,\n    AlertDescription,\n    AlertTitle,\n} from '@/components/ui/alert'\n\n// Define the schema for Slack OAuth configuration\nconst slackOAuthSchema = z.object({\n    slackOAuthEnabled: z.boolean(),\n    slackOAuthClientId: z.string().min(1, 'Client ID is required'),\n    slackOAuthClientSecret: z.string().min(1, 'Client Secret is required'),\n    slackSigningSecret: z.string().min(1, 'Signing Secret is required'),\n});\n\ntype SlackOAuthConfig = z.infer<typeof slackOAuthSchema>;\n\nexport default function SystemSlackConfigPage() {\n    const {user} = useAuth();\n    const {toast} = useToast();\n    const router = useRouter();\n    const [isSaving, setIsSaving] = useState(false);\n    const [showClientSecret, setShowClientSecret] = useState(false);\n    const [showSigningSecret, setShowSigningSecret] = useState(false);\n    const [isLoading, setIsLoading] = useState(true);\n    const [manifest, setManifest] = useState<string>('');\n    const [copiedToClipboard, setCopiedToClipboard] = useState(false);\n\n    // Initialize form\n    const form = useForm<SlackOAuthConfig>({\n        resolver: zodResolver(slackOAuthSchema),\n        defaultValues: {\n            slackOAuthEnabled: false,\n            slackOAuthClientId: '',\n            slackOAuthClientSecret: '',\n            slackSigningSecret: '',\n        },\n    });\n\n    // Fetch current configuration\n    useEffect(() => {\n        const fetchConfig = async () => {\n            if (!user || user.role !== 'ADMIN') {\n                setIsLoading(false);\n                return;\n            }\n\n            try {\n                const response = await fetch('/api/admin/system/slack');\n                if (response.ok) {\n                    const data = await response.json();\n                    form.reset({\n                        slackOAuthEnabled: data.slackOAuthEnabled || false,\n                        slackOAuthClientId: data.slackOAuthClientId || '',\n                        slackOAuthClientSecret: data.slackOAuthClientSecret || '',\n                        slackSigningSecret: data.slackSigningSecret || '',\n                    });\n                }\n            } catch (error) {\n                console.error('Failed to fetch Slack config:', error);\n                toast({\n                    title: 'Error',\n                    description: 'Failed to load Slack configuration',\n                    variant: 'destructive',\n                });\n            } finally {\n                setIsLoading(false);\n            }\n        };\n\n        fetchConfig();\n    }, [user, form, toast]);\n\n    // Fetch manifest\n    useEffect(() => {\n        const fetchManifest = async () => {\n            try {\n                const response = await fetch('/api/integrations/slack/manifest');\n                if (response.ok) {\n                    const manifestData = await response.json();\n                    setManifest(JSON.stringify(manifestData, null, 2));\n                }\n            } catch (error) {\n                console.error('Failed to fetch Slack manifest:', error);\n            }\n        };\n\n        fetchManifest();\n    }, []);\n\n    // Handle copy to clipboard\n    const handleCopyManifest = async () => {\n        try {\n            await navigator.clipboard.writeText(manifest);\n            setCopiedToClipboard(true);\n            setTimeout(() => setCopiedToClipboard(false), 2000);\n            toast({\n                title: 'Copied',\n                description: 'Manifest copied to clipboard',\n            });\n        } catch (error) {\n            toast({\n                title: 'Error',\n                description: 'Failed to copy manifest',\n                variant: 'destructive',\n            });\n        }\n    };\n\n    // Handle download manifest\n    const handleDownloadManifest = () => {\n        const element = document.createElement('a');\n        element.setAttribute('href', `data:text/json;charset=utf-8,${encodeURIComponent(manifest)}`);\n        element.setAttribute('download', 'changerawr-slack-manifest.json');\n        element.style.display = 'none';\n        document.body.appendChild(element);\n        element.click();\n        document.body.removeChild(element);\n    };\n\n    // Handle form submission\n    const onSubmit = async (data: SlackOAuthConfig) => {\n        setIsSaving(true);\n\n        try {\n            const response = await fetch('/api/admin/system/slack', {\n                method: 'PUT',\n                headers: {\n                    'Content-Type': 'application/json',\n                },\n                body: JSON.stringify(data),\n            });\n\n            if (!response.ok) {\n                throw new Error('Failed to save Slack configuration');\n            }\n\n            toast({\n                title: 'Success',\n                description: 'Slack OAuth configuration saved successfully',\n            });\n        } catch (error) {\n            toast({\n                title: 'Error',\n                description: error instanceof Error ? error.message : 'Failed to save configuration',\n                variant: 'destructive',\n            });\n        } finally {\n            setIsSaving(false);\n        }\n    };\n\n    // Check authorization\n    if (!user || user.role !== 'ADMIN') {\n        return (\n            <motion.div\n                initial={{opacity: 0}}\n                animate={{opacity: 1}}\n                className=\"flex items-center justify-center h-96\"\n            >\n                <Card className=\"border-destructive/50 bg-destructive/5\">\n                    <CardContent className=\"pt-6 text-center\">\n                        <Lock className=\"h-8 w-8 text-destructive mx-auto mb-2\"/>\n                        <p className=\"text-muted-foreground\">\n                            You do not have permission to access this page\n                        </p>\n                    </CardContent>\n                </Card>\n            </motion.div>\n        );\n    }\n\n    if (isLoading) {\n        return (\n            <div className=\"flex items-center justify-center h-96\">\n                <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\"/>\n            </div>\n        );\n    }\n\n    return (\n        <motion.div\n            initial={{opacity: 0, y: 20}}\n            animate={{opacity: 1, y: 0}}\n            transition={{duration: 0.3}}\n            className=\"space-y-6\"\n        >\n            {/* Header */}\n            <div className=\"flex items-center gap-4\">\n                <Button\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    onClick={() => router.back()}\n                    className=\"gap-2\"\n                >\n                    <ArrowLeft className=\"w-4 h-4\"/>\n                    Back\n                </Button>\n                <div>\n                    <h1 className=\"text-3xl font-bold\">Slack OAuth Configuration</h1>\n                    <p className=\"text-muted-foreground mt-2\">\n                        Configure Slack OAuth credentials for the Slack integration feature\n                    </p>\n                </div>\n            </div>\n\n            {/* Main Configuration Card */}\n            <Card>\n                <CardHeader>\n                    <CardTitle>OAuth Credentials</CardTitle>\n                    <CardDescription>\n                        Enter your Slack app credentials from the Slack App Directory\n                    </CardDescription>\n                </CardHeader>\n                <CardContent>\n                    <Form {...form}>\n                        <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-6\">\n                            {/* Enable/Disable Toggle */}\n                            <FormField\n                                control={form.control}\n                                name=\"slackOAuthEnabled\"\n                                render={({field}) => (\n                                    <FormItem className=\"flex items-center justify-between rounded-lg border p-4\">\n                                        <div className=\"space-y-0.5\">\n                                            <FormLabel>Enable Slack Integration</FormLabel>\n                                            <FormDescription>\n                                                Enable or disable Slack OAuth for all projects\n                                            </FormDescription>\n                                        </div>\n                                        <FormControl>\n                                            <Switch\n                                                checked={field.value}\n                                                onCheckedChange={field.onChange}\n                                            />\n                                        </FormControl>\n                                    </FormItem>\n                                )}\n                            />\n\n                            {/* Client ID Field */}\n                            <FormField\n                                control={form.control}\n                                name=\"slackOAuthClientId\"\n                                render={({field}) => (\n                                    <FormItem>\n                                        <FormLabel>Client ID</FormLabel>\n                                        <FormControl>\n                                            <Input\n                                                placeholder=\"xoxb-1234567890...\"\n                                                {...field}\n                                                disabled={!form.watch('slackOAuthEnabled')}\n                                            />\n                                        </FormControl>\n                                        <FormDescription>\n                                            Your Slack app's Client ID from the App Directory\n                                        </FormDescription>\n                                        <FormMessage/>\n                                    </FormItem>\n                                )}\n                            />\n\n                            {/* Client Secret Field */}\n                            <FormField\n                                control={form.control}\n                                name=\"slackOAuthClientSecret\"\n                                render={({field}) => (\n                                    <FormItem>\n                                        <FormLabel>Client Secret</FormLabel>\n                                        <div className=\"flex gap-2\">\n                                            <FormControl className=\"flex-1\">\n                                                <Input\n                                                    placeholder=\"••••••••••••••••••••••••••••••\"\n                                                    type={showClientSecret ? 'text' : 'password'}\n                                                    {...field}\n                                                    disabled={!form.watch('slackOAuthEnabled')}\n                                                />\n                                            </FormControl>\n                                            <Button\n                                                type=\"button\"\n                                                variant=\"outline\"\n                                                size=\"icon\"\n                                                onClick={() => setShowClientSecret(!showClientSecret)}\n                                                disabled={!form.watch('slackOAuthEnabled')}\n                                            >\n                                                {showClientSecret ? (\n                                                    <EyeOff className=\"h-4 w-4\"/>\n                                                ) : (\n                                                    <Eye className=\"h-4 w-4\"/>\n                                                )}\n                                            </Button>\n                                        </div>\n                                        <FormDescription>\n                                            Your Slack app's Client Secret (stored encrypted)\n                                        </FormDescription>\n                                        <FormMessage/>\n                                    </FormItem>\n                                )}\n                            />\n\n                            {/* Signing Secret Field */}\n                            <FormField\n                                control={form.control}\n                                name=\"slackSigningSecret\"\n                                render={({field}) => (\n                                    <FormItem>\n                                        <FormLabel>Signing Secret</FormLabel>\n                                        <div className=\"flex gap-2\">\n                                            <FormControl className=\"flex-1\">\n                                                <Input\n                                                    placeholder=\"••••••••••••••••••••••••••••••\"\n                                                    type={showSigningSecret ? 'text' : 'password'}\n                                                    {...field}\n                                                    disabled={!form.watch('slackOAuthEnabled')}\n                                                />\n                                            </FormControl>\n                                            <Button\n                                                type=\"button\"\n                                                variant=\"outline\"\n                                                size=\"icon\"\n                                                onClick={() => setShowSigningSecret(!showSigningSecret)}\n                                                disabled={!form.watch('slackOAuthEnabled')}\n                                            >\n                                                {showSigningSecret ? (\n                                                    <EyeOff className=\"h-4 w-4\"/>\n                                                ) : (\n                                                    <Eye className=\"h-4 w-4\"/>\n                                                )}\n                                            </Button>\n                                        </div>\n                                        <FormDescription>\n                                            Your Slack app's Signing Secret (stored encrypted) - used to verify webhook requests\n                                        </FormDescription>\n                                        <FormMessage/>\n                                    </FormItem>\n                                )}\n                            />\n\n                            {/* Info Alert */}\n                            {form.watch('slackOAuthEnabled') && (\n                                <Alert borderStyle=\"accent\" variant=\"info\">\n                                    <AlertTitle>Setup Instructions</AlertTitle>\n                                    <AlertDescription className=\"space-y-2 mt-2\">\n                                        <p>\n                                            1. Go to{' '}\n                                            <Button\n                                                variant=\"link\"\n                                                size=\"sm\"\n                                                className=\"h-auto p-0\"\n                                                onClick={() => window.open('https://api.slack.com/apps', '_blank')}\n                                            >\n                                                Slack App Directory <ExternalLink className=\"h-3 w-3 ml-1\"/>\n                                            </Button>\n                                        </p>\n                                        <p>\n                                            2. Create or select your app\n                                        </p>\n                                        <p>\n                                            3. Go to \"OAuth &amp; Permissions\" and copy the Client ID and Client Secret\n                                        </p>\n                                        <p>\n                                            4. Set the Redirect URI to: <code className=\"bg-muted px-2 py-1 rounded text-xs\">{typeof window !== 'undefined' ? `${window.location.origin}/api/integrations/slack/callback` : 'loading...'}</code>\n                                        </p>\n                                        <p>\n                                            5. Add the required scopes: <code className=\"bg-muted px-2 py-1 rounded text-xs\">chat:write channels:read users:read</code>\n                                        </p>\n                                    </AlertDescription>\n                                </Alert>\n                            )}\n\n                            {/* Save Button */}\n                            <div className=\"flex gap-2\">\n                                <Button\n                                    type=\"submit\"\n                                    disabled={isSaving}\n                                    className=\"gap-2\"\n                                >\n                                    {isSaving ? (\n                                        <Loader2 className=\"w-4 h-4 animate-spin\"/>\n                                    ) : (\n                                        'Save Configuration'\n                                    )}\n                                </Button>\n                            </div>\n                        </form>\n                    </Form>\n                </CardContent>\n            </Card>\n\n            {/* Slack App Manifest Card */}\n            {manifest && (\n                <Card>\n                    <CardHeader>\n                        <CardTitle>Slack App Manifest</CardTitle>\n                        <CardDescription>\n                            Use this manifest to create a Slack app easily. Copy it and paste it in Slack's App Manifest editor.\n                        </CardDescription>\n                    </CardHeader>\n                    <CardContent className=\"space-y-4\">\n                        <div className=\"relative w-full overflow-hidden\">\n                            <pre className=\"bg-muted p-4 rounded-lg text-sm border max-h-96 overflow-y-auto break-words whitespace-pre-wrap\">\n                                <code>{manifest}</code>\n                            </pre>\n                        </div>\n                        <div className=\"flex gap-2\">\n                            <Button\n                                type=\"button\"\n                                variant=\"outline\"\n                                onClick={handleCopyManifest}\n                                className=\"gap-2\"\n                            >\n                                {copiedToClipboard ? (\n                                    <>\n                                        <Check className=\"w-4 h-4\"/>\n                                        Copied!\n                                    </>\n                                ) : (\n                                    <>\n                                        <Copy className=\"w-4 h-4\"/>\n                                        Copy to Clipboard\n                                    </>\n                                )}\n                            </Button>\n                            <Button\n                                type=\"button\"\n                                variant=\"outline\"\n                                onClick={handleDownloadManifest}\n                                className=\"gap-2\"\n                            >\n                                <Download className=\"w-4 h-4\"/>\n                                Download JSON\n                            </Button>\n                        </div>\n                    </CardContent>\n                </Card>\n            )}\n\n            {/* Not Configured Alert */}\n            {!form.watch('slackOAuthEnabled') && (\n                <Alert variant=\"warning\">\n                    <AlertTitle>Slack Integration Not Configured</AlertTitle>\n                    <AlertDescription>\n                        The Slack integration is currently disabled. Users will see a message stating \"This feature has not been setup in system integrations\" when trying to configure Slack in their projects.\n                    </AlertDescription>\n                </Alert>\n            )}\n        </motion.div>\n    );\n}"
  },
  {
    "path": "app/dashboard/admin/system/templates/page.tsx",
    "content": "'use client'\n\nimport React, {useState, useEffect, useMemo} from 'react'\nimport {useQuery, useMutation} from '@tanstack/react-query'\nimport {useToast} from '@/hooks/use-toast'\nimport {useRouter} from 'next/navigation'\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card'\nimport {Input} from '@/components/ui/input'\nimport {Button} from '@/components/ui/button'\nimport {Badge} from '@/components/ui/badge'\nimport {Separator} from '@/components/ui/separator'\nimport {\n    ArrowLeft,\n    Plus,\n    Trash2,\n    Save,\n    Calendar,\n    Tag,\n    Eye,\n    GripVertical,\n    Loader2,\n} from 'lucide-react'\nimport Link from 'next/link'\n\ninterface DateTemplate {\n    format: string\n    label: string\n}\n\ninterface SystemConfig {\n    customDateTemplates?: DateTemplate[] | null\n    timezone?: string\n    [key: string]: unknown\n}\n\n// All available template variables\nconst TEMPLATE_VARIABLES = [\n    {category: 'Date & Time', variables: [\n        {token: '{YYYY}', description: 'Full year', example: '2026'},\n        {token: '{YY}', description: '2-digit year', example: '26'},\n        {token: '{MM}', description: 'Month (01-12)', example: '02'},\n        {token: '{DD}', description: 'Day (01-31)', example: '20'},\n        {token: '{hh}', description: 'Hour (00-23)', example: '14'},\n        {token: '{mm}', description: 'Minute (00-59)', example: '30'},\n        {token: '{ss}', description: 'Second (00-59)', example: '45'},\n    ]},\n    {category: 'Version', variables: [\n        {token: '{MAJOR}', description: 'Latest major version number', example: '1'},\n        {token: '{MINOR}', description: 'Latest minor version number', example: '5'},\n        {token: '{PATCH}', description: 'Latest patch version number', example: '3'},\n        {token: '{VERSION}', description: 'Full latest version (no v prefix)', example: '1.5.3'},\n        {token: '{NEXT_PATCH}', description: 'Next patch number', example: '4'},\n        {token: '{NEXT_MINOR}', description: 'Next minor number', example: '6'},\n        {token: '{NEXT_MAJOR}', description: 'Next major number', example: '2'},\n    ]},\n]\n\nconst DEFAULT_TEMPLATES: DateTemplate[] = [\n    {format: 'v{YYYY}.{MM}.{DD}', label: 'Date (dotted)'},\n    {format: 'v{YYYY}.{MM}.{DD}.1', label: 'Date (revision)'},\n    {format: 'v{YYYY}{MM}{DD}', label: 'Date (compact)'},\n]\n\nfunction resolvePreview(format: string, timezone: string): string {\n    const now = new Date()\n    const dateParts = new Intl.DateTimeFormat('en-CA', {\n        timeZone: timezone,\n        year: 'numeric',\n        month: '2-digit',\n        day: '2-digit',\n    }).formatToParts(now)\n    const timeParts = new Intl.DateTimeFormat('en-GB', {\n        timeZone: timezone,\n        hour: '2-digit',\n        minute: '2-digit',\n        second: '2-digit',\n        hour12: false,\n    }).formatToParts(now)\n\n    const year = dateParts.find(p => p.type === 'year')?.value ?? '2026'\n    const month = dateParts.find(p => p.type === 'month')?.value ?? '01'\n    const day = dateParts.find(p => p.type === 'day')?.value ?? '01'\n    const hour = timeParts.find(p => p.type === 'hour')?.value ?? '00'\n    const minute = timeParts.find(p => p.type === 'minute')?.value ?? '00'\n    const second = timeParts.find(p => p.type === 'second')?.value ?? '00'\n\n    // Version variables use placeholder values for preview\n    return format\n        .replace(/\\{YYYY}/g, year)\n        .replace(/\\{YY}/g, year.slice(-2))\n        .replace(/\\{MM}/g, month)\n        .replace(/\\{DD}/g, day)\n        .replace(/\\{hh}/g, hour)\n        .replace(/\\{mm}/g, minute)\n        .replace(/\\{ss}/g, second)\n        .replace(/\\{MAJOR}/g, '1')\n        .replace(/\\{MINOR}/g, '5')\n        .replace(/\\{PATCH}/g, '3')\n        .replace(/\\{VERSION}/g, '1.5.3')\n        .replace(/\\{NEXT_PATCH}/g, '4')\n        .replace(/\\{NEXT_MINOR}/g, '6')\n        .replace(/\\{NEXT_MAJOR}/g, '2')\n}\n\nexport default function VersionTemplatesPage() {\n    const {toast} = useToast()\n    const router = useRouter()\n    const [templates, setTemplates] = useState<DateTemplate[]>([])\n    const [newFormat, setNewFormat] = useState('')\n    const [newLabel, setNewLabel] = useState('')\n    const [hasChanges, setHasChanges] = useState(false)\n\n    const {data: config, isLoading} = useQuery<SystemConfig>({\n        queryKey: ['system-config'],\n        queryFn: async () => {\n            const response = await fetch('/api/admin/config')\n            if (!response.ok) throw new Error('Failed to fetch config')\n            return response.json()\n        },\n    })\n\n    const timezone = config?.timezone ?? 'UTC'\n\n    // Initialize templates from config\n    useEffect(() => {\n        if (config) {\n            setTemplates(config.customDateTemplates ?? [])\n            setHasChanges(false)\n        }\n    }, [config])\n\n    const saveMutation = useMutation({\n        mutationFn: async () => {\n            // We need to send the full config, so fetch current first\n            const currentRes = await fetch('/api/admin/config')\n            if (!currentRes.ok) throw new Error('Failed to fetch current config')\n            const current = await currentRes.json()\n\n            const response = await fetch('/api/admin/config', {\n                method: 'PATCH',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({\n                    ...current,\n                    customDateTemplates: templates.length > 0 ? templates : null,\n                }),\n            })\n            if (!response.ok) throw new Error('Failed to save templates')\n            return response.json()\n        },\n        onSuccess: () => {\n            toast({title: 'Templates Saved', description: 'Version templates have been updated.'})\n            setHasChanges(false)\n        },\n        onError: (error) => {\n            toast({title: 'Save Failed', description: error.message, variant: 'destructive'})\n        },\n    })\n\n    const addTemplate = () => {\n        if (!newFormat.trim() || !newLabel.trim()) return\n        setTemplates(prev => [...prev, {format: newFormat.trim(), label: newLabel.trim()}])\n        setNewFormat('')\n        setNewLabel('')\n        setHasChanges(true)\n    }\n\n    const removeTemplate = (index: number) => {\n        setTemplates(prev => prev.filter((_, i) => i !== index))\n        setHasChanges(true)\n    }\n\n    const addDefaults = () => {\n        setTemplates(prev => {\n            const existingFormats = new Set(prev.map(t => t.format))\n            const newTemplates = DEFAULT_TEMPLATES.filter(t => !existingFormats.has(t.format))\n            return [...prev, ...newTemplates]\n        })\n        setHasChanges(true)\n    }\n\n    // Live preview for the \"new\" input\n    const newFormatPreview = useMemo(() => {\n        if (!newFormat.trim()) return ''\n        return resolvePreview(newFormat, timezone)\n    }, [newFormat, timezone])\n\n    if (isLoading) {\n        return (\n            <div className=\"flex items-center justify-center min-h-[400px]\">\n                <Loader2 className=\"h-6 w-6 animate-spin text-muted-foreground\" />\n            </div>\n        )\n    }\n\n    return (\n        <div className=\"space-y-6\">\n            {/* Header */}\n            <div className=\"flex items-center justify-between\">\n                <div className=\"flex items-center gap-3\">\n                    <Button variant=\"ghost\" size=\"sm\" asChild>\n                        <Link href=\"/dashboard/admin/system\">\n                            <ArrowLeft className=\"h-4 w-4\" />\n                        </Link>\n                    </Button>\n                    <div>\n                        <h1 className=\"text-xl font-semibold\">Version Templates</h1>\n                        <p className=\"text-sm text-muted-foreground\">\n                            Custom version format templates for the changelog editor\n                        </p>\n                    </div>\n                </div>\n                <Button\n                    onClick={() => saveMutation.mutate()}\n                    disabled={!hasChanges || saveMutation.isPending}\n                    size=\"sm\"\n                >\n                    {saveMutation.isPending ? (\n                        <Loader2 className=\"h-3.5 w-3.5 mr-1.5 animate-spin\" />\n                    ) : (\n                        <Save className=\"h-3.5 w-3.5 mr-1.5\" />\n                    )}\n                    Save Changes\n                </Button>\n            </div>\n\n            {/* Variable Reference */}\n            <Card>\n                <CardHeader className=\"pb-3\">\n                    <CardTitle className=\"text-base\">Available Variables</CardTitle>\n                    <CardDescription>\n                        Use these variables in your format strings. They get replaced with real values when a user opens the version selector.\n                    </CardDescription>\n                </CardHeader>\n                <CardContent>\n                    <div className=\"space-y-4\">\n                        {TEMPLATE_VARIABLES.map(group => (\n                            <div key={group.category}>\n                                <div className=\"flex items-center gap-2 mb-2\">\n                                    {group.category === 'Date & Time' ? (\n                                        <Calendar className=\"h-3.5 w-3.5 text-muted-foreground\" />\n                                    ) : (\n                                        <Tag className=\"h-3.5 w-3.5 text-muted-foreground\" />\n                                    )}\n                                    <span className=\"text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n                                        {group.category}\n                                    </span>\n                                </div>\n                                <div className=\"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2\">\n                                    {group.variables.map(v => (\n                                        <button\n                                            key={v.token}\n                                            type=\"button\"\n                                            className=\"flex items-center gap-2 rounded-md border px-2.5 py-1.5 text-left hover:bg-accent/50 transition-colors group\"\n                                            onClick={() => setNewFormat(prev => prev + v.token)}\n                                        >\n                                            <code className=\"font-mono text-xs font-medium text-primary\">{v.token}</code>\n                                            <span className=\"text-[11px] text-muted-foreground flex-1 truncate\">{v.description}</span>\n                                            <Badge variant=\"secondary\" className=\"text-[10px] px-1 h-4 font-mono opacity-60 group-hover:opacity-100\">\n                                                {v.example}\n                                            </Badge>\n                                        </button>\n                                    ))}\n                                </div>\n                            </div>\n                        ))}\n                    </div>\n                </CardContent>\n            </Card>\n\n            {/* Templates */}\n            <Card>\n                <CardHeader className=\"pb-3\">\n                    <div className=\"flex items-center justify-between\">\n                        <div>\n                            <CardTitle className=\"text-base\">Templates</CardTitle>\n                            <CardDescription>\n                                {templates.length === 0\n                                    ? 'No custom templates configured. Built-in date templates will be used.'\n                                    : `${templates.length} custom template${templates.length !== 1 ? 's' : ''} configured`\n                                }\n                            </CardDescription>\n                        </div>\n                        {templates.length === 0 && (\n                            <Button variant=\"outline\" size=\"sm\" onClick={addDefaults}>\n                                Add Defaults\n                            </Button>\n                        )}\n                    </div>\n                </CardHeader>\n                <CardContent className=\"space-y-3\">\n                    {/* Existing templates */}\n                    {templates.length > 0 && (\n                        <div className=\"space-y-1.5\">\n                            {templates.map((template, index) => (\n                                <div\n                                    key={index}\n                                    className=\"flex items-center gap-2 rounded-md border px-3 py-2 group\"\n                                >\n                                    <GripVertical className=\"h-3.5 w-3.5 text-muted-foreground/30 shrink-0\" />\n                                    <code className=\"font-mono text-sm flex-1 min-w-0 truncate\">{template.format}</code>\n                                    <Separator orientation=\"vertical\" className=\"h-4\" />\n                                    <span className=\"text-xs text-muted-foreground shrink-0\">{template.label}</span>\n                                    <Separator orientation=\"vertical\" className=\"h-4\" />\n                                    <div className=\"flex items-center gap-1 shrink-0\">\n                                        <Eye className=\"h-3 w-3 text-muted-foreground\" />\n                                        <code className=\"font-mono text-[11px] text-muted-foreground\">\n                                            {resolvePreview(template.format, timezone)}\n                                        </code>\n                                    </div>\n                                    <Button\n                                        type=\"button\"\n                                        variant=\"ghost\"\n                                        size=\"sm\"\n                                        className=\"h-7 w-7 p-0 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity shrink-0\"\n                                        onClick={() => removeTemplate(index)}\n                                    >\n                                        <Trash2 className=\"h-3.5 w-3.5\" />\n                                    </Button>\n                                </div>\n                            ))}\n                        </div>\n                    )}\n\n                    <Separator />\n\n                    {/* Add new template */}\n                    <div className=\"space-y-2\">\n                        <span className=\"text-xs font-medium text-muted-foreground\">Add Template</span>\n                        <div className=\"flex items-end gap-2\">\n                            <div className=\"flex-1 space-y-1\">\n                                <label className=\"text-xs text-muted-foreground\">Format</label>\n                                <Input\n                                    placeholder=\"v{YYYY}.{MM}.{DD}\"\n                                    value={newFormat}\n                                    onChange={(e) => setNewFormat(e.target.value)}\n                                    onKeyDown={(e) => e.key === 'Enter' && addTemplate()}\n                                    className=\"h-8 font-mono text-sm\"\n                                />\n                            </div>\n                            <div className=\"w-36 space-y-1\">\n                                <label className=\"text-xs text-muted-foreground\">Label</label>\n                                <Input\n                                    placeholder=\"Date (dotted)\"\n                                    value={newLabel}\n                                    onChange={(e) => setNewLabel(e.target.value)}\n                                    onKeyDown={(e) => e.key === 'Enter' && addTemplate()}\n                                    className=\"h-8 text-sm\"\n                                />\n                            </div>\n                            <Button\n                                type=\"button\"\n                                variant=\"outline\"\n                                size=\"sm\"\n                                className=\"h-8\"\n                                disabled={!newFormat.trim() || !newLabel.trim()}\n                                onClick={addTemplate}\n                            >\n                                <Plus className=\"h-3.5 w-3.5 mr-1\" />\n                                Add\n                            </Button>\n                        </div>\n                        {/* Live preview */}\n                        {newFormatPreview && (\n                            <div className=\"flex items-center gap-1.5 text-xs text-muted-foreground pl-1\">\n                                <Eye className=\"h-3 w-3\" />\n                                Preview: <code className=\"font-mono bg-muted px-1 rounded\">{newFormatPreview}</code>\n                            </div>\n                        )}\n                    </div>\n                </CardContent>\n            </Card>\n\n            {/* Info note */}\n            <p className=\"text-xs text-muted-foreground text-center\">\n                Version variables ({'{MAJOR}'}, {'{MINOR}'}, {'{PATCH}'}, etc.) are resolved per-project based on the latest published version.\n                {' '}If no custom templates are saved, built-in date templates are shown.\n            </p>\n        </div>\n    )\n}\n"
  },
  {
    "path": "app/dashboard/admin/users/page.tsx",
    "content": "'use client'\n\nimport React, {useState} from 'react';\nimport {useQuery, useMutation, useQueryClient} from '@tanstack/react-query';\nimport {\n    Card,\n    CardContent,\n    CardHeader,\n    CardTitle,\n    CardDescription,\n} from '@/components/ui/card';\nimport {Button} from '@/components/ui/button';\nimport {\n    Table,\n    TableBody,\n    TableCell,\n    TableHead,\n    TableHeader,\n    TableRow,\n} from '@/components/ui/table';\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogFooter,\n    DialogHeader,\n    DialogTitle,\n} from '@/components/ui/dialog';\nimport {\n    DropdownMenu,\n    DropdownMenuContent,\n    DropdownMenuItem,\n    DropdownMenuSeparator,\n    DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\nimport {\n    Select,\n    SelectContent,\n    SelectItem,\n    SelectTrigger,\n    SelectValue,\n} from '@/components/ui/select';\nimport {\n    AlertDialog,\n    AlertDialogAction,\n    AlertDialogCancel,\n    AlertDialogContent,\n    AlertDialogDescription,\n    AlertDialogFooter,\n    AlertDialogHeader,\n    AlertDialogTitle, AlertDialogTrigger,\n} from '@/components/ui/alert-dialog';\nimport {Input} from '@/components/ui/input';\nimport {Label} from '@/components/ui/label';\nimport {Skeleton} from '@/components/ui/skeleton';\nimport {toast} from '@/hooks/use-toast';\nimport {\n    User, UserPlus, Copy, MoreVertical, Pencil,\n    Trash2, Search, X, Shield, Check\n} from 'lucide-react';\nimport {format} from 'date-fns';\nimport {motion, AnimatePresence} from 'framer-motion';\nimport {Role} from '@prisma/client';\nimport {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs';\n\ninterface UserData {\n    id: string;\n    email: string;\n    name: string | null;\n    role: Role;\n    createdAt: string;\n    lastLoginAt: string | null;\n}\n\ninterface InvitationLink {\n    id: string;\n    email: string;\n    role: Role;\n    token: string;\n    createdAt: string;\n    expiresAt: string;\n    usedAt: string | null;\n}\n\nexport default function UsersPage() {\n    const queryClient = useQueryClient();\n    const [isInviteDialogOpen, setIsInviteDialogOpen] = useState(false);\n    const [inviteEmail, setInviteEmail] = useState('');\n    const [inviteRole, setInviteRole] = useState<Role>('STAFF');\n    const [newInvitationUrl, setNewInvitationUrl] = useState<string | null>(null);\n    const [searchTerm, setSearchTerm] = useState('');\n    const [selectedUser, setSelectedUser] = useState<UserData | null>(null);\n    const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);\n    const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);\n    const [editName, setEditName] = useState('');\n    const [editRole, setEditRole] = useState<Role>('STAFF');\n\n    const {data: users, isLoading} = useQuery<UserData[]>({\n        queryKey: ['users'],\n        queryFn: async () => {\n            const response = await fetch('/api/admin/users');\n            if (!response.ok) throw new Error('Failed to fetch users');\n            const data = await response.json();\n            return data.filter((user: UserData) => !user.email.endsWith('@changerawr.sys'));\n        },\n    });\n\n    const {data: invitations} = useQuery<InvitationLink[]>({\n        queryKey: ['invitations'],\n        queryFn: async () => {\n            const response = await fetch('/api/admin/users/invitations');\n            if (!response.ok) throw new Error('Failed to fetch invitations');\n            return response.json();\n        },\n    });\n\n    const updateUser = useMutation<\n        UserData,\n        Error,\n        { userId: string; data: { name?: string; role?: Role } }\n    >({\n        mutationFn: async ({userId, data}: { userId: string; data: { name?: string; role?: Role } }) => {\n            const response = await fetch(`/api/admin/users/${userId}`, {\n                method: 'PATCH',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify(data),\n            });\n            if (!response.ok) {\n                const error = await response.json();\n                throw new Error(error.error || 'Failed to update user');\n            }\n            return response.json();\n        },\n        onSuccess: () => {\n            queryClient.invalidateQueries({queryKey: ['users']});\n            toast({\n                title: 'User Updated',\n                description: 'The user has been updated successfully.',\n            });\n            setIsEditDialogOpen(false);\n        },\n        onError: (error: Error) => {\n            toast({\n                title: 'Update Failed',\n                description: error.message,\n                variant: 'destructive',\n            });\n        },\n    });\n\n    const deleteUser = useMutation<\n        { message: string },\n        Error,\n        string\n    >({\n        mutationFn: async (userId: string) => {\n            const response = await fetch(`/api/admin/users/${userId}`, {\n                method: 'DELETE',\n            });\n            if (!response.ok) {\n                const error = await response.json();\n                throw new Error(error.error || 'Failed to delete user');\n            }\n            return response.json();\n        },\n        onSuccess: () => {\n            queryClient.invalidateQueries({queryKey: ['users']});\n            toast({\n                title: 'User Deleted',\n                description: 'The user has been deleted successfully.',\n            });\n            setIsDeleteDialogOpen(false);\n        },\n        onError: (error: Error) => {\n            toast({\n                title: 'Deletion Failed',\n                description: error.message,\n                variant: 'destructive',\n            });\n        },\n    });\n\n    const createInvitation = useMutation<\n        { invitation: { url: string } },\n        Error,\n        { email: string; role: Role }\n    >({\n        mutationFn: async (data: { email: string; role: Role }) => {\n            const response = await fetch('/api/admin/users', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify(data),\n            });\n            if (!response.ok) {\n                const error = await response.json();\n                throw new Error(error.error || 'Failed to create invitation');\n            }\n            return response.json();\n        },\n        onSuccess: (data) => {\n            setNewInvitationUrl(data.invitation.url);\n            queryClient.invalidateQueries({queryKey: ['invitations']});\n            toast({\n                title: 'Invitation Created',\n                description: 'The invitation link has been created successfully.',\n            });\n            setIsInviteDialogOpen(false);\n        },\n        onError: (error: Error) => {\n            toast({\n                title: 'Invitation Failed',\n                description: error.message,\n                variant: 'destructive',\n            });\n        },\n    });\n\n    const revokeInvitation = useMutation<\n        { message: string },\n        Error,\n        string\n    >({\n        mutationFn: async (id: string) => {\n            const response = await fetch(`/api/admin/users/invitations/${id}`, {\n                method: 'DELETE',\n            });\n            if (!response.ok) throw new Error('Failed to revoke invitation');\n            return response.json();\n        },\n        onSuccess: () => {\n            queryClient.invalidateQueries({queryKey: ['invitations']});\n            toast({\n                title: 'Invitation Revoked',\n                description: 'The invitation link has been revoked.',\n            });\n        },\n    });\n\n    const handleEditUser = (user: UserData) => {\n        setSelectedUser(user);\n        setEditName(user.name || '');\n        setEditRole(user.role);\n        setIsEditDialogOpen(true);\n    };\n\n    const handleDeleteUser = (user: UserData) => {\n        // Get current user's ID from the auth context\n        const currentUserEmail = users?.find(u => u.role === 'ADMIN')?.email;\n\n        if (user.email === currentUserEmail) {\n            toast({\n                title: \"Cannot Delete User\",\n                description: \"You cannot delete your own account.\",\n                variant: \"destructive\",\n            });\n            return;\n        }\n\n        setSelectedUser(user);\n        setIsDeleteDialogOpen(true);\n    };\n\n    const handleUpdateUser = async () => {\n        if (!selectedUser) return;\n\n        const updates: { name?: string; role?: Role } = {};\n        if (editName !== (selectedUser.name || '')) updates.name = editName;\n        if (editRole !== selectedUser.role) updates.role = editRole;\n\n        if (Object.keys(updates).length > 0) {\n            await updateUser.mutate({userId: selectedUser.id, data: updates});\n        }\n    };\n\n    const handleCreateInvitation = async (e: React.FormEvent) => {\n        e.preventDefault();\n        if (!inviteEmail.trim()) return;\n\n        await createInvitation.mutate({\n            email: inviteEmail,\n            role: inviteRole,\n        });\n        setInviteEmail('');\n    };\n\n    const handleCopyInvitationLink = (url: string) => {\n        navigator.clipboard.writeText(url);\n        toast({\n            title: 'Link Copied',\n            description: 'The invitation link has been copied to your clipboard.',\n        });\n    };\n\n    const filteredUsers = users?.filter(user =>\n        user.email.toLowerCase().includes(searchTerm.toLowerCase()) ||\n        (user.name?.toLowerCase() || '').includes(searchTerm.toLowerCase())\n    );\n\n    if (isLoading) {\n        return (\n            <div className=\"space-y-4\">\n                <Skeleton className=\"h-8 w-[200px]\"/>\n                <Card>\n                    <CardHeader>\n                        <Skeleton className=\"h-7 w-[150px]\"/>\n                        <Skeleton className=\"h-4 w-[250px] mt-2\"/>\n                    </CardHeader>\n                    <CardContent>\n                        <div className=\"space-y-4\">\n                            {Array.from({length: 5}).map((_, i) => (\n                                <Skeleton key={i} className=\"h-16 w-full\"/>\n                            ))}\n                        </div>\n                    </CardContent>\n                </Card>\n            </div>\n        );\n    }\n\n    return (\n        <motion.div\n            initial={{opacity: 0, y: 20}}\n            animate={{opacity: 1, y: 0}}\n            className=\"space-y-6\"\n        >\n            {/* Edit User Dialog */}\n            <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>\n                <DialogContent>\n                    <DialogHeader>\n                        <DialogTitle>Edit User</DialogTitle>\n                        <DialogDescription>\n                            Update user details and permissions.\n                        </DialogDescription>\n                    </DialogHeader>\n                    <div className=\"grid gap-4 py-4\">\n                        <div className=\"grid gap-2\">\n                            <Label htmlFor=\"name\">Name</Label>\n                            <Input\n                                id=\"name\"\n                                value={editName}\n                                onChange={(e) => setEditName(e.target.value)}\n                                placeholder=\"User's name\"\n                            />\n                        </div>\n                        <div className=\"grid gap-2\">\n                            <Label htmlFor=\"role\">Role</Label>\n                            <Select\n                                value={editRole}\n                                onValueChange={(value) => setEditRole(value as Role)}\n                            >\n                                <SelectTrigger>\n                                    <SelectValue placeholder=\"Select a role\"/>\n                                </SelectTrigger>\n                                <SelectContent>\n                                    <SelectItem value=\"STAFF\">Staff</SelectItem>\n                                    <SelectItem value=\"ADMIN\">Admin</SelectItem>\n                                </SelectContent>\n                            </Select>\n                        </div>\n                    </div>\n                    <DialogFooter>\n                        <Button\n                            variant=\"outline\"\n                            onClick={() => setIsEditDialogOpen(false)}\n                        >\n                            Cancel\n                        </Button>\n                        <Button\n                            onClick={handleUpdateUser}\n                            disabled={updateUser.isPending}\n                        >\n                            {updateUser.isPending ? 'Saving...' : 'Save Changes'}\n                        </Button>\n                    </DialogFooter>\n                </DialogContent>\n            </Dialog>\n\n            {/* Delete User Dialog */}\n            <AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>\n                <AlertDialogContent>\n                    <AlertDialogHeader>\n                        <AlertDialogTitle>Delete User</AlertDialogTitle>\n                        <AlertDialogDescription>\n                            Are you sure you want to delete {selectedUser?.email}? This action cannot be undone.\n                        </AlertDialogDescription>\n                    </AlertDialogHeader>\n                    <AlertDialogFooter>\n                        <AlertDialogCancel>Cancel</AlertDialogCancel>\n                        <AlertDialogAction\n                            onClick={() => selectedUser && deleteUser.mutate(selectedUser.id)}\n                            className=\"bg-red-600 hover:bg-red-700 focus:ring-red-600\"\n                        >\n                            {deleteUser.isPending ? 'Deleting...' : 'Delete User'}\n                        </AlertDialogAction>\n                    </AlertDialogFooter>\n                </AlertDialogContent>\n            </AlertDialog>\n\n            {/* Invite User Dialog */}\n            <Dialog open={isInviteDialogOpen} onOpenChange={setIsInviteDialogOpen}>\n                <DialogContent>\n                    <DialogHeader>\n                        <DialogTitle>Invite New User</DialogTitle>\n                        <DialogDescription>\n                            Create an invitation link for a new user to join.\n                        </DialogDescription>\n                    </DialogHeader>\n                    <form onSubmit={handleCreateInvitation}>\n                        <div className=\"grid gap-4 py-4\">\n                            <div className=\"grid gap-2\">\n                                <Label htmlFor=\"email\">Email</Label>\n                                <Input\n                                    id=\"email\"\n                                    type=\"email\"\n                                    value={inviteEmail}\n                                    onChange={(e) => setInviteEmail(e.target.value)}\n                                    placeholder=\"user@example.com\"\n                                />\n                            </div>\n                            <div className=\"grid gap-2\">\n                                <Label htmlFor=\"role\">Role</Label>\n                                <Select\n                                    value={inviteRole}\n                                    onValueChange={(value) => setInviteRole(value as Role)}\n                                >\n                                    <SelectTrigger>\n                                        <SelectValue placeholder=\"Select a role\"/>\n                                    </SelectTrigger>\n                                    <SelectContent>\n                                        <SelectItem value=\"STAFF\">Staff</SelectItem>\n                                        <SelectItem value=\"ADMIN\">Admin</SelectItem>\n                                    </SelectContent>\n                                </Select>\n                            </div>\n                        </div>\n                        <DialogFooter>\n                            <Button\n                                type=\"submit\"\n                                disabled={!inviteEmail.trim() || createInvitation.isPending}\n                            >\n                                {createInvitation.isPending ? 'Creating...' : 'Create Invitation'}\n                            </Button>\n                        </DialogFooter>\n                    </form>\n                </DialogContent>\n            </Dialog>\n\n            {/* New Invitation URL Alert */}\n            <AnimatePresence>\n                {newInvitationUrl && (\n                    <AlertDialog open onOpenChange={() => setNewInvitationUrl(null)}>\n                        <AlertDialogContent>\n                            <AlertDialogHeader>\n                                <AlertDialogTitle>Invitation Link Created</AlertDialogTitle>\n                                <AlertDialogDescription>\n                                    Copy this invitation link and share it with the user. The link will expire in 7\n                                    days.\n                                </AlertDialogDescription>\n                            </AlertDialogHeader>\n                            <div className=\"flex items-center gap-2 p-2 bg-muted rounded-md\">\n                                <code className=\"flex-1 text-sm break-all\">{newInvitationUrl}</code>\n                                <Button\n                                    size=\"sm\"\n                                    variant=\"ghost\"\n                                    onClick={() => handleCopyInvitationLink(newInvitationUrl)}\n                                >\n                                    <Copy className=\"h-4 w-4\"/>\n                                </Button>\n                            </div>\n                            <AlertDialogFooter>\n                                <AlertDialogAction onClick={() => setNewInvitationUrl(null)}>\n                                    Done\n                                </AlertDialogAction>\n                            </AlertDialogFooter>\n                        </AlertDialogContent>\n                    </AlertDialog>\n                )}\n            </AnimatePresence>\n\n            {/* Page Header */}\n            <div className=\"flex flex-col gap-4 md:flex-row md:items-center md:justify-between\">\n                <div>\n                    <h1 className=\"text-3xl font-bold tracking-tight\">User Management</h1>\n                    <p className=\"text-muted-foreground mt-2\">\n                        Manage team members and their permissions.\n                    </p>\n                </div>\n                <Button onClick={() => setIsInviteDialogOpen(true)}>\n                    <UserPlus className=\"h-4 w-4 mr-2\"/>\n                    Invite User\n                </Button>\n            </div>\n\n            {/* Search Bar */}\n            <div className=\"flex items-center gap-2\">\n                <div className=\"relative flex-1\">\n                    <Search className=\"absolute left-2 top-2.5 h-4 w-4 text-muted-foreground\"/>\n                    <Input\n                        placeholder=\"Search users by name or email...\"\n                        value={searchTerm}\n                        onChange={(e) => setSearchTerm(e.target.value)}\n                        className=\"pl-8\"\n                    />\n                </div>\n                {searchTerm && (\n                    <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        onClick={() => setSearchTerm('')}\n                        className=\"px-2\"\n                    >\n                        <X className=\"h-4 w-4\"/>\n                    </Button>\n                )}\n            </div>\n\n            {/* Main Content */}\n            <Tabs defaultValue=\"users\" className=\"space-y-6\">\n                <TabsList>\n                    <TabsTrigger value=\"users\" className=\"relative\">\n                        Users\n                        {users?.length ? (\n                            <span className=\"ml-2 rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground\">\n                                {users.length}\n                            </span>\n                        ) : null}\n                    </TabsTrigger>\n                    <TabsTrigger value=\"invitations\" className=\"relative\">\n                        Pending Invitations\n                        {invitations?.filter(i => !i.usedAt)?.length ? (\n                            <span className=\"ml-2 rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground\">\n                                {invitations.filter(i => !i.usedAt).length}\n                            </span>\n                        ) : null}\n                    </TabsTrigger>\n                </TabsList>\n\n                <TabsContent value=\"users\">\n                    <Card>\n                        <CardHeader>\n                            <CardTitle>Users</CardTitle>\n                            <CardDescription>\n                                Manage user accounts and their permissions.\n                            </CardDescription>\n                        </CardHeader>\n                        <CardContent>\n                            <Table>\n                                <TableHeader>\n                                    <TableRow>\n                                        <TableHead>User</TableHead>\n                                        <TableHead>Role</TableHead>\n                                        <TableHead>Joined</TableHead>\n                                        <TableHead>Last Login</TableHead>\n                                        <TableHead className=\"text-right\">Actions</TableHead>\n                                    </TableRow>\n                                </TableHeader>\n                                <TableBody>\n                                    {filteredUsers?.map((user) => (\n                                        <TableRow key={user.id}>\n                                            <TableCell>\n                                                <div className=\"flex items-center gap-2\">\n                                                    <div\n                                                        className=\"h-8 w-8 rounded-full bg-muted flex items-center justify-center\">\n                                                        <User className=\"h-4 w-4 text-muted-foreground\"/>\n                                                    </div>\n                                                    <div>\n                                                        <div className=\"font-medium\">{user.name || 'No name'}</div>\n                                                        <div className=\"text-sm text-muted-foreground\">\n                                                            {user.email}\n                                                        </div>\n                                                    </div>\n                                                </div>\n                                            </TableCell>\n                                            <TableCell>\n                                                <span\n                                                    className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium ${\n                                                        user.role === 'ADMIN'\n                                                            ? 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'\n                                                            : 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'\n                                                    }`}\n                                                >\n                                                    {user.role === 'ADMIN' && <Shield className=\"h-3 w-3\"/>}\n                                                    {user.role}\n                                                </span>\n                                            </TableCell>\n                                            <TableCell>{format(new Date(user.createdAt), 'PP')}</TableCell>\n                                            <TableCell>\n                                                {user.lastLoginAt\n                                                    ? format(new Date(user.lastLoginAt), 'PP')\n                                                    : 'Never'}\n                                            </TableCell>\n                                            <TableCell className=\"text-right\">\n                                                <DropdownMenu>\n                                                    <DropdownMenuTrigger asChild>\n                                                        <Button variant=\"ghost\" size=\"sm\">\n                                                            <MoreVertical className=\"h-4 w-4\"/>\n                                                        </Button>\n                                                    </DropdownMenuTrigger>\n                                                    <DropdownMenuContent align=\"end\">\n                                                        <DropdownMenuItem onClick={() => handleEditUser(user)}>\n                                                            <Pencil className=\"h-4 w-4 mr-2\"/>\n                                                            Edit User\n                                                        </DropdownMenuItem>\n                                                        <DropdownMenuSeparator/>\n                                                        <DropdownMenuItem\n                                                            className=\"text-red-600 dark:text-red-400\"\n                                                            onClick={() => handleDeleteUser(user)}\n                                                        >\n                                                            <Trash2 className=\"h-4 w-4 mr-2\"/>\n                                                            Delete User\n                                                        </DropdownMenuItem>\n                                                    </DropdownMenuContent>\n                                                </DropdownMenu>\n                                            </TableCell>\n                                        </TableRow>\n                                    ))}\n                                    {(!filteredUsers || filteredUsers.length === 0) && (\n                                        <TableRow>\n                                            <TableCell colSpan={5} className=\"h-32 text-center\">\n                                                <div className=\"flex flex-col items-center justify-center\">\n                                                    <User className=\"h-12 w-12 text-muted-foreground mb-4\"/>\n                                                    <h3 className=\"font-medium mb-1\">No Users Found</h3>\n                                                    <p className=\"text-sm text-muted-foreground mb-4\">\n                                                        {searchTerm\n                                                            ? 'No users match your search criteria.'\n                                                            : 'Start by inviting team members.'}\n                                                    </p>\n                                                    {!searchTerm && (\n                                                        <Button\n                                                            onClick={() => setIsInviteDialogOpen(true)}\n                                                            size=\"sm\"\n                                                        >\n                                                            <UserPlus className=\"h-4 w-4 mr-2\"/>\n                                                            Invite User\n                                                        </Button>\n                                                    )}\n                                                </div>\n                                            </TableCell>\n                                        </TableRow>\n                                    )}\n                                </TableBody>\n                            </Table>\n                        </CardContent>\n                    </Card>\n                </TabsContent>\n\n                <TabsContent value=\"invitations\">\n                    <Card>\n                        <CardHeader>\n                            <CardTitle>Pending Invitations</CardTitle>\n                            <CardDescription>\n                                Manage outstanding invitation links.\n                            </CardDescription>\n                        </CardHeader>\n                        <CardContent>\n                            <Table>\n                                <TableHeader>\n                                    <TableRow>\n                                        <TableHead>Email</TableHead>\n                                        <TableHead>Role</TableHead>\n                                        <TableHead>Created</TableHead>\n                                        <TableHead>Expires</TableHead>\n                                        <TableHead>Status</TableHead>\n                                        <TableHead className=\"text-right\">Actions</TableHead>\n                                    </TableRow>\n                                </TableHeader>\n                                <TableBody>\n                                    {invitations?.map((invitation) => {\n                                        const isExpired = new Date(invitation.expiresAt) < new Date();\n                                        const isUsed = !!invitation.usedAt;\n                                        const isActive = !isExpired && !isUsed;\n\n                                        return (\n                                            <TableRow key={invitation.id}>\n                                                <TableCell>{invitation.email}</TableCell>\n                                                <TableCell>\n                                                    <span\n                                                        className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium ${\n                                                            invitation.role === 'ADMIN'\n                                                                ? 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'\n                                                                : 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'\n                                                        }`}\n                                                    >\n                                                        {invitation.role === 'ADMIN' && <Shield className=\"h-3 w-3\"/>}\n                                                        {invitation.role}\n                                                    </span>\n                                                </TableCell>\n                                                <TableCell>{format(new Date(invitation.createdAt), 'PP')}</TableCell>\n                                                <TableCell>{format(new Date(invitation.expiresAt), 'PP')}</TableCell>\n                                                <TableCell>\n                                                    <span\n                                                        className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium ${\n                                                            isActive\n                                                                ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'\n                                                                : isUsed\n                                                                    ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'\n                                                                    : 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'\n                                                        }`}\n                                                    >\n                                                        {isActive && <Check className=\"h-3 w-3\"/>}\n                                                        {isActive ? 'Active' : isUsed ? 'Used' : 'Expired'}\n                                                    </span>\n                                                </TableCell>\n                                                <TableCell className=\"text-right\">\n                                                    <div className=\"flex items-center justify-end gap-2\">\n                                                        {isActive && (\n                                                            <>\n                                                                <Button\n                                                                    variant=\"ghost\"\n                                                                    size=\"sm\"\n                                                                    onClick={() => handleCopyInvitationLink(\n                                                                        `${window.location.origin}/register/${invitation.token}`\n                                                                    )}\n                                                                >\n                                                                    <Copy className=\"h-4 w-4\"/>\n                                                                </Button>\n                                                                <AlertDialog>\n                                                                    <AlertDialogTrigger asChild>\n                                                                        <Button\n                                                                            variant=\"ghost\"\n                                                                            size=\"sm\"\n                                                                            className=\"text-red-600 hover:text-red-700 dark:text-red-500 dark:hover:text-red-400\"\n                                                                        >\n                                                                            <Trash2 className=\"h-4 w-4\"/>\n                                                                        </Button>\n                                                                    </AlertDialogTrigger>\n                                                                    <AlertDialogContent>\n                                                                        <AlertDialogHeader>\n                                                                            <AlertDialogTitle>Revoke\n                                                                                Invitation</AlertDialogTitle>\n                                                                            <AlertDialogDescription>\n                                                                                This will invalidate the invitation link\n                                                                                for {invitation.email}.\n                                                                            </AlertDialogDescription>\n                                                                        </AlertDialogHeader>\n                                                                        <AlertDialogFooter>\n                                                                            <AlertDialogCancel>Cancel</AlertDialogCancel>\n                                                                            <AlertDialogAction\n                                                                                onClick={() => revokeInvitation.mutate(invitation.id)}\n                                                                                className=\"bg-red-600 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-600\"\n                                                                            >\n                                                                                Revoke Invitation\n                                                                            </AlertDialogAction>\n                                                                        </AlertDialogFooter>\n                                                                    </AlertDialogContent>\n                                                                </AlertDialog>\n                                                            </>\n                                                        )}\n                                                    </div>\n                                                </TableCell>\n                                            </TableRow>\n                                        );\n                                    })}\n                                    {(!invitations || invitations.length === 0) && (\n                                        <TableRow>\n                                            <TableCell colSpan={6} className=\"h-32 text-center\">\n                                                <div className=\"flex flex-col items-center justify-center\">\n                                                    <User className=\"h-12 w-12 text-muted-foreground mb-4\"/>\n                                                    <h3 className=\"font-medium mb-1\">No Pending Invitations</h3>\n                                                    <p className=\"text-sm text-muted-foreground mb-4\">\n                                                        Create an invitation to add new team members.\n                                                    </p>\n                                                    <Button\n                                                        onClick={() => setIsInviteDialogOpen(true)}\n                                                        size=\"sm\"\n                                                    >\n                                                        <UserPlus className=\"h-4 w-4 mr-2\"/>\n                                                        Invite User\n                                                    </Button>\n                                                </div>\n                                            </TableCell>\n                                        </TableRow>\n                                    )}\n                                </TableBody>\n                            </Table>\n                        </CardContent>\n                    </Card>\n                </TabsContent>\n            </Tabs>\n        </motion.div>\n    );\n}"
  },
  {
    "path": "app/dashboard/bookmarks/page.tsx",
    "content": "// /app/dashboard/bookmarks/page.tsx\n\n'use client'\n\nimport React, {useState, useMemo, useEffect} from 'react'\nimport Link from 'next/link'\nimport {useQuery} from '@tanstack/react-query'\nimport {\n    Star,\n    Search,\n    MoreVertical,\n    ExternalLink,\n    Trash2,\n    FolderOpen,\n    FileText,\n    Eye,\n    Edit3,\n    Clock,\n    Download,\n    Upload,\n    RefreshCw,\n    SortAsc,\n    SearchIcon,\n} from 'lucide-react'\nimport {Button} from '@/components/ui/button'\nimport {Input} from '@/components/ui/input'\nimport {Badge} from '@/components/ui/badge'\nimport {Skeleton} from '@/components/ui/skeleton'\nimport {Card, CardContent} from '@/components/ui/card'\nimport {\n    DropdownMenu,\n    DropdownMenuContent,\n    DropdownMenuItem,\n    DropdownMenuSeparator,\n    DropdownMenuTrigger\n} from '@/components/ui/dropdown-menu'\nimport {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from '@/components/ui/tooltip'\nimport {formatDistanceToNow} from 'date-fns'\nimport {BookmarkService, type BookmarkedItem} from '@/lib/services/bookmarks/bookmark.service'\n\ninterface Project {\n    id: string\n    name: string\n    isPublic?: boolean\n}\n\ninterface EnhancedBookmark extends BookmarkedItem {\n    project?: Project\n    changelogEntry?: {\n        version?: string\n        publishedAt?: string\n        isPublished: boolean\n        tags: Array<{ id: string; name: string }>\n    }\n}\n\ntype SortOption = 'recent' | 'alphabetical' | 'project'\n\nexport default function BookmarksPage() {\n    const [searchQuery, setSearchQuery] = useState('')\n    const [sortBy, setSortBy] = useState<SortOption>('recent')\n    const [allBookmarks, setAllBookmarks] = useState<BookmarkedItem[]>([])\n    const [isLoadingBookmarks, setIsLoadingBookmarks] = useState(true)\n\n    // Load all bookmarks using the service directly\n    useEffect(() => {\n        const loadBookmarks = () => {\n            try {\n                setIsLoadingBookmarks(true)\n                const bookmarksByProject = BookmarkService.getAllBookmarks()\n                const flatBookmarks: BookmarkedItem[] = []\n\n                // eslint-disable-next-line @typescript-eslint/no-unused-vars\n                Object.entries(bookmarksByProject).forEach(([projectId, bookmarks]) => {\n                    flatBookmarks.push(...bookmarks.map(b => ({\n                        ...b,\n                        bookmarkedAt: b.bookmarkedAt || new Date().toISOString()\n                    })))\n                })\n\n                setAllBookmarks(flatBookmarks)\n            } catch (error) {\n                console.error('Failed to load bookmarks:', error)\n                setAllBookmarks([])\n            } finally {\n                setIsLoadingBookmarks(false)\n            }\n        }\n\n        loadBookmarks()\n\n        // Listen for storage changes to update bookmarks in real-time\n        const handleStorageChange = (event: StorageEvent) => {\n            if (event.key?.startsWith('bookmarked-') || event.key === 'changerawr-global-bookmarks') {\n                loadBookmarks()\n            }\n        }\n\n        window.addEventListener('storage', handleStorageChange)\n        return () => window.removeEventListener('storage', handleStorageChange)\n    }, [])\n\n    // Get unique project IDs from bookmarks\n    const projectIds = useMemo(() =>\n            [...new Set(allBookmarks.map(b => b.projectId))],\n        [allBookmarks]\n    )\n\n    // Fetch project details for all bookmarked projects\n    const {data: projects = [], isLoading: isLoadingProjects} = useQuery<Project[]>({\n        queryKey: ['projects-minimal', projectIds],\n        queryFn: async () => {\n            if (projectIds.length === 0) return []\n\n            const projectPromises = projectIds.map(async (projectId) => {\n                try {\n                    const response = await fetch(`/api/projects/${projectId}`)\n                    if (!response.ok) {\n                        return {id: projectId, name: 'Unknown Project'}\n                    }\n                    const project = await response.json()\n                    return {\n                        id: project.id,\n                        name: project.name,\n                        isPublic: project.isPublic\n                    }\n                } catch {\n                    return {id: projectId, name: 'Unknown Project'}\n                }\n            })\n\n            return Promise.all(projectPromises)\n        },\n        enabled: projectIds.length > 0,\n        staleTime: 300000,\n    })\n\n    // Fetch changelog entry details for bookmarks\n    const {data: enrichedBookmarks = [], isLoading: isLoadingEntries} = useQuery<EnhancedBookmark[]>({\n        queryKey: ['enriched-bookmarks', allBookmarks, projects],\n        queryFn: async () => {\n            if (allBookmarks.length === 0) return []\n\n            const enriched = await Promise.all(\n                allBookmarks.map(async (bookmark) => {\n                    const project = projects.find(p => p.id === bookmark.projectId) || {\n                        id: bookmark.projectId,\n                        name: 'Unknown Project'\n                    }\n\n                    let changelogEntry = undefined\n\n                    try {\n                        const response = await fetch(\n                            `/api/projects/${bookmark.projectId}/changelog/${bookmark.id}`\n                        )\n                        if (response.ok) {\n                            const entry = await response.json()\n                            changelogEntry = {\n                                version: entry.version,\n                                publishedAt: entry.publishedAt,\n                                isPublished: !!entry.publishedAt,\n                                tags: entry.tags || []\n                            }\n                        }\n                    } catch {\n                        changelogEntry = {\n                            version: undefined,\n                            publishedAt: undefined,\n                            isPublished: false,\n                            tags: []\n                        }\n                    }\n\n                    return {\n                        ...bookmark,\n                        project,\n                        changelogEntry\n                    }\n                })\n            )\n\n            return enriched\n        },\n        enabled: allBookmarks.length > 0,\n        staleTime: 60000,\n    })\n\n    // Group bookmarks by project\n    const groupedBookmarks = useMemo(() => {\n        const filtered = enrichedBookmarks.filter(bookmark => {\n            if (!searchQuery.trim()) return true\n            const query = searchQuery.toLowerCase()\n            return bookmark.title.toLowerCase().includes(query) ||\n                bookmark.project?.name.toLowerCase().includes(query)\n        })\n\n        // Sort bookmarks\n        filtered.sort((a, b) => {\n            switch (sortBy) {\n                case 'recent':\n                    return new Date(b.bookmarkedAt || 0).getTime() - new Date(a.bookmarkedAt || 0).getTime()\n                case 'alphabetical':\n                    return a.title.localeCompare(b.title)\n                case 'project':\n                    return (a.project?.name || '').localeCompare(b.project?.name || '')\n                default:\n                    return 0\n            }\n        })\n\n        // Group by project\n        const grouped: Record<string, EnhancedBookmark[]> = {}\n        filtered.forEach(bookmark => {\n            const projectName = bookmark.project?.name || 'Unknown Project'\n            if (!grouped[projectName]) {\n                grouped[projectName] = []\n            }\n            grouped[projectName].push(bookmark)\n        })\n\n        return grouped\n    }, [enrichedBookmarks, searchQuery, sortBy])\n\n    // Handlers\n    const handleRemoveBookmark = async (bookmark: EnhancedBookmark) => {\n        const success = BookmarkService.removeBookmark(bookmark.id, bookmark.projectId)\n        if (success) {\n            setAllBookmarks(prev => prev.filter(b => !(b.id === bookmark.id && b.projectId === bookmark.projectId)))\n        }\n    }\n\n    const handleExportBookmarks = () => {\n        const data = BookmarkService.exportBookmarks()\n        const blob = new Blob([data], {type: 'application/json'})\n        const url = URL.createObjectURL(blob)\n        const a = document.createElement('a')\n        a.href = url\n        a.download = `changerawr-bookmarks-${new Date().toISOString().split('T')[0]}.json`\n        document.body.appendChild(a)\n        a.click()\n        document.body.removeChild(a)\n        URL.revokeObjectURL(url)\n    }\n\n    const handleImportBookmarks = (event: React.ChangeEvent<HTMLInputElement>) => {\n        const file = event.target.files?.[0]\n        if (!file) return\n\n        const reader = new FileReader()\n        reader.onload = (e) => {\n            try {\n                const content = e.target?.result as string\n                const success = BookmarkService.importBookmarks(content)\n                if (success) {\n                    window.location.reload()\n                } else {\n                    alert('Failed to import bookmarks. Please check the file format.')\n                }\n            } catch {\n                alert('Failed to read the import file.')\n            }\n        }\n        reader.readAsText(file)\n        event.target.value = ''\n    }\n\n    const isLoading = isLoadingBookmarks || isLoadingProjects || isLoadingEntries\n    const totalBookmarks = enrichedBookmarks.length\n    const filteredCount = Object.values(groupedBookmarks).flat().length\n\n    return (\n        <div className=\"min-h-screen bg-background\">\n            {/* Header */}\n            <div className=\"bg-card border-b sticky top-0 z-10\">\n                <div className=\"max-w-7xl mx-auto px-6 py-4\">\n                    <div className=\"flex items-center justify-between\">\n                        <div className=\"flex items-center gap-4\">\n                            <div className=\"flex items-center gap-3\">\n                                <Star className=\"h-6 w-6 text-amber-500\"/>\n                                <h1 className=\"text-xl font-semibold text-foreground\">Bookmarks</h1>\n                                {!isLoading && (\n                                    <span className=\"text-sm text-muted-foreground\">\n                                        {filteredCount !== totalBookmarks\n                                            ? `${filteredCount} of ${totalBookmarks} bookmarks`\n                                            : `${totalBookmarks} bookmarks`\n                                        }\n                                    </span>\n                                )}\n                            </div>\n                        </div>\n\n                        <div className=\"flex items-center gap-3\">\n                            {/* Search */}\n                            <div className=\"relative\">\n                                <Search\n                                    className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground\"/>\n                                <Input\n                                    startIcon={<SearchIcon />}\n                                    placeholder=\"Search bookmarks\"\n                                    value={searchQuery}\n                                    onChange={(e) => setSearchQuery(e.target.value)}\n                                    className=\"pl-10 w-64\"\n                                />\n                            </div>\n\n                            {/* Sort */}\n                            <DropdownMenu>\n                                <DropdownMenuTrigger asChild>\n                                    <Button variant=\"outline\" size=\"sm\">\n                                        {sortBy === 'recent' && <Clock className=\"h-4 w-4 mr-2\"/>}\n                                        {sortBy === 'alphabetical' && <SortAsc className=\"h-4 w-4 mr-2\"/>}\n                                        {sortBy === 'project' && <FolderOpen className=\"h-4 w-4 mr-2\"/>}\n                                        Sort\n                                    </Button>\n                                </DropdownMenuTrigger>\n                                <DropdownMenuContent align=\"end\">\n                                    <DropdownMenuItem onClick={() => setSortBy('recent')}>\n                                        <Clock className=\"h-4 w-4 mr-2\"/>\n                                        Recently added\n                                    </DropdownMenuItem>\n                                    <DropdownMenuItem onClick={() => setSortBy('alphabetical')}>\n                                        <SortAsc className=\"h-4 w-4 mr-2\"/>\n                                        Alphabetical\n                                    </DropdownMenuItem>\n                                    <DropdownMenuItem onClick={() => setSortBy('project')}>\n                                        <FolderOpen className=\"h-4 w-4 mr-2\"/>\n                                        By project\n                                    </DropdownMenuItem>\n                                </DropdownMenuContent>\n                            </DropdownMenu>\n\n                            {/* More Actions */}\n                            <DropdownMenu>\n                                <DropdownMenuTrigger asChild>\n                                    <Button variant=\"outline\" size=\"sm\">\n                                        <MoreVertical className=\"h-4 w-4\"/>\n                                    </Button>\n                                </DropdownMenuTrigger>\n                                <DropdownMenuContent align=\"end\">\n                                    <DropdownMenuItem onClick={handleExportBookmarks}>\n                                        <Download className=\"h-4 w-4 mr-2\"/>\n                                        Export bookmarks\n                                    </DropdownMenuItem>\n                                    <DropdownMenuItem asChild>\n                                        <label className=\"cursor-pointer flex items-center\">\n                                            <Upload className=\"h-4 w-4 mr-2\"/>\n                                            Import bookmarks\n                                            <input\n                                                type=\"file\"\n                                                accept=\".json\"\n                                                onChange={handleImportBookmarks}\n                                                className=\"hidden\"\n                                            />\n                                        </label>\n                                    </DropdownMenuItem>\n                                    <DropdownMenuSeparator/>\n                                    <DropdownMenuItem onClick={() => window.location.reload()}>\n                                        <RefreshCw className=\"h-4 w-4 mr-2\"/>\n                                        Refresh\n                                    </DropdownMenuItem>\n                                </DropdownMenuContent>\n                            </DropdownMenu>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            {/* Content */}\n            <div className=\"max-w-7xl mx-auto px-6 py-6\">\n                {isLoading ? (\n                    <div className=\"space-y-6\">\n                        {Array.from({length: 3}).map((_, i) => (\n                            <div key={i} className=\"space-y-3\">\n                                <Skeleton className=\"h-6 w-48\"/>\n                                <div className=\"space-y-2\">\n                                    {Array.from({length: 3}).map((_, j) => (\n                                        <Card key={j}>\n                                            <CardContent className=\"flex items-center gap-4 p-3\">\n                                                <Skeleton className=\"h-4 w-4\"/>\n                                                <Skeleton className=\"h-5 w-1/3\"/>\n                                                <Skeleton className=\"h-4 w-1/4\"/>\n                                                <Skeleton className=\"h-4 w-1/6\"/>\n                                            </CardContent>\n                                        </Card>\n                                    ))}\n                                </div>\n                            </div>\n                        ))}\n                    </div>\n                ) : Object.keys(groupedBookmarks).length === 0 ? (\n                    <div className=\"text-center py-12\">\n                        <Star className=\"h-16 w-16 text-muted-foreground mx-auto mb-4\"/>\n                        <h3 className=\"text-lg font-semibold text-foreground mb-2\">\n                            {allBookmarks.length === 0 ? 'No bookmarks yet' : 'No bookmarks match your search'}\n                        </h3>\n                        <p className=\"text-muted-foreground mb-6 max-w-md mx-auto\">\n                            {allBookmarks.length === 0\n                                ? 'Start bookmarking changelog entries from the editor to see them here.'\n                                : 'Try adjusting your search terms or clear the search to see all bookmarks.'\n                            }\n                        </p>\n                        {allBookmarks.length === 0 && (\n                            <Button asChild>\n                                <Link href=\"/dashboard/projects\">\n                                    Browse Projects\n                                </Link>\n                            </Button>\n                        )}\n                    </div>\n                ) : (\n                    <div className=\"space-y-8\">\n                        {Object.entries(groupedBookmarks).map(([projectName, bookmarks]) => (\n                            <div key={projectName} className=\"space-y-3\">\n                                {/* Project Header */}\n                                <div className=\"flex items-center gap-3 pb-2 border-b\">\n                                    <FolderOpen className=\"h-5 w-5 text-primary\"/>\n                                    <h2 className=\"text-lg font-semibold text-foreground\">{projectName}</h2>\n                                    <span className=\"text-sm text-muted-foreground\">({bookmarks.length})</span>\n                                </div>\n\n                                {/* Bookmarks List */}\n                                <div className=\"space-y-1\">\n                                    {bookmarks.map((bookmark) => (\n                                        <BookmarkRow\n                                            key={`${bookmark.projectId}-${bookmark.id}`}\n                                            bookmark={bookmark}\n                                            onRemove={() => handleRemoveBookmark(bookmark)}\n                                        />\n                                    ))}\n                                </div>\n                            </div>\n                        ))}\n                    </div>\n                )}\n            </div>\n        </div>\n    )\n}\n\n// Chrome-style Bookmark Row\ninterface BookmarkRowProps {\n    bookmark: EnhancedBookmark\n    onRemove: () => void\n}\n\nfunction BookmarkRow({bookmark, onRemove}: BookmarkRowProps) {\n    const isPublished = bookmark.changelogEntry?.isPublished\n    const version = bookmark.changelogEntry?.version\n\n    return (\n        <Card className=\"group hover:bg-accent/50 transition-colors\">\n            <CardContent className=\"flex items-center gap-4 p-3\">\n                {/* Favicon/Icon */}\n                <div className=\"flex-shrink-0\">\n                    <div className=\"h-4 w-4 bg-primary/10 rounded flex items-center justify-center\">\n                        <FileText className=\"h-3 w-3 text-primary\"/>\n                    </div>\n                </div>\n\n                {/* Title and URL */}\n                <div className=\"flex-1 min-w-0\">\n                    <div className=\"flex items-center gap-2 mb-1\">\n                        <Link\n                            href={`/dashboard/projects/${bookmark.projectId}/changelog/${bookmark.id}`}\n                            className=\"font-medium text-primary hover:text-primary/80 hover:underline truncate text-sm\"\n                        >\n                            {bookmark.title}\n                        </Link>\n\n                        {/* Status Badge */}\n                        {isPublished ? (\n                            <Badge variant=\"secondary\"\n                                   className=\"bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/20 text-xs px-1.5 py-0.5\">\n                                <Eye className=\"h-2.5 w-2.5 mr-1\"/>\n                                Live\n                            </Badge>\n                        ) : (\n                            <Badge variant=\"secondary\"\n                                   className=\"bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20 text-xs px-1.5 py-0.5\">\n                                <Edit3 className=\"h-2.5 w-2.5 mr-1\"/>\n                                Draft\n                            </Badge>\n                        )}\n\n                        {/* Version */}\n                        {version && (\n                            <Badge variant=\"outline\" className=\"text-xs px-1.5 py-0.5\">\n                                {version.startsWith('v') ? version : `v${version}`}\n                            </Badge>\n                        )}\n                    </div>\n\n                    {/* URL/Path */}\n                    <p className=\"text-xs text-muted-foreground truncate\">\n                        /dashboard/projects/{bookmark.projectId}/changelog/{bookmark.id}\n                        {bookmark.bookmarkedAt && (\n                            <span className=\"ml-2\">\n                                • Added {formatDistanceToNow(new Date(bookmark.bookmarkedAt))} ago\n                            </span>\n                        )}\n                    </p>\n                </div>\n\n                {/* Actions */}\n                <div className=\"flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity\">\n                    <TooltipProvider>\n                        <Tooltip>\n                            <TooltipTrigger asChild>\n                                <Button\n                                    variant=\"ghost\"\n                                    size=\"sm\"\n                                    className=\"h-7 w-7 p-0\"\n                                    asChild\n                                >\n                                    <Link\n                                        href={`/dashboard/projects/${bookmark.projectId}/changelog/${bookmark.id}`}\n                                        target=\"_blank\"\n                                        rel=\"noopener noreferrer\"\n                                    >\n                                        <ExternalLink className=\"h-3.5 w-3.5\"/>\n                                    </Link>\n                                </Button>\n                            </TooltipTrigger>\n                            <TooltipContent>Open in new tab</TooltipContent>\n                        </Tooltip>\n                    </TooltipProvider>\n\n                    <DropdownMenu>\n                        <DropdownMenuTrigger asChild>\n                            <Button variant=\"ghost\" size=\"sm\" className=\"h-7 w-7 p-0\">\n                                <MoreVertical className=\"h-3.5 w-3.5\"/>\n                            </Button>\n                        </DropdownMenuTrigger>\n                        <DropdownMenuContent align=\"end\">\n                            <DropdownMenuItem asChild>\n                                <Link href={`/dashboard/projects/${bookmark.projectId}/changelog/${bookmark.id}`}>\n                                    <FileText className=\"h-4 w-4 mr-2\"/>\n                                    Open\n                                </Link>\n                            </DropdownMenuItem>\n                            <DropdownMenuItem asChild>\n                                <Link\n                                    href={`/dashboard/projects/${bookmark.projectId}/changelog/${bookmark.id}`}\n                                    target=\"_blank\"\n                                    rel=\"noopener noreferrer\"\n                                >\n                                    <ExternalLink className=\"h-4 w-4 mr-2\"/>\n                                    Open in new tab\n                                </Link>\n                            </DropdownMenuItem>\n                            <DropdownMenuSeparator/>\n                            <DropdownMenuItem\n                                onClick={onRemove}\n                                className=\"text-destructive focus:text-destructive\"\n                            >\n                                <Trash2 className=\"h-4 w-4 mr-2\"/>\n                                Remove\n                            </DropdownMenuItem>\n                        </DropdownMenuContent>\n                    </DropdownMenu>\n                </div>\n            </CardContent>\n        </Card>\n    )\n}"
  },
  {
    "path": "app/dashboard/layout.tsx",
    "content": "'use client'\n\nimport React, {useEffect, useState} from 'react'\nimport {useRouter} from 'next/navigation'\nimport {useAuth} from '@/context/auth'\nimport {\n    BookmarkIcon,\n    ChartNoAxesCombined,\n    ClipboardCheck,\n    Clock,\n    FileText,\n    Fingerprint,\n    Folder,\n    Globe,\n    Info,\n    Key,\n    LayoutGrid,\n    Loader2,\n    PanelRightClose,\n    ServerCog,\n    Settings,\n    Shield,\n    Sparkles,\n    Users\n} from 'lucide-react'\nimport {cn} from '@/lib/utils'\nimport {useMediaQuery} from '@/hooks/use-media-query'\nimport WhatsNewModal from '@/components/dashboard/WhatsNewModal'\nimport {useWhatsNew} from '@/hooks/useWhatsNew'\nimport {NavSection, Sidebar, SidebarUser} from '@/components/ui/sidebar'\nimport {getGravatarUrl} from '@/lib/utils/gravatar'\nimport {CommandPaletteProvider} from \"@/components/providers/CommandPaletteProvider\"\nimport {TelemetryPromptModal} from '@/components/telemetry/PromptModal'\nimport {useTelemetry} from '@/hooks/useTelemetry'\n\n// Navigation Configuration\nconst NAV_SECTIONS: NavSection[] = [\n    {\n        title: \"Main\",\n        items: [\n            {\n                href: \"/dashboard\",\n                label: \"Dashboard\",\n                icon: LayoutGrid,\n                requiredRole: ['ADMIN', 'STAFF']\n            },\n            {\n                href: \"/dashboard/projects\",\n                label: \"Projects\",\n                icon: Folder,\n                requiredRole: ['ADMIN', 'STAFF']\n            },\n            {\n                href: \"/dashboard/requests\",\n                label: \"Requests\",\n                icon: Clock,\n                requiredRole: ['STAFF']\n            }\n        ]\n    },\n    {\n        title: \"Admin\",\n        items: [\n            {\n                href: \"/dashboard/admin\",\n                label: \"Overview\",\n                icon: Shield,\n                requiredRole: ['ADMIN']\n            },\n            {\n                href: \"/dashboard/admin/users\",\n                label: \"User Management\",\n                icon: Users,\n                requiredRole: ['ADMIN']\n            },\n            {\n                href: \"/dashboard/admin/analytics\",\n                label: \"Analytics\",\n                icon: ChartNoAxesCombined,\n                requiredRole: ['ADMIN']\n            },\n            {\n                href: \"/dashboard/admin/oauth\",\n                label: \"OAuth Providers\",\n                icon: Fingerprint,\n                requiredRole: ['ADMIN']\n            },\n            {\n                href: \"/dashboard/admin/domains\",\n                label: \"Domains\",\n                icon: Globe,\n                requiredRole: ['ADMIN']\n            },\n            {\n                href: \"/dashboard/admin/api-keys\",\n                label: \"API Keys\",\n                icon: Key,\n                requiredRole: ['ADMIN']\n            },\n            {\n                href: \"/dashboard/admin/audit-logs\",\n                label: \"Audit Logs\",\n                icon: FileText,\n                requiredRole: ['ADMIN']\n            },\n            {\n                href: \"/dashboard/admin/ai-settings\",\n                label: \"AI Integration\",\n                icon: Sparkles,\n                requiredRole: ['ADMIN']\n            },\n            {\n                href: \"/dashboard/admin/requests\",\n                label: \"Requests\",\n                icon: ClipboardCheck,\n                requiredRole: ['ADMIN']\n            },\n            {\n                href: \"/dashboard/admin/system\",\n                label: \"System Config\",\n                icon: ServerCog,\n                requiredRole: ['ADMIN']\n            },\n            {\n                href: \"/dashboard/admin/about\",\n                label: \"About\",\n                icon: Info,\n                requiredRole: ['ADMIN']\n            },\n        ]\n    },\n    {\n        title: \"Account\",\n        items: [\n            {\n                href: \"/dashboard/settings\",\n                label: \"Settings\",\n                icon: Settings,\n                requiredRole: ['ADMIN', 'STAFF']\n            },\n            {\n                href: \"/dashboard/bookmarks\",\n                label: \"Bookmarks\",\n                icon: BookmarkIcon,\n                requiredRole: ['ADMIN', 'STAFF']\n            },\n        ]\n    }\n]\n\n// Loading Component\nfunction LoadingScreen() {\n    return (\n        <div className=\"flex items-center justify-center min-h-screen\">\n            <div className=\"text-center\">\n                <Loader2 className=\"h-8 w-8 animate-spin mx-auto mb-4\"/>\n                <p className=\"text-sm text-muted-foreground\">Loading...</p>\n            </div>\n        </div>\n    )\n}\n\n// Main Layout Component\nexport default function DashboardLayout({\n                                            children,\n                                        }: {\n    children: React.ReactNode\n}) {\n    const router = useRouter()\n    const {user, isLoading, logout} = useAuth()\n    const [sidebarExpanded, setSidebarExpanded] = useState(true)\n    const [sidebarVisible, setSidebarVisible] = useState(true)\n    const isMobile = useMediaQuery('(max-width: 768px)')\n    const isTablet = useMediaQuery('(max-width: 1024px)')\n\n    // What's New modal state\n    const {\n        showWhatsNew,\n        whatsNewContent,\n        closeWhatsNew,\n        isLoading: isWhatsNewLoading\n    } = useWhatsNew()\n\n    // Telemetry modal state - always call hook but pass enabled flag\n    const {\n        showPrompt: showTelemetryPrompt,\n        handleTelemetryChoice,\n    } = useTelemetry(!!user)\n\n    // Convert Prisma User to SidebarUser\n    const sidebarUser: SidebarUser | null = user ? {\n        id: user.id,\n        name: user.name,\n        email: user.email,\n        role: user.role,\n        avatar: user.email ? getGravatarUrl(user.email, 80) : undefined\n    } : null\n\n    // Set sidebar state based on screen size\n    useEffect(() => {\n        if (isTablet) {\n            setSidebarExpanded(false)\n        } else {\n            setSidebarExpanded(true)\n        }\n    }, [isTablet])\n\n    useEffect(() => {\n        if (!isLoading && !user) {\n            router.push('/login')\n        }\n    }, [user, isLoading, router])\n\n    // Store sidebar state in localStorage\n    useEffect(() => {\n        if (!isTablet && !isMobile) {\n            try {\n                localStorage.setItem('sidebarExpanded', String(sidebarExpanded))\n                localStorage.setItem('sidebarVisible', String(sidebarVisible))\n            } catch (error) {\n                console.error('Error saving sidebar state:', error)\n            }\n        }\n    }, [sidebarExpanded, sidebarVisible, isTablet, isMobile])\n\n    // Load sidebar state from localStorage on initial load\n    useEffect(() => {\n        if (!isTablet && !isMobile) {\n            try {\n                const savedExpandedState = localStorage.getItem('sidebarExpanded')\n                const savedVisibleState = localStorage.getItem('sidebarVisible')\n                if (savedExpandedState !== null) {\n                    setSidebarExpanded(savedExpandedState === 'true')\n                }\n                if (savedVisibleState !== null) {\n                    setSidebarVisible(savedVisibleState === 'true')\n                }\n            } catch (error) {\n                console.error('Error loading sidebar state:', error)\n            }\n        }\n    }, [isTablet, isMobile])\n\n    // Show loading screen while checking auth\n    if (isLoading) {\n        return <LoadingScreen/>\n    }\n\n    // Redirect to login if no user\n    if (!user || !sidebarUser) {\n        return null\n    }\n\n    return (\n        <CommandPaletteProvider>\n            <div className=\"min-h-screen bg-background\">\n                <Sidebar\n                    user={sidebarUser}\n                    sections={NAV_SECTIONS}\n                    onLogout={logout}\n                    brandName=\"Changerawr\"\n                    brandHref=\"/dashboard\"\n                    isExpanded={sidebarExpanded}\n                    setIsExpanded={setSidebarExpanded}\n                    isVisible={sidebarVisible}\n                    // setIsVisible={setSidebarVisible}\n                />\n\n                {/* Main content area with proper responsive behavior */}\n                <main\n                    className={cn(\n                        \"min-h-screen transition-all duration-300 ease-in-out\",\n                        // Mobile: always full width with top padding for mobile header\n                        \"pt-16 md:pt-0\",\n                        // Desktop: adjust margin based on sidebar state\n                        !isMobile && (\n                            sidebarVisible\n                                ? (sidebarExpanded ? \"md:ml-64\" : \"md:ml-16\")\n                                : \"md:ml-0\"\n                        )\n                    )}\n                >\n                    {/* Show sidebar toggle when hidden */}\n                    {!sidebarVisible && !isMobile && (\n                        <div className=\"fixed top-4 left-4 z-50 group\">\n                            {/* Larger invisible hover trigger area */}\n                            <div className=\"absolute -inset-4 w-16 h-16\"/>\n                            <button\n                                onClick={() => setSidebarVisible(true)}\n                                className=\"relative p-2 bg-background border border-border rounded-md shadow-sm hover:bg-secondary transition-all duration-200 opacity-0 group-hover:opacity-100\"\n                                aria-label=\"Show sidebar\"\n                            >\n                                <PanelRightClose className=\"h-5 w-5\"/>\n                            </button>\n                        </div>\n                    )}\n\n                    <div className=\"h-full p-4 md:p-6 lg:p-8\">\n                        <div\n                            className={cn(\n                                \"mx-auto\",\n                                // When sidebar is hidden, use much wider container\n                                sidebarVisible ? \"max-w-7xl\" : \"max-w-none\"\n                            )}\n                        >\n                            {children}\n                        </div>\n                    </div>\n                </main>\n\n                {/* Telemetry Prompt Modal */}\n                {user && (\n                    <TelemetryPromptModal\n                        isOpen={showTelemetryPrompt}\n                        onChoice={handleTelemetryChoice}\n                    />\n                )}\n\n                {/* What's New Modal */}\n                {!isWhatsNewLoading && (\n                    <WhatsNewModal\n                        content={whatsNewContent}\n                        isOpen={showWhatsNew}\n                        onClose={closeWhatsNew}\n                    />\n                )}\n            </div>\n        </CommandPaletteProvider>\n    )\n}"
  },
  {
    "path": "app/dashboard/page.tsx",
    "content": "'use client'\n\nimport {useAuth} from '@/context/auth'\nimport {useQuery} from '@tanstack/react-query'\nimport Link from 'next/link'\nimport {motion} from 'framer-motion'\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader\n} from \"@/components/ui/card\"\nimport {Button} from \"@/components/ui/button\"\nimport {ScrollArea} from \"@/components/ui/scroll-area\"\nimport {Avatar, AvatarFallback, AvatarImage} from \"@/components/ui/avatar\"\nimport {Badge} from \"@/components/ui/badge\"\nimport {\n    FileText,\n    Plus,\n    ArrowRight,\n    Activity,\n    Settings,\n    Sparkles,\n    BookOpen,\n    ChevronRight,\n    Code,\n    LayoutDashboard,\n    TrendingUp,\n    Target,\n    PenTool\n} from 'lucide-react'\nimport {formatDistanceToNow} from \"date-fns\"\nimport type {\n    DashboardStats,\n    ProjectPreview,\n    Activity as DashboardActivity\n} from '@/lib/types/dashboard'\nimport {getGravatarUrl} from \"@/lib/utils/gravatar\"\nimport React from \"react\"\n\nconst fadeIn = {\n    initial: {opacity: 0, y: 10},\n    animate: {opacity: 1, y: 0},\n    exit: {opacity: 0, y: -10}\n}\n\nconst container = {\n    hidden: {opacity: 0},\n    show: {\n        opacity: 1,\n        transition: {\n            staggerChildren: 0.08\n        }\n    }\n}\n\nconst changelogMessages = [\n    \"You're doing rawrsome with these updates!\",\n    \"Your users can't wait to see what's new!\",\n    \"Every update tells a story, what'll be yours?\",\n    \"Your changelog is looking pawsitively amazing\",\n    \"Time to share your latest masterpiece\",\n    \"Your users are rooting for your next announcement\",\n    \"What incredible thing did you build today?\",\n    \"Your updates deserve their moment to shine\",\n    \"Ready to make your users' day?\",\n    \"Your changelog is about to get a whole lot cooler\",\n    \"Another day, another reason to celebrate your work\",\n    \"Your users love hearing from you - what's the news?\",\n    \"Time to turn your hard work into happy users\",\n    \"Your updates are the highlight of someone's day\",\n    \"Ready to share something rawrsome?\",\n    \"Your users are pawsing for your next update\",\n    \"What amazing thing are you shipping today?\",\n    \"Your changelog is hungry for fresh content\",\n    \"Time to roar about your latest achievement\",\n    \"Your users are on the edge of their seats\",\n    \"Another brilliant update waiting to be shared\",\n    \"Your changelog game is getting stronger\",\n    \"Ready to drop some knowledge on your users?\",\n    \"Your updates are about to make waves\",\n    \"Time to unleash your creative genius\",\n    \"Your users trust you to keep them in the loop\",\n    \"What's your next big reveal?\",\n    \"Your changelog is your stage - time to perform\",\n    \"Ready to turn code into celebration?\",\n    \"Your users are your biggest fans - give them news!\",\n    \"Another day, another chance to impress\",\n    \"Your updates are pure magic waiting to happen\",\n    \"Time to share the fruits of your labor\",\n    \"Your changelog is calling your name\",\n    \"Ready to make your mark on the world?\",\n    \"Your users appreciate your transparency\",\n    \"What story will your changelog tell today?\",\n    \"Your updates are like presents for your users\",\n    \"Time to show off your incredible work\",\n    \"Your changelog is your victory journal\",\n    \"Ready to turn features into fanfare?\",\n    \"Your users are counting on your updates\",\n    \"Another opportunity to shine brightly\",\n    \"Your changelog is your user's favorite read\",\n    \"Time to transform silence into celebration\",\n    \"Your updates are your signature on the world\",\n    \"Ready to make your users smile?\",\n    \"Your changelog is your digital megaphone\",\n    \"Another chance to exceed expectations\",\n    \"Your users are your partners in this journey\",\n    \"What incredible news are you sharing today?\",\n    \"Your updates are conversations waiting to happen\",\n    \"Time to bridge the gap between you and your users\",\n    \"Your changelog is your professional storytelling\",\n    \"Ready to turn development into delight?\",\n    \"Your users value your communication\",\n    \"Another update, another reason to be proud\",\n    \"Your changelog is your transparency window\",\n    \"Time to let your work speak for itself\",\n    \"Your updates are investments in user happiness\",\n    \"Ready to make your changelog un-fur-gettable?\",\n    \"Your users are eager for your latest news\",\n    \"Another day to document your awesomeness\",\n    \"Your changelog is your professional diary\",\n    \"Time to share your development journey\",\n    \"Your updates are breadcrumbs of progress\",\n    \"Ready to turn commits into conversations?\",\n    \"Your users appreciate your dedication\",\n    \"Another chance to build trust through transparency\",\n    \"Your changelog is your user relationship builder\",\n    \"Time to celebrate your coding victories\",\n    \"Your updates are proof of your commitment\",\n    \"Ready to make your users feel valued?\",\n    \"Your changelog is your digital handshake\",\n    \"Another opportunity to show you care\",\n    \"Your users are invested in your success\",\n    \"What exciting development are you announcing?\",\n    \"Your updates are your brand ambassadors\",\n    \"Time to turn features into friendships\",\n    \"Your changelog is your professional pulse\",\n    \"Ready to share your latest breakthrough?\",\n    \"Your users deserve to celebrate with you\",\n    \"Another update, another step forward together\",\n    \"Your changelog is your user's compass\",\n    \"Time to turn progress into praise\",\n    \"Your updates are your legacy in motion\",\n    \"Ready to make your changelog claw-some?\",\n    \"Your users are your most important audience\",\n    \"Another day to strengthen user bonds\",\n    \"Your changelog is your trust-building tool\",\n    \"Time to share your development poetry\",\n    \"Your updates are your professional heartbeat\",\n    \"Ready to turn code into community?\",\n    \"Your users are cheering for your success\",\n    \"Another chance to exceed user expectations\",\n    \"Your changelog is your digital autobiography\",\n    \"Time to celebrate progress with your people\",\n    \"Your updates are your user's roadmap\",\n    \"Ready to make your changelog roar-some?\",\n    \"Your users are your development partners\",\n    \"Another opportunity to inspire through transparency\",\n    \"Your changelog is your professional signature\",\n    \"Time to turn development into changelogs\",\n    \"Your updates are your commitment made visible\",\n    \"Ready to share your latest user gift?\",\n    \"Your users are waiting for your next chapter\",\n    \"Another day to build through communication\",\n    \"Your changelog is your user's favorite newsletter\",\n    \"Time to turn features into lasting relationships\",\n    \"CHORE: Un-hardcode the copyright year\"\n];\n\ninterface StatsCardProps {\n    title: string\n    value: number | string\n    description: string\n    icon: React.ComponentType<{ className?: string }>\n}\n\nconst StatsCard: React.FC<StatsCardProps> = ({\n                                                 title,\n                                                 value,\n                                                 description,\n                                                 icon: Icon\n                                             }) => (\n    <Card className=\"transition-all duration-200 hover:shadow-md dark:hover:shadow-lg/20\">\n        <CardContent className=\"p-6\">\n            <div className=\"flex items-center justify-between\">\n                <div className=\"space-y-2\">\n                    <p className=\"text-sm font-medium text-muted-foreground\">{title}</p>\n                    <div className=\"text-2xl font-semibold\">{value}</div>\n                    <p className=\"text-xs text-muted-foreground\">{description}</p>\n                </div>\n                <div className=\"p-2 rounded-lg bg-muted/50\">\n                    <Icon className=\"h-5 w-5 text-muted-foreground\"/>\n                </div>\n            </div>\n        </CardContent>\n    </Card>\n)\n\ninterface QuickActionProps {\n    title: string\n    description: string\n    href: string\n    icon: React.ComponentType<{ className?: string }>\n}\n\nconst QuickAction: React.FC<QuickActionProps> = ({\n                                                     title,\n                                                     description,\n                                                     href,\n                                                     icon: Icon\n                                                 }) => (\n    <Card className=\"transition-all duration-200 hover:shadow-md dark:hover:shadow-lg/20 group cursor-pointer\">\n        <Link href={href}>\n            <CardContent className=\"p-4\">\n                <div className=\"flex items-center gap-3\">\n                    <div className=\"p-2 rounded-md bg-muted/50 group-hover:bg-muted transition-colors\">\n                        <Icon className=\"h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors\"/>\n                    </div>\n                    <div className=\"flex-1 min-w-0\">\n                        <h3 className=\"font-medium text-sm group-hover:text-primary transition-colors\">\n                            {title}\n                        </h3>\n                        <p className=\"text-xs text-muted-foreground truncate\">{description}</p>\n                    </div>\n                    <ChevronRight\n                        className=\"h-4 w-4 text-muted-foreground group-hover:text-primary group-hover:translate-x-0.5 transition-all\"/>\n                </div>\n            </CardContent>\n        </Link>\n    </Card>\n)\n\nexport default function DashboardPage() {\n    const {user} = useAuth()\n    const randomMessage = changelogMessages[Math.floor(Math.random() * changelogMessages.length)]\n\n    const {data: stats, isLoading} = useQuery<DashboardStats>({\n        queryKey: ['dashboard-stats'],\n        queryFn: async () => {\n            const response = await fetch('/api/dashboard/stats')\n            if (!response.ok) throw new Error('Failed to fetch dashboard stats')\n            return response.json()\n        }\n    })\n\n    if (isLoading) {\n        return (\n            <div className=\"min-h-screen flex items-center justify-center\">\n                <motion.div\n                    initial={{opacity: 0}}\n                    animate={{opacity: 1}}\n                    className=\"space-y-4 text-center\"\n                >\n                    <Sparkles className=\"h-8 w-8 mx-auto text-primary animate-pulse\"/>\n                    <p className=\"text-sm text-muted-foreground\">Loading your dashboard...</p>\n                </motion.div>\n            </div>\n        )\n    }\n\n    const totalEntries = stats?.projectPreviews?.reduce((acc, project) => acc + project.changelogCount, 0) || 0\n    const activeProjects = stats?.projectPreviews?.filter(project => !project.id.startsWith('placeholder')).length || 0\n\n    return (\n        <div className=\"min-h-screen bg-background\">\n            <div className=\"mx-auto max-w-6xl px-4 py-8\">\n                <motion.div\n                    initial=\"hidden\"\n                    animate=\"show\"\n                    variants={container}\n                    className=\"space-y-8\"\n                >\n                    {/* Welcome Section */}\n                    <motion.div variants={fadeIn}>\n                        <Card\n                            className=\"overflow-hidden border-0 bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 shadow-lg\">\n                            <div className=\"absolute inset-0 bg-grid-white/[0.1] bg-[size:24px_24px]\"/>\n                            <CardContent className=\"relative p-8\">\n                                <div\n                                    className=\"flex flex-col lg:flex-row items-start lg:items-center justify-between gap-6\">\n                                    <div className=\"space-y-3 flex-1\">\n                                        <div className=\"inline-flex items-center gap-2 mb-1\">\n                                            <span className=\"text-xl\">🦖</span>\n                                            <Badge variant=\"glass\" className=\"bg-white/20 text-white border-white/30\">\n                                                Changerawr\n                                            </Badge>\n                                        </div>\n                                        <h1 className=\"text-3xl lg:text-4xl font-bold text-white leading-tight\">\n                                            Welcome back, {user?.name?.split(' ')[0] || 'there'}!\n                                        </h1>\n                                        <p className=\"text-blue-100 max-w-md\">\n                                            {randomMessage}\n                                        </p>\n                                    </div>\n\n                                    <div className=\"flex-shrink-0\">\n                                        <Avatar className=\"h-16 w-16 border-2 border-white/30\">\n                                            <AvatarImage\n                                                src={user?.email ? getGravatarUrl(user?.email, 160) : undefined}\n                                                alt={user?.name || 'User avatar'}\n                                            />\n                                            <AvatarFallback className=\"text-lg font-semibold bg-white/20 text-white\">\n                                                {user?.name?.split(' ').map(n => n[0]).join('') || user?.email?.[0] || '?'}\n                                            </AvatarFallback>\n                                        </Avatar>\n                                    </div>\n                                </div>\n                            </CardContent>\n                        </Card>\n                    </motion.div>\n\n                    {/* Stats Overview */}\n                    <motion.div variants={fadeIn} className=\"space-y-4\">\n                        <h2 className=\"text-xl font-semibold\">Overview</h2>\n                        <div className=\"grid gap-4 grid-cols-2 md:grid-cols-4\">\n                            <StatsCard\n                                title=\"Projects\"\n                                value={stats?.totalProjects || 0}\n                                description=\"Total projects\"\n                                icon={BookOpen}\n                            />\n                            <StatsCard\n                                title=\"Entries\"\n                                value={totalEntries}\n                                description=\"All changelog entries\"\n                                icon={FileText}\n                            />\n                            <StatsCard\n                                title=\"Active\"\n                                value={activeProjects}\n                                description=\"Currently active\"\n                                icon={Target}\n                            />\n                            <StatsCard\n                                title=\"This Month\"\n                                value={stats?.recentActivity?.filter(activity => {\n                                    const activityDate = new Date(activity.timestamp)\n                                    const now = new Date()\n                                    return activityDate.getMonth() === now.getMonth() &&\n                                        activityDate.getFullYear() === now.getFullYear()\n                                })?.length || 0}\n                                description=\"Recent activity\"\n                                icon={TrendingUp}\n                            />\n                        </div>\n                    </motion.div>\n\n                    {/* Quick Actions */}\n                    <motion.div variants={fadeIn} className=\"space-y-4\">\n                        <h2 className=\"text-xl font-semibold\">Quick Actions</h2>\n                        <div className=\"grid gap-3 grid-cols-1 md:grid-cols-3\">\n                            <QuickAction\n                                title=\"Create Project\"\n                                description=\"Start a new changelog project\"\n                                href=\"/dashboard/projects/new\"\n                                icon={Plus}\n                            />\n                            <QuickAction\n                                title=\"Write Entry\"\n                                description=\"Add a changelog entry\"\n                                href=\"/dashboard/projects\"\n                                icon={PenTool}\n                            />\n                            <QuickAction\n                                title=\"Settings\"\n                                description=\"Manage your account\"\n                                href=\"/dashboard/settings\"\n                                icon={Settings}\n                            />\n                        </div>\n                    </motion.div>\n\n                    <div className=\"grid gap-8 lg:grid-cols-3\">\n                        {/* Recent Projects */}\n                        <motion.div variants={fadeIn} className=\"lg:col-span-2 space-y-4\">\n                            <div className=\"flex items-center justify-between\">\n                                <h2 className=\"text-xl font-semibold\">Recent Projects</h2>\n                                <Button variant=\"ghost\" size=\"sm\" asChild>\n                                    <Link href=\"/dashboard/projects\" className=\"text-sm\">\n                                        View all\n                                        <ArrowRight className=\"h-4 w-4 ml-1\"/>\n                                    </Link>\n                                </Button>\n                            </div>\n\n                            <div className=\"space-y-3\">\n                                {stats?.projectPreviews?.slice(0, 4).map((project: ProjectPreview, index) => (\n                                    <motion.div\n                                        key={project.id}\n                                        variants={fadeIn}\n                                        custom={index}\n                                    >\n                                        <Card\n                                            className=\"group hover:shadow-md dark:hover:shadow-lg/20 transition-all duration-200 hover:border-primary/50\">\n                                            <Link\n                                                href={project.id.startsWith('placeholder') ?\n                                                    '/dashboard/projects/new' :\n                                                    `/dashboard/projects/${project.id}`\n                                                }\n                                                className=\"block\"\n                                            >\n                                                <CardContent className=\"p-4\">\n                                                    <div className=\"flex items-center justify-between\">\n                                                        <div className=\"flex items-center gap-3\">\n                                                            <div\n                                                                className=\"h-8 w-8 rounded-md bg-muted/50 flex items-center justify-center\">\n                                                                <LayoutDashboard\n                                                                    className=\"h-4 w-4 text-muted-foreground\"/>\n                                                            </div>\n                                                            <div className=\"space-y-1\">\n                                                                <h3 className=\"font-medium group-hover:text-primary transition-colors\">\n                                                                    {project.name}\n                                                                </h3>\n                                                                <div\n                                                                    className=\"flex items-center gap-3 text-xs text-muted-foreground\">\n                                                                    <span>{project.changelogCount} entries</span>\n                                                                    {!project.id.startsWith('placeholder') && (\n                                                                        <>\n                                                                            <span>•</span>\n                                                                            <span>\n                                                                                {formatDistanceToNow(new Date(project.lastUpdated))} ago\n                                                                            </span>\n                                                                        </>\n                                                                    )}\n                                                                </div>\n                                                            </div>\n                                                        </div>\n                                                        <ChevronRight\n                                                            className=\"h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors\"/>\n                                                    </div>\n                                                </CardContent>\n                                            </Link>\n                                        </Card>\n                                    </motion.div>\n                                ))}\n\n                                {/* Create New Project Card */}\n                                <motion.div variants={fadeIn}>\n                                    <Card\n                                        className=\"border-dashed border-2 hover:border-primary/50 hover:bg-muted/20 transition-all duration-200 group\">\n                                        <Link href=\"/dashboard/projects/new\">\n                                            <CardContent className=\"p-4 flex items-center justify-center py-8\">\n                                                <div className=\"text-center space-y-2\">\n                                                    <div\n                                                        className=\"h-8 w-8 rounded-md bg-muted/50 flex items-center justify-center mx-auto group-hover:bg-muted transition-colors\">\n                                                        <Plus\n                                                            className=\"h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors\"/>\n                                                    </div>\n                                                    <div>\n                                                        <p className=\"font-medium text-sm group-hover:text-primary transition-colors\">\n                                                            Create New Project\n                                                        </p>\n                                                        <p className=\"text-xs text-muted-foreground\">\n                                                            Start tracking changes\n                                                        </p>\n                                                    </div>\n                                                </div>\n                                            </CardContent>\n                                        </Link>\n                                    </Card>\n                                </motion.div>\n                            </div>\n                        </motion.div>\n\n                        {/* Activity Feed */}\n                        <motion.div variants={fadeIn} className=\"space-y-4\">\n                            <h2 className=\"text-xl font-semibold\">Recent Activity</h2>\n                            <Card>\n                                <CardHeader className=\"pb-3\">\n                                    <CardDescription>\n                                        Latest updates across your projects\n                                    </CardDescription>\n                                </CardHeader>\n                                <CardContent>\n                                    <ScrollArea className=\"h-[400px] pr-3\">\n                                        <div className=\"space-y-3\">\n                                            {stats?.recentActivity?.length ? (\n                                                stats.recentActivity.slice(0, 10).map((activity: DashboardActivity) => (\n                                                    <motion.div\n                                                        key={activity.id}\n                                                        variants={fadeIn}\n                                                        className=\"flex items-start gap-3 p-3 rounded-lg hover:bg-muted/50 transition-colors group\"\n                                                    >\n                                                        <div className=\"mt-0.5 flex-shrink-0\">\n                                                            <div\n                                                                className=\"h-6 w-6 rounded-md bg-muted/50 flex items-center justify-center\">\n                                                                <Code className=\"h-3 w-3 text-muted-foreground\"/>\n                                                            </div>\n                                                        </div>\n                                                        <div className=\"flex-1 min-w-0 space-y-1\">\n                                                            <p className=\"text-sm font-medium leading-relaxed\">{activity.message}</p>\n                                                            <div\n                                                                className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n                                                                <span>{formatDistanceToNow(new Date(activity.timestamp))} ago</span>\n                                                                <span>•</span>\n                                                                <Link\n                                                                    href={`/dashboard/projects/${activity.projectId}/`}\n                                                                    className=\"text-primary hover:underline inline-flex items-center gap-1\"\n                                                                >\n                                                                    {activity.projectName}\n                                                                </Link>\n                                                            </div>\n                                                        </div>\n                                                    </motion.div>\n                                                ))\n                                            ) : (\n                                                <div\n                                                    className=\"flex flex-col items-center justify-center h-[200px] text-center space-y-3\">\n                                                    <div\n                                                        className=\"h-12 w-12 rounded-md bg-muted/50 flex items-center justify-center\">\n                                                        <Activity className=\"h-6 w-6 text-muted-foreground\"/>\n                                                    </div>\n                                                    <div className=\"space-y-1\">\n                                                        <p className=\"text-sm font-medium\">No activity yet</p>\n                                                        <p className=\"text-xs text-muted-foreground\">\n                                                            Activity will appear here as you make changes\n                                                        </p>\n                                                    </div>\n                                                </div>\n                                            )}\n                                        </div>\n                                    </ScrollArea>\n                                </CardContent>\n                            </Card>\n                        </motion.div>\n                    </div>\n                </motion.div>\n            </div>\n        </div>\n    )\n}"
  },
  {
    "path": "app/dashboard/projects/[projectId]/analytics/page.tsx",
    "content": "'use client';\n\nimport {useState} from 'react';\nimport {useQuery} from '@tanstack/react-query';\nimport {useParams} from 'next/navigation';\nimport {motion} from 'framer-motion';\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card';\nimport {\n    Select,\n    SelectContent,\n    SelectItem,\n    SelectTrigger,\n    SelectValue,\n} from '@/components/ui/select';\nimport {Button} from '@/components/ui/button';\nimport {Skeleton} from '@/components/ui/skeleton';\nimport {\n    BarChart4,\n    Eye,\n    Users,\n    Globe,\n    FileText,\n    Download,\n    RefreshCw,\n    TrendingUp,\n} from 'lucide-react';\nimport {AnalyticsChart} from '@/components/analytics/analytics-chart';\nimport {AnalyticsMetricCard} from '@/components/analytics/analytics-metric-card';\nimport {CountryAnalyticsTable} from '@/components/analytics/country-analytics-table';\nimport {EntryAnalyticsTable} from '@/components/analytics/entry-analytics-table';\nimport {ReferrerAnalyticsTable} from '@/components/analytics/referrer-analytics-table';\nimport type {AnalyticsPeriod, ProjectAnalyticsData} from '@/lib/types/analytics';\n\nconst fadeIn = {\n    initial: {opacity: 0, y: 20},\n    animate: {opacity: 1, y: 0},\n    transition: {duration: 0.5}\n};\n\nconst staggerChildren = {\n    animate: {\n        transition: {\n            staggerChildren: 0.1\n        }\n    }\n};\n\nexport default function ProjectAnalyticsPage() {\n    const params = useParams();\n    const projectId = params.projectId as string;\n    const [selectedPeriod, setSelectedPeriod] = useState<AnalyticsPeriod>('30d');\n\n    const {\n        data: analyticsData,\n        isLoading,\n        error,\n        refetch,\n        isRefetching\n    } = useQuery<{ success: boolean; data: ProjectAnalyticsData }>({\n        queryKey: ['project-analytics', projectId, selectedPeriod],\n        queryFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}/analytics?period=${selectedPeriod}`);\n            if (!response.ok) {\n                throw new Error('Failed to fetch analytics data');\n            }\n            return response.json();\n        },\n        refetchOnWindowFocus: false,\n        staleTime: 5 * 60 * 1000, // 5 minutes\n    });\n\n    const handleExport = async () => {\n        try {\n            const response = await fetch(`/api/projects/${projectId}/analytics/export?period=${selectedPeriod}`);\n            if (response.ok) {\n                const blob = await response.blob();\n                const url = window.URL.createObjectURL(blob);\n                const a = document.createElement('a');\n                a.href = url;\n                a.download = `analytics-${projectId}-${selectedPeriod}.csv`;\n                document.body.appendChild(a);\n                a.click();\n                window.URL.revokeObjectURL(url);\n                document.body.removeChild(a);\n            }\n        } catch (error) {\n            console.error('Failed to export analytics:', error);\n        }\n    };\n\n    if (error) {\n        return (\n            <div className=\"container max-w-7xl space-y-6 p-4 md:p-8\">\n                <Card>\n                    <CardContent className=\"pt-6\">\n                        <div className=\"text-center space-y-4\">\n                            <div className=\"text-destructive\">\n                                <BarChart4 className=\"h-12 w-12 mx-auto mb-4\"/>\n                                <h3 className=\"text-lg font-semibold\">Failed to Load Analytics</h3>\n                                <p className=\"text-muted-foreground\">\n                                    {error instanceof Error ? error.message : 'An unexpected error occurred'}\n                                </p>\n                            </div>\n                            <Button onClick={() => refetch()} variant=\"outline\">\n                                <RefreshCw className=\"h-4 w-4 mr-2\"/>\n                                Try Again\n                            </Button>\n                        </div>\n                    </CardContent>\n                </Card>\n            </div>\n        );\n    }\n\n    const data = analyticsData?.data;\n\n    return (\n        <div className=\"container max-w-7xl space-y-6 p-4 md:p-8\">\n            {/* Header */}\n            <motion.div\n                variants={fadeIn}\n                initial=\"initial\"\n                animate=\"animate\"\n                className=\"flex flex-col md:flex-row md:items-center md:justify-between gap-4\"\n            >\n                <div className=\"flex items-center gap-4\">\n                    <Button variant=\"ghost\" size=\"sm\" asChild>\n                    </Button>\n                    <div>\n                        <h1 className=\"text-3xl font-bold tracking-tight flex items-center gap-3\">\n                            <BarChart4 className=\"h-8 w-8 text-primary\"/>\n                            Analytics\n                        </h1>\n                        {data && (\n                            <p className=\"text-muted-foreground\">\n                                {data.projectName} • {data.period.toUpperCase()} Overview\n                            </p>\n                        )}\n                    </div>\n                </div>\n\n                <div className=\"flex items-center gap-3\">\n                    <Select value={selectedPeriod} onValueChange={(value: AnalyticsPeriod) => setSelectedPeriod(value)}>\n                        <SelectTrigger className=\"w-32\">\n                            <SelectValue/>\n                        </SelectTrigger>\n                        <SelectContent>\n                            <SelectItem value=\"7d\">Last 7 days</SelectItem>\n                            <SelectItem value=\"30d\">Last 30 days</SelectItem>\n                            <SelectItem value=\"90d\">Last 90 days</SelectItem>\n                            <SelectItem value=\"1y\">Last year</SelectItem>\n                        </SelectContent>\n                    </Select>\n\n                    <Button\n                        variant=\"outline\"\n                        size=\"sm\"\n                        onClick={handleExport}\n                        disabled={isLoading || !data}\n                    >\n                        <Download className=\"h-4 w-4 mr-2\"/>\n                        Export\n                    </Button>\n\n                    <Button\n                        variant=\"outline\"\n                        size=\"sm\"\n                        onClick={() => refetch()}\n                        disabled={isRefetching}\n                    >\n                        <RefreshCw className={`h-4 w-4 mr-2 ${isRefetching ? 'animate-spin' : ''}`}/>\n                        Refresh\n                    </Button>\n                </div>\n            </motion.div>\n\n            {isLoading ? (\n                <div className=\"space-y-6\">\n                    <div className=\"grid gap-6 md:grid-cols-2 lg:grid-cols-4\">\n                        {Array.from({length: 4}).map((_, i) => (\n                            <Card key={i}>\n                                <CardHeader>\n                                    <Skeleton className=\"h-4 w-24\"/>\n                                </CardHeader>\n                                <CardContent>\n                                    <Skeleton className=\"h-8 w-16 mb-2\"/>\n                                    <Skeleton className=\"h-3 w-20\"/>\n                                </CardContent>\n                            </Card>\n                        ))}\n                    </div>\n                    <Card>\n                        <CardHeader>\n                            <Skeleton className=\"h-6 w-32\"/>\n                        </CardHeader>\n                        <CardContent>\n                            <Skeleton className=\"h-80 w-full\"/>\n                        </CardContent>\n                    </Card>\n                </div>\n            ) : data ? (\n                <motion.div\n                    variants={staggerChildren}\n                    initial=\"initial\"\n                    animate=\"animate\"\n                    className=\"space-y-6\"\n                >\n                    {/* Metrics Overview */}\n                    <motion.div variants={fadeIn} className=\"grid gap-6 md:grid-cols-2 lg:grid-cols-4\">\n                        <AnalyticsMetricCard\n                            title=\"Total Views\"\n                            value={data.totalViews}\n                            icon={Eye}\n                            description=\"Page and entry views\"\n                        />\n                        <AnalyticsMetricCard\n                            title=\"Unique Visitors\"\n                            value={data.uniqueVisitors}\n                            icon={Users}\n                            description=\"Based on unique sessions\"\n                        />\n                        <AnalyticsMetricCard\n                            title=\"Countries\"\n                            value={data.topCountries.length}\n                            icon={Globe}\n                            description=\"Geographic reach\"\n                        />\n                        <AnalyticsMetricCard\n                            title=\"Top Entries\"\n                            value={data.topEntries.length}\n                            icon={FileText}\n                            description=\"Popular changelog entries\"\n                        />\n                    </motion.div>\n\n                    {/* Views Chart */}\n                    <motion.div variants={fadeIn}>\n                        <Card>\n                            <CardHeader>\n                                <CardTitle className=\"flex items-center gap-2\">\n                                    <TrendingUp className=\"h-5 w-5\"/>\n                                    Views Over Time\n                                </CardTitle>\n                                <CardDescription>\n                                    Daily views and unique visitors for the selected period\n                                </CardDescription>\n                            </CardHeader>\n                            <CardContent>\n                                <AnalyticsChart data={data.dailyViews}/>\n                            </CardContent>\n                        </Card>\n                    </motion.div>\n\n                    {/* Data Tables */}\n                    <div className=\"grid gap-6 lg:grid-cols-2\">\n                        <motion.div variants={fadeIn}>\n                            <CountryAnalyticsTable countries={data.topCountries}/>\n                        </motion.div>\n                        <motion.div variants={fadeIn}>\n                            <ReferrerAnalyticsTable referrers={data.topReferrers}/>\n                        </motion.div>\n                    </div>\n\n                    {/* Top Entries */}\n                    <motion.div variants={fadeIn}>\n                        <EntryAnalyticsTable entries={data.topEntries} projectId={projectId}/>\n                    </motion.div>\n                </motion.div>\n            ) : null}\n        </div>\n    );\n}"
  },
  {
    "path": "app/dashboard/projects/[projectId]/api-keys/page.tsx",
    "content": "'use client'\n\nimport React, { useState } from 'react';\nimport { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';\nimport { useParams } from 'next/navigation';\nimport { useAuth } from '@/context/auth';\nimport {\n    Card,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card';\nimport { Button } from '@/components/ui/button';\nimport {\n    AlertDialog,\n    AlertDialogAction,\n    AlertDialogCancel,\n    AlertDialogContent,\n    AlertDialogDescription,\n    AlertDialogFooter,\n    AlertDialogHeader,\n    AlertDialogTitle,\n    AlertDialogTrigger,\n} from '@/components/ui/alert-dialog';\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogFooter,\n    DialogHeader,\n    DialogTitle,\n    DialogTrigger,\n} from '@/components/ui/dialog';\nimport {\n    Select,\n    SelectContent,\n    SelectItem,\n    SelectTrigger,\n    SelectValue,\n} from '@/components/ui/select';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport { Skeleton } from '@/components/ui/skeleton';\nimport { Switch } from '@/components/ui/switch';\nimport { toast } from '@/hooks/use-toast';\nimport {\n    Key,\n    Plus,\n    Trash2,\n    Ban,\n    Pencil,\n    Copy,\n    FileText,\n    Shield,\n    X,\n    ExternalLink,\n    Eye,\n    EyeOff,\n    Filter\n} from 'lucide-react';\nimport { format } from 'date-fns';\nimport { AnimatePresence, motion } from 'framer-motion';\nimport Link from 'next/link';\nimport SDKShowcaseCompact from '@/components/admin/api/sdk-showcase-compact';\nimport { Badge } from '@/components/ui/badge';\nimport { PermissionsModal } from '@/components/admin/api/PermissionsModal';\nimport { PERMISSION_GROUPS } from '@/lib/api/permissions';\n\ninterface ApiKey {\n    id: string;\n    name: string;\n    key: string;\n    lastUsed: string | null;\n    createdAt: string;\n    expiresAt: string | null;\n    isRevoked: boolean;\n    projectId: string | null;\n    permissions: string[];\n    isGlobal: boolean;\n    userId: string;\n    user: {\n        id: string;\n        name: string | null;\n        email: string;\n    };\n}\n\ninterface RenameDialogProps {\n    open: boolean;\n    onOpenChange: (open: boolean) => void;\n    onRename: (newName: string) => Promise<void>;\n    currentName: string;\n}\n\nfunction RenameDialog({\n                          open,\n                          onOpenChange,\n                          onRename,\n                          currentName,\n                      }: RenameDialogProps) {\n    const [newName, setNewName] = useState(currentName);\n    const [isSubmitting, setIsSubmitting] = useState(false);\n\n    const handleSubmit = async (e: React.FormEvent) => {\n        e.preventDefault();\n        if (!newName.trim() || newName === currentName) return;\n\n        setIsSubmitting(true);\n        try {\n            await onRename(newName);\n            onOpenChange(false);\n        } catch (error) {\n            console.error('Failed to rename API key:', error);\n        } finally {\n            setIsSubmitting(false);\n        }\n    };\n\n    return (\n        <Dialog open={open} onOpenChange={onOpenChange}>\n            <DialogContent className=\"max-w-md\">\n                <DialogHeader>\n                    <DialogTitle>Rename API Key</DialogTitle>\n                    <DialogDescription>\n                        Enter a new name for your API key.\n                    </DialogDescription>\n                </DialogHeader>\n                <form onSubmit={handleSubmit}>\n                    <div className=\"grid gap-4 py-4\">\n                        <div className=\"grid gap-2\">\n                            <Label htmlFor=\"new-name\">New Name</Label>\n                            <Input\n                                id=\"new-name\"\n                                value={newName}\n                                onChange={(e) => setNewName(e.target.value)}\n                                placeholder=\"e.g., Production API Key\"\n                            />\n                        </div>\n                    </div>\n                    <DialogFooter>\n                        <Button\n                            type=\"button\"\n                            variant=\"outline\"\n                            onClick={() => onOpenChange(false)}\n                            disabled={isSubmitting}\n                        >\n                            Cancel\n                        </Button>\n                        <Button\n                            type=\"submit\"\n                            disabled={!newName.trim() || newName === currentName || isSubmitting}\n                        >\n                            {isSubmitting ? 'Renaming...' : 'Rename'}\n                        </Button>\n                    </DialogFooter>\n                </form>\n            </DialogContent>\n        </Dialog>\n    );\n}\n\nfunction NewKeyAlert({ keyData, onClose, onCopy }: { keyData: { key: string; id: string }; onClose: () => void; onCopy: (key: string) => void }) {\n    return (\n        <motion.div\n            initial={{ opacity: 0, y: -10 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0, y: -10 }}\n            className=\"bg-yellow-50 dark:bg-yellow-950/40 border border-yellow-200 dark:border-yellow-900/50 rounded-lg mb-6\"\n        >\n            <div className=\"px-4 py-4\">\n                <div className=\"flex items-center justify-between mb-2\">\n                    <div className=\"flex items-center gap-2\">\n                        <Shield className=\"h-5 w-5 text-yellow-600 dark:text-yellow-500\" />\n                        <h4 className=\"font-medium text-yellow-800 dark:text-yellow-500\">New API Key Created</h4>\n                    </div>\n                    <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        onClick={onClose}\n                        className=\"h-8 w-8 p-0 text-yellow-600 dark:text-yellow-500 hover:text-yellow-700 dark:hover:text-yellow-600 hover:bg-transparent\"\n                    >\n                        <X className=\"h-4 w-4\" />\n                    </Button>\n                </div>\n\n                <p className=\"text-sm text-yellow-700 dark:text-yellow-400 mb-3\">\n                    Save your API key now. For security reasons, you won&apos;t be able to view it again.\n                </p>\n\n                <div className=\"relative\">\n                    <div className=\"bg-yellow-100 dark:bg-yellow-950/60 border border-yellow-200 dark:border-yellow-900/30 rounded-md p-3 font-mono text-sm break-all text-yellow-800 dark:text-yellow-300\">\n                        {keyData.key}\n                    </div>\n                    <Button\n                        variant=\"outline\"\n                        size=\"sm\"\n                        onClick={() => onCopy(keyData.key)}\n                        className=\"absolute top-2 right-2 h-8 bg-yellow-100 dark:bg-yellow-950/70 border-yellow-200 dark:border-yellow-900/50 text-yellow-800 dark:text-yellow-300 hover:bg-yellow-200 dark:hover:bg-yellow-900 hover:text-yellow-900 dark:hover:text-yellow-200\"\n                    >\n                        <Copy className=\"h-3.5 w-3.5 mr-1\" />\n                        Copy\n                    </Button>\n                </div>\n            </div>\n        </motion.div>\n    );\n}\n\nexport default function ProjectApiKeysPage() {\n    const params = useParams();\n    const projectId = params.projectId as string;\n    const queryClient = useQueryClient();\n    const { user } = useAuth();\n    const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);\n    const [isPermissionsModalOpen, setIsPermissionsModalOpen] = useState(false);\n    const [newKeyName, setNewKeyName] = useState('');\n    const [newKeyPermissions, setNewKeyPermissions] = useState<string[]>(PERMISSION_GROUPS.FULL_ACCESS);\n    const [newKeyIsGlobal, setNewKeyIsGlobal] = useState(false);\n    const [newKeyData, setNewKeyData] = useState<{ key: string; id: string } | null>(null);\n    const [renameKey, setRenameKey] = useState<ApiKey | null>(null);\n    const [userFilter, setUserFilter] = useState<string>('all');\n\n    const { data: systemConfig } = useQuery<{ adminOnlyApiKeyCreation: boolean }>({\n        queryKey: ['system-config-api-keys'],\n        queryFn: async () => {\n            const response = await fetch('/api/admin/config');\n            if (!response.ok) return { adminOnlyApiKeyCreation: false };\n            return response.json();\n        },\n    });\n\n    const { data: apiKeys, isLoading } = useQuery<ApiKey[]>({\n        queryKey: ['project-api-keys', projectId, userFilter],\n        queryFn: async () => {\n            const url = new URL(`/api/projects/${projectId}/api-keys`, window.location.origin);\n            if (userFilter !== 'all') {\n                url.searchParams.set('userId', userFilter);\n            }\n            const response = await fetch(url.toString());\n            if (!response.ok) throw new Error('Failed to fetch API keys');\n            return response.json();\n        },\n        refetchInterval: 30000,\n        staleTime: 15000,\n    });\n\n    const isAdmin = user?.role === 'ADMIN';\n    const canCreateKeys = !systemConfig?.adminOnlyApiKeyCreation || isAdmin;\n\n    // Get unique users from API keys for filter dropdown\n    const uniqueUsers = React.useMemo(() => {\n        if (!apiKeys || !isAdmin) return [];\n        const usersMap = new Map<string, { id: string; name: string | null; email: string }>();\n        apiKeys.forEach(key => {\n            if (!usersMap.has(key.userId)) {\n                usersMap.set(key.userId, key.user);\n            }\n        });\n        return Array.from(usersMap.values());\n    }, [apiKeys, isAdmin]);\n\n    const createApiKey = useMutation({\n        mutationFn: async ({ name, permissions, isGlobal }: { name: string; permissions: string[]; isGlobal: boolean }) => {\n            const response = await fetch(`/api/projects/${projectId}/api-keys`, {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({ name, permissions, isGlobal }),\n            });\n            if (!response.ok) throw new Error('Failed to create API key');\n            return response.json();\n        },\n        onSuccess: (data) => {\n            queryClient.setQueryData(['project-api-keys', projectId], (old: ApiKey[] | undefined) => {\n                return old ? [...old, data] : [data];\n            });\n            setNewKeyData({ key: data.key, id: data.id });\n            toast({\n                title: 'API Key Created',\n                description: 'The new API key has been created successfully.',\n            });\n        },\n    });\n\n    const renameApiKey = useMutation({\n        mutationFn: async ({ id, name }: { id: string; name: string }) => {\n            const response = await fetch(`/api/projects/${projectId}/api-keys/${id}`, {\n                method: 'PATCH',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({ name }),\n            });\n            if (!response.ok) throw new Error('Failed to rename API key');\n            return response.json();\n        },\n        onMutate: async ({ id, name }) => {\n            await queryClient.cancelQueries({ queryKey: ['project-api-keys', projectId] });\n            const previousKeys = queryClient.getQueryData(['project-api-keys', projectId]);\n\n            queryClient.setQueryData(['project-api-keys', projectId], (old: ApiKey[] | undefined) => {\n                return old?.map(key =>\n                    key.id === id ? { ...key, name } : key\n                );\n            });\n\n            return { previousKeys };\n        },\n        onError: (err, variables, context) => {\n            if (context?.previousKeys) {\n                queryClient.setQueryData(['project-api-keys', projectId], context.previousKeys);\n            }\n        },\n        onSettled: () => {\n            queryClient.invalidateQueries({ queryKey: ['project-api-keys', projectId] });\n        },\n    });\n\n    const revokeApiKey = useMutation({\n        mutationFn: async (id: string) => {\n            const response = await fetch(`/api/projects/${projectId}/api-keys/${id}`, {\n                method: 'PATCH',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({ isRevoked: true }),\n            });\n            if (!response.ok) throw new Error('Failed to revoke API key');\n            return response.json();\n        },\n        onMutate: async (id) => {\n            await queryClient.cancelQueries({ queryKey: ['project-api-keys', projectId] });\n            const previousKeys = queryClient.getQueryData(['project-api-keys', projectId]);\n\n            queryClient.setQueryData(['project-api-keys', projectId], (old: ApiKey[] | undefined) => {\n                return old?.map(key =>\n                    key.id === id ? { ...key, isRevoked: true } : key\n                );\n            });\n\n            return { previousKeys };\n        },\n        onError: (err, id, context) => {\n            if (context?.previousKeys) {\n                queryClient.setQueryData(['project-api-keys', projectId], context.previousKeys);\n            }\n        },\n        onSettled: () => {\n            queryClient.invalidateQueries({ queryKey: ['project-api-keys', projectId] });\n        },\n    });\n\n    const deleteApiKey = useMutation({\n        mutationFn: async (id: string) => {\n            const response = await fetch(`/api/projects/${projectId}/api-keys/${id}`, {\n                method: 'DELETE',\n            });\n            if (!response.ok) throw new Error('Failed to delete API key');\n        },\n        onMutate: async (id) => {\n            await queryClient.cancelQueries({ queryKey: ['project-api-keys', projectId] });\n            const previousKeys = queryClient.getQueryData(['project-api-keys', projectId]);\n\n            queryClient.setQueryData(['project-api-keys', projectId], (old: ApiKey[] | undefined) => {\n                return old?.filter(key => key.id !== id);\n            });\n\n            return { previousKeys };\n        },\n        onError: (err, id, context) => {\n            if (context?.previousKeys) {\n                queryClient.setQueryData(['project-api-keys', projectId], context.previousKeys);\n            }\n        },\n        onSettled: () => {\n            queryClient.invalidateQueries({ queryKey: ['project-api-keys', projectId] });\n        },\n    });\n\n    const handleCreateKey = async (e: React.FormEvent) => {\n        e.preventDefault();\n        if (!newKeyName.trim()) return;\n\n        await createApiKey.mutate({ name: newKeyName, permissions: newKeyPermissions, isGlobal: newKeyIsGlobal });\n        setNewKeyName('');\n        setNewKeyPermissions(PERMISSION_GROUPS.FULL_ACCESS);\n        setNewKeyIsGlobal(false);\n        setIsCreateDialogOpen(false);\n    };\n\n    const handleCopyKey = (key: string) => {\n        navigator.clipboard.writeText(key);\n        toast({\n            title: 'API Key Copied',\n            description: 'The API key has been copied to your clipboard.',\n        });\n    };\n\n    if (isLoading) {\n        return (\n            <div className=\"container max-w-screen-2xl mx-auto py-6 px-4 sm:px-6 lg:px-8\">\n                <div className=\"mb-6 flex justify-between items-center\">\n                    <Skeleton className=\"h-8 w-32\" />\n                    <Skeleton className=\"h-10 w-32\" />\n                </div>\n                <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n                    <div className=\"lg:col-span-2\">\n                        <Skeleton className=\"h-64 w-full rounded-lg\" />\n                    </div>\n                    <div className=\"lg:col-span-1\">\n                        <Skeleton className=\"h-64 w-full rounded-lg\" />\n                    </div>\n                </div>\n            </div>\n        );\n    }\n\n    return (\n        <div className=\"container max-w-screen-2xl mx-auto py-6 px-4 sm:px-6 lg:px-8\">\n            {/* Page header */}\n            <div className=\"flex flex-col md:flex-row md:items-center justify-between mb-6\">\n                <div>\n                    <h1 className=\"text-2xl font-bold\">API Keys</h1>\n                    <p className=\"text-muted-foreground mt-1\">\n                        Create and manage API keys for this project.\n                    </p>\n                </div>\n                <div className=\"flex items-center gap-3 mt-4 md:mt-0\">\n                    <Button variant=\"outline\" size=\"sm\" asChild className=\"h-9\">\n                        <Link href=\"/api-docs\" className=\"flex items-center\">\n                            <FileText className=\"h-4 w-4 mr-2\" />\n                            API Docs\n                            <ExternalLink className=\"ml-1 h-3 w-3\" />\n                        </Link>\n                    </Button>\n                    {canCreateKeys && (\n                        <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>\n                            <DialogTrigger asChild>\n                                <Button size=\"sm\" className=\"h-9\">\n                                    <Plus className=\"h-4 w-4 mr-2\" />\n                                    Create Key\n                                </Button>\n                            </DialogTrigger>\n                        <DialogContent className=\"max-w-md\">\n                            <DialogHeader>\n                                <DialogTitle>Create New API Key</DialogTitle>\n                                <DialogDescription>\n                                    Give your API key a name to help you identify its use.\n                                </DialogDescription>\n                            </DialogHeader>\n                            <form onSubmit={handleCreateKey}>\n                                <div className=\"grid gap-4 py-4\">\n                                    <div className=\"grid gap-2\">\n                                        <Label htmlFor=\"name\">API Key Name</Label>\n                                        <Input\n                                            id=\"name\"\n                                            value={newKeyName}\n                                            onChange={(e) => setNewKeyName(e.target.value)}\n                                            placeholder=\"e.g., Production API Key\"\n                                        />\n                                    </div>\n                                    <div className=\"grid gap-2\">\n                                        <Label>Permissions</Label>\n                                        <Button\n                                            type=\"button\"\n                                            variant=\"outline\"\n                                            onClick={() => setIsPermissionsModalOpen(true)}\n                                            className=\"justify-start\"\n                                        >\n                                            <Shield className=\"h-4 w-4 mr-2\" />\n                                            {newKeyPermissions.length} permission{newKeyPermissions.length !== 1 ? 's' : ''} selected\n                                        </Button>\n                                    </div>\n                                    <div className=\"flex items-center justify-between rounded-lg border p-4\">\n                                        <div className=\"flex gap-2\">\n                                            {newKeyIsGlobal ? <Eye className=\"h-5 w-5 text-muted-foreground mt-0.5\" /> : <EyeOff className=\"h-5 w-5 text-muted-foreground mt-0.5\" />}\n                                            <div className=\"space-y-0.5\">\n                                                <Label htmlFor=\"isGlobal\" className=\"text-base cursor-pointer\">\n                                                    Visible to Administrators\n                                                </Label>\n                                                <p className=\"text-sm text-muted-foreground\">\n                                                    {newKeyIsGlobal\n                                                        ? 'Administrators can see and manage this key'\n                                                        : 'Only you can see and manage this key'}\n                                                </p>\n                                            </div>\n                                        </div>\n                                        <Switch\n                                            id=\"isGlobal\"\n                                            checked={newKeyIsGlobal}\n                                            onCheckedChange={setNewKeyIsGlobal}\n                                        />\n                                    </div>\n                                </div>\n                                <DialogFooter>\n                                    <Button type=\"submit\" disabled={!newKeyName.trim()}>\n                                        Create Key\n                                    </Button>\n                                </DialogFooter>\n                            </form>\n                        </DialogContent>\n                        </Dialog>\n                    )}\n                </div>\n            </div>\n\n            <AnimatePresence>\n                {newKeyData && (\n                    <NewKeyAlert\n                        keyData={newKeyData}\n                        onClose={() => setNewKeyData(null)}\n                        onCopy={handleCopyKey}\n                    />\n                )}\n            </AnimatePresence>\n\n            {/* Main Content - Table and SDK Card */}\n            <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n                {/* API Keys Card */}\n                <div className=\"lg:col-span-2\">\n                    <Card className=\"shadow-sm\">\n                        <CardHeader className=\"pb-3 border-b flex flex-row items-center justify-between\">\n                            <CardTitle className=\"text-base font-medium\">Your API Keys</CardTitle>\n                            {isAdmin && uniqueUsers.length > 0 && (\n                                <div className=\"flex items-center gap-2\">\n                                    <Filter className=\"h-4 w-4 text-muted-foreground\" />\n                                    <Select value={userFilter} onValueChange={setUserFilter}>\n                                        <SelectTrigger className=\"w-[200px] h-9\">\n                                            <SelectValue placeholder=\"Filter by user\" />\n                                        </SelectTrigger>\n                                        <SelectContent>\n                                            <SelectItem value=\"all\">All Users</SelectItem>\n                                            {uniqueUsers.map((u) => (\n                                                <SelectItem key={u.id} value={u.id}>\n                                                    {u.name || u.email}\n                                                </SelectItem>\n                                            ))}\n                                        </SelectContent>\n                                    </Select>\n                                </div>\n                            )}\n                        </CardHeader>\n\n                        {/* API Keys Table */}\n                        <div className=\"overflow-x-auto\">\n                            <table className=\"w-full\">\n                                <thead>\n                                <tr className=\"border-b\">\n                                    <th className=\"text-left text-xs font-medium text-muted-foreground uppercase tracking-wider py-3 px-6\">\n                                        Name\n                                    </th>\n                                    {isAdmin && (\n                                        <th className=\"text-left text-xs font-medium text-muted-foreground uppercase tracking-wider py-3 px-6\">\n                                            Owner\n                                        </th>\n                                    )}\n                                    <th className=\"text-left text-xs font-medium text-muted-foreground uppercase tracking-wider py-3 px-6\">\n                                        Visibility\n                                    </th>\n                                    <th className=\"text-left text-xs font-medium text-muted-foreground uppercase tracking-wider py-3 px-6\">\n                                        Permissions\n                                    </th>\n                                    <th className=\"text-left text-xs font-medium text-muted-foreground uppercase tracking-wider py-3 px-6\">\n                                        Created\n                                    </th>\n                                    <th className=\"text-left text-xs font-medium text-muted-foreground uppercase tracking-wider py-3 px-6\">\n                                        Status\n                                    </th>\n                                    <th className=\"text-right text-xs font-medium text-muted-foreground uppercase tracking-wider py-3 px-6\">\n                                        Actions\n                                    </th>\n                                </tr>\n                                </thead>\n                                <tbody className=\"divide-y\">\n                                {apiKeys && apiKeys.length > 0 ? (\n                                    apiKeys.map((key) => (\n                                        <tr\n                                            key={key.id}\n                                            className=\"hover:bg-muted/50 transition-colors\"\n                                        >\n                                            <td className=\"py-4 px-6 text-sm font-medium\">\n                                                {key.name}\n                                            </td>\n                                            {isAdmin && (\n                                                <td className=\"py-4 px-6 text-sm text-muted-foreground\">\n                                                    <div>\n                                                        <div className=\"font-medium text-foreground\">{key.user.name || 'Unknown'}</div>\n                                                        <div className=\"text-xs\">{key.user.email}</div>\n                                                    </div>\n                                                </td>\n                                            )}\n                                            <td className=\"py-4 px-6 text-sm\">\n                                                {key.isGlobal ? (\n                                                    <Badge variant=\"secondary\" className=\"bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-500/20\">\n                                                        <Eye className=\"h-3 w-3 mr-1\" />\n                                                        Global\n                                                    </Badge>\n                                                ) : (\n                                                    <Badge variant=\"outline\" className=\"text-muted-foreground\">\n                                                        <EyeOff className=\"h-3 w-3 mr-1\" />\n                                                        Private\n                                                    </Badge>\n                                                )}\n                                            </td>\n                                            <td className=\"py-4 px-6 text-sm\">\n                                                <div className=\"flex items-center gap-1\">\n                                                    <Badge variant=\"secondary\" className=\"text-xs\">\n                                                        {key.permissions.length} permission{key.permissions.length !== 1 ? 's' : ''}\n                                                    </Badge>\n                                                </div>\n                                            </td>\n                                            <td className=\"py-4 px-6 text-sm text-muted-foreground\">\n                                                {format(new Date(key.createdAt), 'PPP')}\n                                            </td>\n                                            <td className=\"py-4 px-6 text-sm\">\n                                                {key.isRevoked ? (\n                                                    <Badge variant=\"destructive\">Revoked</Badge>\n                                                ) : (\n                                                    <Badge variant=\"default\">Active</Badge>\n                                                )}\n                                            </td>\n                                            <td className=\"py-2 px-6 text-sm text-right\">\n                                                <div className=\"flex items-center justify-end gap-2\">\n                                                    {!key.isRevoked && (\n                                                        <>\n                                                            <Button\n                                                                variant=\"ghost\"\n                                                                size=\"sm\"\n                                                                onClick={() => setRenameKey(key)}\n                                                                className=\"h-8 w-8 p-0\"\n                                                            >\n                                                                <span className=\"sr-only\">Rename</span>\n                                                                <Pencil className=\"h-4 w-4\" />\n                                                            </Button>\n                                                            <AlertDialog>\n                                                                <AlertDialogTrigger asChild>\n                                                                    <Button\n                                                                        variant=\"ghost\"\n                                                                        size=\"sm\"\n                                                                        className=\"h-8 w-8 p-0 text-destructive/80 hover:text-destructive hover:bg-destructive/10\"\n                                                                    >\n                                                                        <span className=\"sr-only\">Revoke</span>\n                                                                        <Ban className=\"h-4 w-4\" />\n                                                                    </Button>\n                                                                </AlertDialogTrigger>\n                                                                <AlertDialogContent>\n                                                                    <AlertDialogHeader>\n                                                                        <AlertDialogTitle>Revoke API Key</AlertDialogTitle>\n                                                                        <AlertDialogDescription>\n                                                                            Are you sure you want to\n                                                                            revoke &ldquo;{key.name}&rdquo;?\n                                                                            This will immediately prevent any further use of\n                                                                            this key.\n                                                                        </AlertDialogDescription>\n                                                                    </AlertDialogHeader>\n                                                                    <AlertDialogFooter>\n                                                                        <AlertDialogCancel>Cancel</AlertDialogCancel>\n                                                                        <AlertDialogAction\n                                                                            onClick={() => revokeApiKey.mutate(key.id)}\n                                                                            className=\"bg-destructive hover:bg-destructive/90 focus:ring-destructive\"\n                                                                        >\n                                                                            Revoke Key\n                                                                        </AlertDialogAction>\n                                                                    </AlertDialogFooter>\n                                                                </AlertDialogContent>\n                                                            </AlertDialog>\n                                                        </>\n                                                    )}\n                                                    {key.isRevoked && (\n                                                        <AlertDialog>\n                                                            <AlertDialogTrigger asChild>\n                                                                <Button\n                                                                    variant=\"ghost\"\n                                                                    size=\"sm\"\n                                                                    className=\"h-8 w-8 p-0 text-destructive/80 hover:text-destructive hover:bg-destructive/10\"\n                                                                >\n                                                                    <span className=\"sr-only\">Delete</span>\n                                                                    <Trash2 className=\"h-4 w-4\" />\n                                                                </Button>\n                                                            </AlertDialogTrigger>\n                                                            <AlertDialogContent>\n                                                                <AlertDialogHeader>\n                                                                    <AlertDialogTitle>Delete API Key</AlertDialogTitle>\n                                                                    <AlertDialogDescription>\n                                                                        Are you sure you want to permanently\n                                                                        delete &ldquo;{key.name}&rdquo;?\n                                                                        This action cannot be undone.\n                                                                    </AlertDialogDescription>\n                                                                </AlertDialogHeader>\n                                                                <AlertDialogFooter>\n                                                                    <AlertDialogCancel>Cancel</AlertDialogCancel>\n                                                                    <AlertDialogAction\n                                                                        onClick={() => deleteApiKey.mutate(key.id)}\n                                                                        className=\"bg-destructive hover:bg-destructive/90 focus:ring-destructive\"\n                                                                    >\n                                                                        Delete Key\n                                                                    </AlertDialogAction>\n                                                                </AlertDialogFooter>\n                                                            </AlertDialogContent>\n                                                        </AlertDialog>\n                                                    )}\n                                                </div>\n                                            </td>\n                                        </tr>\n                                    ))\n                                ) : (\n                                    <tr>\n                                        <td colSpan={isAdmin ? 7 : 6} className=\"py-16 text-center\">\n                                            <div className=\"flex flex-col items-center justify-center\">\n                                                <div className=\"rounded-full p-4 mb-4 bg-muted\">\n                                                    <Key className=\"h-8 w-8 text-muted-foreground/60\" />\n                                                </div>\n                                                <h3 className=\"text-lg font-medium mb-1\">No API Keys</h3>\n                                                <p className=\"text-sm text-muted-foreground mb-4 max-w-sm\">\n                                                    {canCreateKeys\n                                                        ? 'Create an API key to get started with the Changerawr API.'\n                                                        : 'Only administrators can create API keys for this project. Contact an administrator to request an API key.'\n                                                    }\n                                                </p>\n                                                {canCreateKeys && (\n                                                    <Button\n                                                        onClick={() => setIsCreateDialogOpen(true)}\n                                                        size=\"sm\"\n                                                    >\n                                                        <Plus className=\"h-4 w-4 mr-2\" />\n                                                        Create Key\n                                                    </Button>\n                                                )}\n                                            </div>\n                                        </td>\n                                    </tr>\n                                )}\n                                </tbody>\n                            </table>\n                        </div>\n                    </Card>\n                </div>\n\n                {/* SDKs Showcase - Right Side */}\n                <div className=\"lg:col-span-1\">\n                    <SDKShowcaseCompact />\n                </div>\n            </div>\n\n            <RenameDialog\n                open={!!renameKey}\n                onOpenChange={(open) => !open && setRenameKey(null)}\n                currentName={renameKey?.name ?? ''}\n                onRename={async (newName) => {\n                    if (!renameKey) return;\n                    await renameApiKey.mutateAsync({ id: renameKey.id, name: newName });\n                }}\n            />\n\n            <PermissionsModal\n                open={isPermissionsModalOpen}\n                onOpenChange={setIsPermissionsModalOpen}\n                selectedPermissions={newKeyPermissions}\n                onSave={setNewKeyPermissions}\n            />\n        </div>\n    );\n}"
  },
  {
    "path": "app/dashboard/projects/[projectId]/catch-up/page.tsx",
    "content": "'use client';\n\nimport {use, useState} from 'react';\nimport {useQuery} from '@tanstack/react-query';\nimport {motion} from 'framer-motion';\nimport Link from 'next/link';\nimport {Calendar, Clock, Copy, FileText, TrendingUp, Sparkles} from 'lucide-react';\nimport {Button} from '@/components/ui/button';\nimport {Card, CardContent} from '@/components/ui/card';\nimport {Skeleton} from '@/components/ui/skeleton';\nimport {Badge} from '@/components/ui/badge';\nimport {useToast} from '@/hooks/use-toast';\nimport {SinceSelector} from '@/components/project/catch-up/SinceSelector';\nimport type {CatchUpResponse} from '@/lib/types/projects/catch-up/types';\nimport {formatDistanceToNow, format} from 'date-fns';\nimport {RenderMarkdown} from \"@/components/markdown-editor/RenderMarkdown\";\n\ninterface CatchUpPageProps {\n    params: Promise<{ projectId: string }>;\n}\n\ninterface ProjectSummaryResponse {\n    summary: string;\n    highlights: string[];\n    tone: 'exciting' | 'steady' | 'minimal';\n    readingTime: number;\n}\n\nconst fadeIn = {\n    initial: {opacity: 0, y: 30},\n    animate: {opacity: 1, y: 0},\n    transition: {duration: 0.7, ease: \"easeOut\"}\n};\n\nconst slideIn = {\n    initial: {opacity: 0, x: -20},\n    animate: {opacity: 1, x: 0},\n    transition: {duration: 0.6, ease: \"easeOut\"}\n};\n\nexport default function CatchUpPage({params}: CatchUpPageProps) {\n    const {projectId} = use(params);\n    const [since, setSince] = useState('auto');\n    const {toast} = useToast();\n\n    // Fetch catch-up data\n    const {\n        data: catchUpData,\n        isLoading: isLoadingCatchUp,\n        error: catchUpError,\n    } = useQuery<CatchUpResponse>({\n        queryKey: ['catch-up', projectId, since],\n        queryFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}/catch-up?since=${encodeURIComponent(since)}`);\n            if (!response.ok) {\n                throw new Error('Failed to fetch catch-up data');\n            }\n            return response.json();\n        },\n        staleTime: 1000 * 60 * 5, // 5 minutes\n    });\n\n    // Fetch project summary\n    const {\n        data: projectSummary,\n        isLoading: isLoadingSummary,\n        error: summaryError,\n    } = useQuery<ProjectSummaryResponse>({\n        queryKey: ['project-summary', projectId, since],\n        queryFn: async () => {\n            if (!catchUpData || catchUpData.totalEntries === 0) {\n                return null;\n            }\n\n            const response = await fetch(`/api/projects/${projectId}/catch-up/ai-summary`, {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({\n                    since,\n                    entries: catchUpData.entries,\n                    summary: catchUpData.summary,\n                    fromDate: catchUpData.fromDate,\n                }),\n            });\n\n            if (!response.ok) {\n                throw new Error('Failed to generate project summary');\n            }\n\n            return response.json();\n        },\n        enabled: !!catchUpData && catchUpData.totalEntries > 0,\n        staleTime: 1000 * 60 * 10, // 10 minutes\n    });\n\n    const handleCopyPost = async () => {\n        if (!projectSummary || !catchUpData) return;\n\n        const post = `# What's New Since ${format(new Date(catchUpData.fromDate), 'MMMM do')}\n\n${projectSummary.summary}\n\n---\n\nPublished on ${new Date().toLocaleDateString()}`;\n\n        try {\n            await navigator.clipboard.writeText(post);\n            toast({\n                title: \"Copied!\",\n                description: \"Post copied to clipboard\",\n            });\n        } catch {\n            toast({\n                title: \"Error\",\n                description: \"Failed to copy post\",\n                variant: \"destructive\",\n            });\n        }\n    };\n\n    const isLoading = isLoadingCatchUp || isLoadingSummary;\n    const hasError = catchUpError || summaryError;\n\n    if (hasError) {\n        return (\n            <div className=\"min-h-screen flex items-center justify-center p-8\">\n                <Card className=\"max-w-lg w-full\">\n                    <CardContent className=\"pt-16 pb-16 text-center space-y-6\">\n                        <div className=\"text-8xl\">💔</div>\n                        <div className=\"space-y-3\">\n                            <h2 className=\"text-3xl font-bold\">Something went wrong</h2>\n                            <p className=\"text-muted-foreground text-lg\">\n                                Unable to load your project update right now.\n                            </p>\n                        </div>\n                        <Button onClick={() => window.location.reload()} size=\"lg\">\n                            Try Again\n                        </Button>\n                    </CardContent>\n                </Card>\n            </div>\n        );\n    }\n\n    return (\n        <div className=\"min-h-screen bg-gradient-to-br from-background via-background to-muted/30\">\n            {/* Full Width Header */}\n            <div className=\"w-full border-b bg-card/50 backdrop-blur-sm sticky top-0 z-10\">\n                <div className=\"max-w-7xl mx-auto px-8 py-6\">\n                    <div className=\"flex items-center justify-between\">\n                        <motion.div initial=\"initial\" animate=\"animate\" variants={slideIn}>\n                            <Badge variant=\"outline\" className=\"gap-2 text-sm\">\n                                <Calendar className=\"h-4 w-4\"/>\n                                {format(new Date(), 'EEEE, MMMM do, yyyy')}\n                            </Badge>\n                        </motion.div>\n\n                        {projectSummary && (\n                            <motion.div\n                                initial=\"initial\"\n                                animate=\"animate\"\n                                variants={slideIn}\n                                className=\"flex items-center gap-4\"\n                            >\n                                <div className=\"flex items-center gap-4 text-sm text-muted-foreground\">\n                                    <div className=\"flex items-center gap-1\">\n                                        <Clock className=\"h-4 w-4\"/>\n                                        {projectSummary.readingTime} min\n                                    </div>\n                                    <div className=\"flex items-center gap-1\">\n                                        <Sparkles className=\"h-4 w-4\"/>\n                                        Fresh content\n                                    </div>\n                                </div>\n                                <Button onClick={handleCopyPost} variant=\"outline\" size=\"sm\" className=\"gap-2\">\n                                    <Copy className=\"h-4 w-4\"/>\n                                    Copy\n                                </Button>\n                            </motion.div>\n                        )}\n                    </div>\n                </div>\n            </div>\n\n            <div className=\"max-w-7xl mx-auto px-8 py-12\">\n                <div className=\"grid grid-cols-12 gap-12\">\n                    {/* Left Sidebar - Controls */}\n                    <div className=\"col-span-12 lg:col-span-3 space-y-8\">\n                        <motion.div\n                            initial=\"initial\"\n                            animate=\"animate\"\n                            variants={fadeIn}\n                        >\n                            <Card className=\"sticky top-32\">\n                                <CardContent className=\"p-6 space-y-6\">\n                                    <div className=\"space-y-3\">\n                                        <h3 className=\"font-semibold\">Time Period</h3>\n                                        <p className=\"text-sm text-muted-foreground\">\n                                            See what&apos;s changed since a specific point\n                                        </p>\n                                    </div>\n                                    <SinceSelector value={since} onChange={setSince} projectId={projectId}/>\n                                </CardContent>\n                            </Card>\n                        </motion.div>\n\n                        {/* Stats Sidebar */}\n                        {catchUpData && catchUpData.totalEntries > 0 && (\n                            <motion.div\n                                initial=\"initial\"\n                                animate=\"animate\"\n                                variants={fadeIn}\n                                transition={{delay: 0.2}}\n                            >\n                                <Card>\n                                    <CardContent className=\"p-6 space-y-6\">\n                                        <h3 className=\"font-semibold\">Update Summary</h3>\n                                        <div className=\"space-y-4\">\n                                            <div className=\"flex items-center justify-between\">\n                                                <span className=\"text-sm text-muted-foreground\">Features</span>\n                                                <Badge variant=\"secondary\"\n                                                       className=\"bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300\">\n                                                    {catchUpData.summary.features}\n                                                </Badge>\n                                            </div>\n                                            <div className=\"flex items-center justify-between\">\n                                                <span className=\"text-sm text-muted-foreground\">Bug Fixes</span>\n                                                <Badge variant=\"secondary\"\n                                                       className=\"bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300\">\n                                                    {catchUpData.summary.fixes}\n                                                </Badge>\n                                            </div>\n                                            <div className=\"flex items-center justify-between\">\n                                                <span className=\"text-sm text-muted-foreground\">Improvements</span>\n                                                <Badge variant=\"secondary\"\n                                                       className=\"bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300\">\n                                                    {catchUpData.summary.other}\n                                                </Badge>\n                                            </div>\n                                        </div>\n                                    </CardContent>\n                                </Card>\n                            </motion.div>\n                        )}\n                    </div>\n\n                    {/* Main Content Area */}\n                    <div className=\"col-span-12 lg:col-span-9\">\n                        {/* Hero Section */}\n                        <motion.div\n                            initial=\"initial\"\n                            animate=\"animate\"\n                            variants={fadeIn}\n                            className=\"space-y-8 mb-12\"\n                        >\n                            <div className=\"space-y-6\">\n                                <h1 className=\"text-6xl md:text-7xl font-bold leading-tight\">\n                                    {catchUpData && catchUpData.totalEntries > 0 ? (\n                                        <>What&apos;s New Since {format(new Date(catchUpData.fromDate), 'MMMM do')}</>\n                                    ) : (\n                                        'Project Update'\n                                    )}\n                                </h1>\n\n                                <p className=\"text-2xl text-muted-foreground leading-relaxed max-w-4xl\">\n                                    {catchUpData && catchUpData.totalEntries > 0 ? (\n                                        `Catching you up on ${catchUpData.totalEntries} update${catchUpData.totalEntries !== 1 ? 's' : ''} and what they mean for the project`\n                                    ) : (\n                                        'Stay current with the latest developments and improvements'\n                                    )}\n                                </p>\n                            </div>\n                        </motion.div>\n\n                        {/* Loading State */}\n                        {isLoading && (\n                            <motion.div\n                                initial=\"initial\"\n                                animate=\"animate\"\n                                variants={fadeIn}\n                                className=\"space-y-12\"\n                            >\n                                <div className=\"space-y-4\">\n                                    <Skeleton className=\"h-12 w-full\"/>\n                                    <Skeleton className=\"h-8 w-3/4\"/>\n                                    <Skeleton className=\"h-6 w-full\"/>\n                                    <Skeleton className=\"h-6 w-full\"/>\n                                    <Skeleton className=\"h-6 w-2/3\"/>\n                                </div>\n                                <div className=\"space-y-4\">\n                                    <Skeleton className=\"h-6 w-full\"/>\n                                    <Skeleton className=\"h-6 w-full\"/>\n                                    <Skeleton className=\"h-6 w-4/5\"/>\n                                </div>\n                            </motion.div>\n                        )}\n\n                        {/* Empty State */}\n                        {catchUpData && catchUpData.totalEntries === 0 && (\n                            <motion.div\n                                initial=\"initial\"\n                                animate=\"animate\"\n                                variants={fadeIn}\n                                className=\"text-center py-24\"\n                            >\n                                <div className=\"space-y-8\">\n                                    <div className=\"text-9xl\">🎯</div>\n                                    <div className=\"space-y-4\">\n                                        <h2 className=\"text-5xl font-bold\">You&apos;re All Caught Up!</h2>\n                                        <p className=\"text-xl text-muted-foreground max-w-2xl mx-auto\">\n                                            No new updates\n                                            since {formatDistanceToNow(new Date(catchUpData.fromDate), {addSuffix: true})}.\n                                            Everything is current and up to date.\n                                        </p>\n                                    </div>\n                                    <Button asChild size=\"lg\" className=\"rounded-full px-8\">\n                                        <Link href={`/dashboard/projects/${projectId}/changelog/new`}>\n                                            <FileText className=\"h-5 w-5 mr-2\"/>\n                                            Create New Update\n                                        </Link>\n                                    </Button>\n                                </div>\n                            </motion.div>\n                        )}\n\n                        {/* Main Content */}\n                        {catchUpData && projectSummary && catchUpData.totalEntries > 0 && (\n                            <motion.div\n                                initial=\"initial\"\n                                animate=\"animate\"\n                                variants={fadeIn}\n                                className=\"space-y-12\"\n                            >\n                                {/* Main Article */}\n                                <Card className=\"border-0 shadow-xl\">\n                                    <CardContent className=\"p-16\">\n                                        <div\n                                            className=\"prose prose-xl dark:prose-invert max-w-none prose-headings:font-bold prose-p:leading-relaxed prose-p:text-lg\">\n                                            <RenderMarkdown>{projectSummary.summary}</RenderMarkdown>\n                                        </div>\n                                    </CardContent>\n                                </Card>\n\n                                {/* Key Highlights */}\n                                {projectSummary.highlights && projectSummary.highlights.length > 0 && (\n                                    <Card className=\"border-primary/20 bg-primary/5\">\n                                        <CardContent className=\"p-12\">\n                                            <div className=\"space-y-8\">\n                                                <div className=\"flex items-center gap-4\">\n                                                    <div className=\"p-3 bg-primary/20 rounded-xl\">\n                                                        <TrendingUp className=\"h-6 w-6 text-primary\"/>\n                                                    </div>\n                                                    <h3 className=\"text-2xl font-bold\">Key Highlights</h3>\n                                                </div>\n\n                                                <div className=\"grid gap-6\">\n                                                    {projectSummary.highlights.map((highlight, index) => (\n                                                        <motion.div\n                                                            key={index}\n                                                            initial={{opacity: 0, x: -20}}\n                                                            animate={{opacity: 1, x: 0}}\n                                                            transition={{delay: index * 0.1}}\n                                                            className=\"flex items-start gap-6 p-6 bg-background/80 backdrop-blur-sm rounded-xl border\"\n                                                        >\n                                                            <div\n                                                                className=\"h-3 w-3 rounded-full bg-primary mt-2 flex-shrink-0\"/>\n                                                            <p className=\"text-lg leading-relaxed\">{highlight}</p>\n                                                        </motion.div>\n                                                    ))}\n                                                </div>\n                                            </div>\n                                        </CardContent>\n                                    </Card>\n                                )}\n\n                                {/* Footer */}\n                                <div className=\"text-center py-12 space-y-6\">\n                                    <div className=\"h-px bg-gradient-to-r from-transparent via-border to-transparent\"/>\n                                    <p className=\"text-lg text-muted-foreground\">\n                                        Thanks for staying up to date with the project progress!\n                                    </p>\n                                    <Button onClick={handleCopyPost} variant=\"outline\" size=\"lg\" className=\"gap-3\">\n                                        <Copy className=\"h-5 w-5\"/>\n                                        Copy This Update\n                                    </Button>\n                                </div>\n                            </motion.div>\n                        )}\n                    </div>\n                </div>\n            </div>\n        </div>\n    );\n}"
  },
  {
    "path": "app/dashboard/projects/[projectId]/changelog/[entryId]/page.tsx",
    "content": "'use client'\n\nimport { use } from 'react'\nimport { ChangelogEditor } from '@/components/changelog/ChangelogEditor'\n\ninterface ChangelogPageProps {\n    params: Promise<{\n        projectId: string\n        entryId?: string\n    }>\n}\n\nexport default function ChangelogEntryPage({ params }: ChangelogPageProps) {\n    const { projectId, entryId } = use(params)\n\n    return <ChangelogEditor projectId={projectId} entryId={entryId} />\n}"
  },
  {
    "path": "app/dashboard/projects/[projectId]/changelog/new/page.tsx",
    "content": "'use client'\n\nimport { use } from 'react'\nimport { useSearchParams } from 'next/navigation'\nimport { ChangelogEditor } from '@/components/changelog/ChangelogEditor'\nimport { useEffect, useState } from 'react'\n\ninterface NewChangelogPageProps {\n    params: Promise<{\n        projectId: string\n    }>\n}\n\nexport default function NewChangelogEntryPage({ params }: NewChangelogPageProps) {\n    const { projectId } = use(params)\n    const searchParams = useSearchParams()\n    const [initialContent, setInitialContent] = useState<string>('')\n    const [initialVersion, setInitialVersion] = useState<string>('')\n    const [initialTitle, setInitialTitle] = useState<string>('')\n\n    useEffect(() => {\n        // Get content from URL parameter and decode it\n        const contentParam = searchParams.get('content')\n        const versionParam = searchParams.get('version')\n        const titleParam = searchParams.get('title')\n\n        if (contentParam) {\n            try {\n                // Decode the URL-encoded content\n                const decodedContent = decodeURIComponent(contentParam)\n                setInitialContent(decodedContent)\n\n                // If no title is provided, try to extract one from the content\n                if (!titleParam && decodedContent) {\n                    const titleMatch = decodedContent.match(/^#\\s+(.+)/m)\n                    if (titleMatch) {\n                        setInitialTitle(titleMatch[1].trim())\n                    }\n                }\n            } catch (error) {\n                console.warn('Failed to decode content parameter:', error)\n            }\n        }\n\n        if (versionParam) {\n            setInitialVersion(versionParam)\n        }\n\n        if (titleParam) {\n            setInitialTitle(titleParam)\n        }\n    }, [searchParams])\n\n    return (\n        <ChangelogEditor\n            projectId={projectId}\n            isNewChangelog={true}\n            initialContent={initialContent}\n            initialVersion={initialVersion}\n            initialTitle={initialTitle}\n        />\n    )\n}"
  },
  {
    "path": "app/dashboard/projects/[projectId]/changelog/page.tsx",
    "content": "'use client'\n\nimport {use, useState} from 'react'\nimport Link from 'next/link'\nimport {useQuery} from '@tanstack/react-query'\nimport {useDebounce} from 'use-debounce'\nimport {format} from 'date-fns'\nimport {AnimatePresence, motion} from 'framer-motion'\nimport {ArrowUpDown, Calendar, ChevronRight, LayoutGrid, List, Plus, Search, Tag} from 'lucide-react'\nimport {Input} from '@/components/ui/input'\nimport {Button} from '@/components/ui/button'\nimport {Card, CardContent} from \"@/components/ui/card\"\nimport {Badge} from \"@/components/ui/badge\"\nimport {compareVersions} from 'compare-versions'\nimport {ChangelogEntry, ChangelogTag as ChTag} from '@/lib/types/changelog'\nimport {CatchUpView} from \"@/components/project/catch-up/CatchUpView\";\n\ninterface ChangelogPageProps {\n    params: Promise<{ projectId: string }>\n}\n\ntype ViewMode = 'grid' | 'list'\ntype SortOrder = 'newest' | 'oldest' | 'version'\n\nexport default function ChangelogPage({params}: ChangelogPageProps) {\n    const {projectId} = use(params)\n    const [searchInput, setSearchInput] = useState('')\n    const [search] = useDebounce(searchInput, 500)\n    const [selectedTag, setSelectedTag] = useState<string | null>(null)\n    const [viewMode, setViewMode] = useState<ViewMode>('grid')\n    const [sortOrder, setSortOrder] = useState<SortOrder>('newest')\n\n    const {data, isLoading} = useQuery({\n        queryKey: ['changelog', projectId, search, selectedTag],\n        queryFn: async () => {\n            const searchParams = new URLSearchParams({\n                ...(search && {search}),\n                ...(selectedTag && {tag: selectedTag})\n            })\n\n            const response = await fetch(\n                `/api/projects/${projectId}/changelog?${searchParams}`\n            )\n            if (!response.ok) throw new Error('Failed to fetch changelog')\n            return response.json()\n        }\n    })\n\n    const sortedEntries = data?.entries ? [...data.entries].sort((a, b) => {\n        if (sortOrder === 'newest') {\n            return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()\n        }\n        if (sortOrder === 'oldest') {\n            return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()\n        }\n        if (a.version && b.version) {\n            try {\n                return -compareVersions(a.version, b.version)\n            } catch {\n                return 0\n            }\n        }\n        if (a.version) return -1\n        if (b.version) return 1\n        return 0\n    }) : []\n\n    const container = {\n        hidden: {opacity: 0},\n        show: {\n            opacity: 1,\n            transition: {\n                staggerChildren: 0.05\n            }\n        }\n    }\n\n    const item = {\n        hidden: {y: 20, opacity: 0},\n        show: {y: 0, opacity: 1}\n    }\n\n    const fadeIn = {\n        initial: {opacity: 0, y: 20},\n        animate: {opacity: 1, y: 0},\n        transition: {duration: 0.5}\n    };\n\n    return (\n        <div className=\"h-full min-h-screen bg-background\">\n            <div className=\"container py-8 space-y-8\">\n                {/* Header */}\n                <div\n                    className=\"relative overflow-hidden rounded-2xl bg-gradient-to-r from-blue-600 to-indigo-600 p-8 mb-8 shadow-lg\">\n                    <div className=\"absolute inset-0 bg-grid-white/[0.2] bg-[size:16px_16px]\"/>\n                    <div\n                        className=\"relative flex flex-col md:flex-row justify-between items-start md:items-center gap-4\">\n                        <div>\n                            <h1 className=\"text-3xl font-bold text-white mb-2\">Changelog Management</h1>\n                            <p className=\"text-blue-100\">Manage and organize your changelog entries</p>\n                        </div>\n                        <Button\n                            asChild\n                            className=\"bg-white/90 hover:bg-white text-blue-600 border-none shadow-md hover:shadow-lg transition-all\"\n                        >\n                            <Link href={`/dashboard/projects/${projectId}/changelog/new`}>\n                                <Plus className=\"h-4 w-4 mr-2\"/>\n                                New Entry\n                            </Link>\n                        </Button>\n                    </div>\n                </div>\n\n                {/* Controls */}\n                <Card>\n                    <CardContent className=\"p-6 space-y-4\">\n                        <div className=\"flex flex-col sm:flex-row gap-4\">\n                            <div className=\"relative flex-1\">\n                                <Search\n                                    className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground\"/>\n                                <Input\n                                    placeholder=\"Search entries...\"\n                                    value={searchInput}\n                                    onChange={(e) => setSearchInput(e.target.value)}\n                                    className=\"pl-10\"\n                                />\n                            </div>\n                            <div className=\"flex gap-2\">\n                                <Button\n                                    variant=\"outline\"\n                                    onClick={() => setSortOrder(current => {\n                                        const orders: SortOrder[] = ['newest', 'oldest', 'version']\n                                        const currentIndex = orders.indexOf(current)\n                                        return orders[(currentIndex + 1) % orders.length]\n                                    })}\n                                >\n                                    <ArrowUpDown className=\"h-4 w-4 mr-2\"/>\n                                    {sortOrder === 'newest' ? 'Newest' :\n                                        sortOrder === 'oldest' ? 'Oldest' : 'Version'}\n                                </Button>\n                                <div className=\"flex rounded-md overflow-hidden border\">\n                                    <Button\n                                        variant={viewMode === 'grid' ? \"default\" : \"ghost\"}\n                                        onClick={() => setViewMode('grid')}\n                                        className=\"rounded-none px-3\"\n                                    >\n                                        <LayoutGrid className=\"h-4 w-4\"/>\n                                    </Button>\n                                    <Button\n                                        variant={viewMode === 'list' ? \"default\" : \"ghost\"}\n                                        onClick={() => setViewMode('list')}\n                                        className=\"rounded-none px-3\"\n                                    >\n                                        <List className=\"h-4 w-4\"/>\n                                    </Button>\n                                </div>\n                            </div>\n                        </div>\n\n                        {/* Enhanced Tag Filter Section with Colors */}\n                        {data?.tags && data.tags.length > 0 && (\n                            <div className=\"space-y-2\">\n                                <div className=\"flex items-center gap-2\">\n                                    <Tag className=\"h-4 w-4 text-muted-foreground\"/>\n                                    <span className=\"text-sm font-medium text-muted-foreground\">Filter by tags:</span>\n                                    {selectedTag && (\n                                        <Button\n                                            variant=\"ghost\"\n                                            size=\"sm\"\n                                            onClick={() => setSelectedTag(null)}\n                                            className=\"h-6 px-2 text-xs\"\n                                        >\n                                            Clear filter\n                                        </Button>\n                                    )}\n                                </div>\n                                <div className=\"flex gap-2 flex-wrap\">\n                                    {data.tags.map((tag: ChTag) => {\n                                        const isSelected = selectedTag === tag.name;\n                                        return (\n                                            <Button\n                                                key={tag.id}\n                                                variant={isSelected ? \"default\" : \"outline\"}\n                                                size=\"sm\"\n                                                onClick={() => setSelectedTag(isSelected ? null : tag.name)}\n                                                className=\"flex items-center gap-2\"\n                                            >\n                                                {tag.color && (\n                                                    <div\n                                                        className=\"h-3 w-3 rounded-full border border-white/20\"\n                                                        style={{backgroundColor: tag.color}}\n                                                    />\n                                                )}\n                                                {tag.name}\n                                            </Button>\n                                        );\n                                    })}\n                                </div>\n                            </div>\n                        )}\n                    </CardContent>\n                </Card>\n\n                {/* Catch-Up Section - NEW */}\n                <motion.div\n                    initial=\"initial\"\n                    animate=\"animate\"\n                    variants={fadeIn}\n                >\n                    <CatchUpView projectId={projectId} />\n                </motion.div>\n\n                {/* Entries */}\n                <AnimatePresence mode=\"wait\">\n                    {isLoading ? (\n                        <motion.div\n                            key=\"loading\"\n                            initial={{opacity: 0}}\n                            animate={{opacity: 1}}\n                            exit={{opacity: 0}}\n                            className={viewMode === 'grid' ?\n                                \"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4\" :\n                                \"space-y-4\"\n                            }\n                        >\n                            {[...Array(6)].map((_, i) => (\n                                <Card key={i} className=\"animate-pulse\">\n                                    <CardContent className=\"p-6\">\n                                        <div className=\"h-4 bg-muted rounded w-3/4 mb-4\"/>\n                                        <div className=\"h-4 bg-muted rounded w-1/2 mb-3\"/>\n                                        <div className=\"flex gap-2\">\n                                            <div className=\"h-6 bg-muted rounded w-16\"/>\n                                            <div className=\"h-6 bg-muted rounded w-20\"/>\n                                        </div>\n                                    </CardContent>\n                                </Card>\n                            ))}\n                        </motion.div>\n                    ) : (\n                        <motion.div\n                            key=\"content\"\n                            variants={container}\n                            initial=\"hidden\"\n                            animate=\"show\"\n                            className={viewMode === 'grid' ?\n                                \"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4\" :\n                                \"space-y-4\"\n                            }\n                        >\n                            {sortedEntries.map((entry: ChangelogEntry) => (\n                                <motion.div\n                                    key={entry.id}\n                                    variants={item}\n                                    whileHover={{y: -2}}\n                                >\n                                    <Card className=\"group transition-colors hover:border-primary/50\">\n                                        <CardContent className=\"p-6\">\n                                            <Link\n                                                href={`/dashboard/projects/${projectId}/changelog/${entry.id}`}\n                                                className=\"block\"\n                                            >\n                                                <div className=\"flex justify-between items-start mb-4\">\n                                                    <h3 className=\"font-semibold text-foreground group-hover:text-primary transition-colors line-clamp-2\">\n                                                        {entry.title}\n                                                    </h3>\n                                                    <ChevronRight\n                                                        className=\"h-5 w-5 text-muted-foreground group-hover:text-primary transition-all transform group-hover:translate-x-1 ml-2 flex-shrink-0\"/>\n                                                </div>\n\n                                                <div\n                                                    className=\"flex items-center gap-4 text-sm text-muted-foreground mb-4\">\n                                                    {entry.version && (\n                                                        <Badge variant=\"outline\" size=\"sm\">\n                                                            <Tag className=\"h-3 w-3 mr-1\"/>\n                                                            {entry.version}\n                                                        </Badge>\n                                                    )}\n                                                    <div className=\"flex items-center gap-1\">\n                                                        <Calendar className=\"h-3 w-3\"/>\n                                                        <span>{format(new Date(entry.createdAt), 'MMM d, yyyy')}</span>\n                                                    </div>\n                                                </div>\n\n                                                {/* Tags */}\n                                                {entry.tags.length > 0 && (\n                                                    <div className=\"flex flex-wrap gap-2\">\n                                                        {entry.tags.map((tag: ChTag) => (\n                                                            <Badge\n                                                                key={tag.id}\n                                                                variant=\"secondary\"\n                                                                color={tag.color || undefined}\n                                                                size=\"sm\"\n                                                                className=\"flex items-center gap-1.5\"\n                                                            >\n                                                                {tag.name}\n                                                            </Badge>\n                                                        ))}\n                                                    </div>\n                                                )}\n                                            </Link>\n                                        </CardContent>\n                                    </Card>\n                                </motion.div>\n                            ))}\n                        </motion.div>\n                    )}\n                </AnimatePresence>\n\n                {/* Empty State */}\n                {!isLoading && data?.entries.length === 0 && (\n                    <Card>\n                        <CardContent className=\"p-12 text-center\">\n                            <div\n                                className=\"w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center mx-auto mb-4\">\n                                <Plus className=\"h-6 w-6 text-primary\"/>\n                            </div>\n                            <h3 className=\"text-lg font-medium text-foreground mb-2\">\n                                {search || selectedTag ? 'No entries match your filters' : 'No entries found'}\n                            </h3>\n                            <p className=\"text-muted-foreground mb-6\">\n                                {search || selectedTag\n                                    ? 'Try adjusting your search or tag filters to find what you\\'re looking for.'\n                                    : 'Get started by creating your first changelog entry'\n                                }\n                            </p>\n                            {search || selectedTag ? (\n                                <div className=\"flex gap-2 justify-center\">\n                                    {search && (\n                                        <Button\n                                            variant=\"outline\"\n                                            onClick={() => setSearchInput('')}\n                                        >\n                                            Clear search\n                                        </Button>\n                                    )}\n                                    {selectedTag && (\n                                        <Button\n                                            variant=\"outline\"\n                                            onClick={() => setSelectedTag(null)}\n                                        >\n                                            Clear tag filter\n                                        </Button>\n                                    )}\n                                </div>\n                            ) : (\n                                <Button asChild>\n                                    <Link href={`/dashboard/projects/${projectId}/changelog/new`}>\n                                        <Plus className=\"h-4 w-4 mr-2\"/>\n                                        Create Entry\n                                    </Link>\n                                </Button>\n                            )}\n                        </CardContent>\n                    </Card>\n                )}\n            </div>\n        </div>\n    )\n}"
  },
  {
    "path": "app/dashboard/projects/[projectId]/domains/[domain]/client.tsx",
    "content": "'use client'\n\nimport { useState, useEffect } from 'react'\nimport { useRouter } from 'next/navigation'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Alert, AlertDescription } from '@/components/ui/alert'\nimport { Switch } from '@/components/ui/switch'\nimport { Label } from '@/components/ui/label'\nimport { Separator } from '@/components/ui/separator'\nimport {\n    Globe,\n    Shield,\n    CheckCircle,\n    Clock,\n    AlertTriangle,\n    ArrowLeft,\n    Copy,\n    ExternalLink,\n    Lock,\n    RefreshCw,\n    Trash2,\n    Zap,\n    RotateCw,\n    ShieldOff,\n} from 'lucide-react'\nimport { motion, AnimatePresence } from 'framer-motion'\nimport type { CustomDomain, DNSInstructions } from '@/lib/types/custom-domains'\nimport { SSLManagement } from '../components/ssl'\n\ninterface DomainSettingsClientProps {\n    projectId: string\n    domain: string\n}\n\nexport function DomainSettingsClient({ projectId, domain: domainName }: DomainSettingsClientProps) {\n    const router = useRouter()\n    const [domain, setDomain] = useState<CustomDomain | null>(null)\n    const [isLoading, setIsLoading] = useState(true)\n    const [isVerifying, setIsVerifying] = useState(false)\n    const [isDeleting, setIsDeleting] = useState(false)\n    const [error, setError] = useState<string | null>(null)\n    const [success, setSuccess] = useState<string | null>(null)\n    const [sslEnabled, setSslEnabled] = useState(false)\n    const [dnsInstructions, setDnsInstructions] = useState<DNSInstructions | null>(null)\n\n    useEffect(() => {\n        loadDomain()\n        loadRuntimeConfig()\n        loadDnsInstructions()\n    }, [])\n\n    useEffect(() => {\n        if (success || error) {\n            const timer = setTimeout(() => {\n                setSuccess(null)\n                setError(null)\n            }, 5000)\n            return () => clearTimeout(timer)\n        }\n    }, [success, error])\n\n    const loadRuntimeConfig = async () => {\n        try {\n            const response = await fetch('/api/config/runtime')\n            const config = await response.json()\n            setSslEnabled(config.sslEnabled)\n        } catch (error) {\n            console.error('Failed to load runtime config:', error)\n            // Default to false if config fetch fails\n            setSslEnabled(false)\n        }\n    }\n\n    const loadDnsInstructions = async () => {\n        // Only load DNS instructions if domain is not verified\n        if (domain && domain.verified) return\n\n        try {\n            const response = await fetch(`/api/custom-domains/${encodeURIComponent(domainName)}/dns-instructions`)\n            const result = await response.json()\n            if (result.success) {\n                setDnsInstructions(result.dnsInstructions)\n            }\n        } catch (error) {\n            console.error('Failed to load DNS instructions:', error)\n        }\n    }\n\n    const loadDomain = async () => {\n        try {\n            setIsLoading(true)\n            const response = await fetch(`/api/custom-domains/list?scope=project&projectId=${projectId}`)\n            const result = await response.json()\n\n            if (result.success) {\n                const foundDomain = result.domains?.find((d: CustomDomain) => d.domain === domainName)\n                if (foundDomain) {\n                    setDomain(foundDomain)\n                } else {\n                    setError('Domain not found')\n                }\n            } else {\n                setError(result.error || 'Failed to load domain')\n            }\n        } catch (error) {\n            setError('Failed to load domain')\n            console.error(error)\n        } finally {\n            setIsLoading(false)\n        }\n    }\n\n    const handleVerifyDomain = async () => {\n        setIsVerifying(true)\n        setError(null)\n\n        try {\n            const response = await fetch('/api/custom-domains/verify', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({ domain: domainName }),\n            })\n\n            const result = await response.json()\n            if (result.success) {\n                await loadDomain()\n                if (result.verification.verified) {\n                    setSuccess('Domain verified successfully!')\n                } else {\n                    setError(`Verification failed: ${result.verification.errors?.join(', ') || 'DNS records not found'}`)\n                }\n            } else {\n                setError(result.error || 'Verification failed')\n            }\n        } catch (error) {\n            setError('Failed to verify domain')\n            console.error(error)\n        } finally {\n            setIsVerifying(false)\n        }\n    }\n\n    const handleDeleteDomain = async () => {\n        if (!confirm(`Are you sure you want to delete ${domainName}? This action cannot be undone.`)) {\n            return\n        }\n\n        setIsDeleting(true)\n        setError(null)\n\n        try {\n            const response = await fetch(`/api/custom-domains/${encodeURIComponent(domainName)}`, {\n                method: 'DELETE',\n            })\n\n            const result = await response.json()\n            if (result.success) {\n                setSuccess('Domain deleted successfully')\n                setTimeout(() => {\n                    router.push(`/dashboard/projects/${projectId}/domains`)\n                }, 1000)\n            } else {\n                setError(result.error || 'Failed to delete domain')\n            }\n        } catch (error) {\n            setError('Failed to delete domain')\n            console.error(error)\n        } finally {\n            setIsDeleting(false)\n        }\n    }\n\n    const copyToClipboard = async (text: string) => {\n        try {\n            await navigator.clipboard.writeText(text)\n            setSuccess('Copied to clipboard!')\n        } catch {\n            setError('Failed to copy to clipboard')\n        }\n    }\n\n    if (isLoading) {\n        return (\n            <div className=\"flex items-center justify-center min-h-[50vh]\">\n                <div className=\"text-center\">\n                    <RefreshCw className=\"w-8 h-8 animate-spin mx-auto mb-4 text-muted-foreground\" />\n                    <p className=\"text-muted-foreground\">Loading domain settings...</p>\n                </div>\n            </div>\n        )\n    }\n\n    if (!domain) {\n        return (\n            <div className=\"flex items-center justify-center min-h-[50vh]\">\n                <div className=\"text-center\">\n                    <AlertTriangle className=\"w-12 h-12 mx-auto mb-4 text-muted-foreground\" />\n                    <h2 className=\"text-xl font-semibold mb-2\">Domain Not Found</h2>\n                    <p className=\"text-muted-foreground mb-4\">The requested domain could not be found.</p>\n                    <Button onClick={() => router.push(`/dashboard/projects/${projectId}/domains`)}>\n                        <ArrowLeft className=\"w-4 h-4 mr-2\" />\n                        Back to Domains\n                    </Button>\n                </div>\n            </div>\n        )\n    }\n\n    return (\n        <div className=\"space-y-6\">\n            {/* Header */}\n            <div className=\"flex items-center justify-between\">\n                <div className=\"space-y-1\">\n                    <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        onClick={() => router.push(`/dashboard/projects/${projectId}/domains`)}\n                        className=\"mb-2\"\n                    >\n                        <ArrowLeft className=\"w-4 h-4 mr-2\" />\n                        Back to Domains\n                    </Button>\n                    <div className=\"flex items-center gap-3\">\n                        <div className=\"w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center\">\n                            <Globe className=\"w-5 h-5 text-white\" />\n                        </div>\n                        <div>\n                            <h1 className=\"text-2xl font-bold tracking-tight\">{domain.domain}</h1>\n                            <p className=\"text-sm text-muted-foreground\">Domain Settings</p>\n                        </div>\n                    </div>\n                </div>\n                <div className=\"flex items-center gap-2\">\n                    {domain.verified && (\n                        <Button\n                            variant=\"outline\"\n                            size=\"sm\"\n                            onClick={() => window.open(`https://${domain.domain}`, '_blank')}\n                        >\n                            <ExternalLink className=\"w-4 h-4 mr-2\" />\n                            Visit Site\n                        </Button>\n                    )}\n                </div>\n            </div>\n\n            {/* Alerts */}\n            <AnimatePresence>\n                {error && (\n                    <motion.div\n                        initial={{ opacity: 0, y: -10 }}\n                        animate={{ opacity: 1, y: 0 }}\n                        exit={{ opacity: 0, y: -10 }}\n                    >\n                        <Alert variant=\"destructive\">\n                            <AlertDescription>{error}</AlertDescription>\n                        </Alert>\n                    </motion.div>\n                )}\n                {success && (\n                    <motion.div\n                        initial={{ opacity: 0, y: -10 }}\n                        animate={{ opacity: 1, y: 0 }}\n                        exit={{ opacity: 0, y: -10 }}\n                    >\n                        <Alert variant=\"success\">\n                            <AlertDescription>{success}</AlertDescription>\n                        </Alert>\n                    </motion.div>\n                )}\n            </AnimatePresence>\n\n            <div className=\"grid gap-6 md:grid-cols-3\">\n                {/* Main Content */}\n                <div className=\"md:col-span-2 space-y-6\">\n                    {/* Domain Status Card */}\n                    <Card>\n                        <CardHeader>\n                            <CardTitle>Domain Status</CardTitle>\n                            <CardDescription>Current status and verification information</CardDescription>\n                        </CardHeader>\n                        <CardContent className=\"space-y-4\">\n                            <div className=\"flex items-center justify-between\">\n                                <div className=\"space-y-1\">\n                                    <Label>Verification Status</Label>\n                                    <p className=\"text-sm text-muted-foreground\">\n                                        {domain.verified ? 'Domain is verified and active' : 'Awaiting DNS verification'}\n                                    </p>\n                                </div>\n                                {domain.verified ? (\n                                    <Badge variant=\"default\" className=\"bg-green-600\">\n                                        <CheckCircle className=\"w-3 h-3 mr-1\" />\n                                        Verified\n                                    </Badge>\n                                ) : (\n                                    <Badge variant=\"secondary\">\n                                        <Clock className=\"w-3 h-3 mr-1\" />\n                                        Pending\n                                    </Badge>\n                                )}\n                            </div>\n\n                            {!domain.verified && (\n                                <>\n                                    <Separator />\n                                    <Button\n                                        onClick={handleVerifyDomain}\n                                        disabled={isVerifying}\n                                        className=\"w-full\"\n                                    >\n                                        {isVerifying ? (\n                                            <>\n                                                <RefreshCw className=\"w-4 h-4 mr-2 animate-spin\" />\n                                                Verifying...\n                                            </>\n                                        ) : (\n                                            <>\n                                                <CheckCircle className=\"w-4 h-4 mr-2\" />\n                                                Verify Domain\n                                            </>\n                                        )}\n                                    </Button>\n                                </>\n                            )}\n                        </CardContent>\n                    </Card>\n\n                    {/* DNS Configuration Card */}\n                    {dnsInstructions && !domain.verified && (\n                        <Card>\n                            <CardHeader>\n                                <CardTitle>DNS Configuration</CardTitle>\n                                <CardDescription>Add these DNS records to verify your domain</CardDescription>\n                            </CardHeader>\n                            <CardContent className=\"space-y-4\">\n                                {/* CNAME Record */}\n                                <div className=\"space-y-2\">\n                                    <div className=\"flex items-center justify-between\">\n                                        <Label className=\"text-sm font-semibold\">CNAME Record</Label>\n                                        <Badge variant=\"outline\">Required</Badge>\n                                    </div>\n                                    <div className=\"bg-muted rounded-lg p-3 space-y-2\">\n                                        <div className=\"grid grid-cols-3 gap-2 text-xs\">\n                                            <div>\n                                                <span className=\"text-muted-foreground\">Type:</span>\n                                                <p className=\"font-mono mt-1\">CNAME</p>\n                                            </div>\n                                            <div>\n                                                <span className=\"text-muted-foreground\">Name:</span>\n                                                <p className=\"font-mono mt-1\">{dnsInstructions.cname.name}</p>\n                                            </div>\n                                            <div>\n                                                <span className=\"text-muted-foreground\">Value:</span>\n                                                <p className=\"font-mono mt-1\">{dnsInstructions.cname.value}</p>\n                                            </div>\n                                        </div>\n                                        <Button\n                                            variant=\"ghost\"\n                                            size=\"sm\"\n                                            onClick={() =>\n                                                copyToClipboard(\n                                                    `${dnsInstructions.cname.name} CNAME ${dnsInstructions.cname.value}`\n                                                )\n                                            }\n                                            className=\"w-full\"\n                                        >\n                                            <Copy className=\"w-3 h-3 mr-2\" />\n                                            Copy Record\n                                        </Button>\n                                    </div>\n                                    <p className=\"text-xs text-muted-foreground\">{dnsInstructions.cname.description}</p>\n                                </div>\n\n                                <Separator />\n\n                                {/* TXT Record */}\n                                <div className=\"space-y-2\">\n                                    <div className=\"flex items-center justify-between\">\n                                        <Label className=\"text-sm font-semibold\">TXT Record</Label>\n                                        <Badge variant=\"outline\">Required</Badge>\n                                    </div>\n                                    <div className=\"bg-muted rounded-lg p-3 space-y-2\">\n                                        <div className=\"space-y-2 text-xs\">\n                                            <div>\n                                                <span className=\"text-muted-foreground\">Type:</span>\n                                                <p className=\"font-mono mt-1\">TXT</p>\n                                            </div>\n                                            <div>\n                                                <span className=\"text-muted-foreground\">Name:</span>\n                                                <p className=\"font-mono mt-1\">{dnsInstructions.txt.name}</p>\n                                            </div>\n                                            <div>\n                                                <span className=\"text-muted-foreground\">Value:</span>\n                                                <p className=\"font-mono mt-1 break-all\">{dnsInstructions.txt.value}</p>\n                                            </div>\n                                        </div>\n                                        <Button\n                                            variant=\"ghost\"\n                                            size=\"sm\"\n                                            onClick={() =>\n                                                copyToClipboard(`${dnsInstructions.txt.name} TXT ${dnsInstructions.txt.value}`)\n                                            }\n                                            className=\"w-full\"\n                                        >\n                                            <Copy className=\"w-3 h-3 mr-2\" />\n                                            Copy Record\n                                        </Button>\n                                    </div>\n                                    <p className=\"text-xs text-muted-foreground\">{dnsInstructions.txt.description}</p>\n                                </div>\n\n                                <Alert variant=\"info\">\n                                    <AlertDescription className=\"text-xs\">\n                                        DNS changes can take up to 48 hours to propagate, but usually complete within 5-10 minutes.\n                                    </AlertDescription>\n                                </Alert>\n                            </CardContent>\n                        </Card>\n                    )}\n\n                    {/* SSL Certificate Card */}\n                    {sslEnabled && domain.verified && (\n                        <Card>\n                            <CardHeader>\n                                <CardTitle>SSL Certificate</CardTitle>\n                                <CardDescription>Secure your domain with HTTPS</CardDescription>\n                            </CardHeader>\n                            <CardContent>\n                                <SSLManagement\n                                    domain={domain}\n                                    onUpdate={loadDomain}\n                                    onError={setError}\n                                    onSuccess={setSuccess}\n                                />\n                            </CardContent>\n                        </Card>\n                    )}\n\n                    {/* Danger Zone */}\n                    <Card className=\"border-destructive\">\n                        <CardHeader>\n                            <CardTitle className=\"text-destructive\">Danger Zone</CardTitle>\n                            <CardDescription>Irreversible actions</CardDescription>\n                        </CardHeader>\n                        <CardContent>\n                            <Button\n                                variant=\"destructive\"\n                                onClick={handleDeleteDomain}\n                                disabled={isDeleting}\n                                className=\"w-full\"\n                            >\n                                {isDeleting ? (\n                                    <>\n                                        <RefreshCw className=\"w-4 h-4 mr-2 animate-spin\" />\n                                        Deleting...\n                                    </>\n                                ) : (\n                                    <>\n                                        <Trash2 className=\"w-4 h-4 mr-2\" />\n                                        Delete Domain\n                                    </>\n                                )}\n                            </Button>\n                        </CardContent>\n                    </Card>\n                </div>\n\n                {/* Sidebar */}\n                <div className=\"space-y-6\">\n                    {/* Domain Info */}\n                    <Card>\n                        <CardHeader>\n                            <CardTitle className=\"text-base\">Domain Info</CardTitle>\n                        </CardHeader>\n                        <CardContent className=\"space-y-3 text-sm\">\n                            <div>\n                                <Label className=\"text-xs text-muted-foreground\">Added</Label>\n                                <p className=\"mt-1\">{new Date(domain.createdAt).toLocaleString()}</p>\n                            </div>\n                            {domain.verifiedAt && (\n                                <div>\n                                    <Label className=\"text-xs text-muted-foreground\">Verified</Label>\n                                    <p className=\"mt-1\">{new Date(domain.verifiedAt).toLocaleString()}</p>\n                                </div>\n                            )}\n                            <div>\n                                <Label className=\"text-xs text-muted-foreground\">SSL Mode</Label>\n                                <p className=\"mt-1\">\n                                    {domain.sslMode === 'LETS_ENCRYPT'\n                                        ? \"Let's Encrypt\"\n                                        : domain.sslMode === 'EXTERNAL'\n                                            ? 'Provider-Managed'\n                                            : 'None'}\n                                </p>\n                            </div>\n                        </CardContent>\n                    </Card>\n                </div>\n            </div>\n        </div>\n    )\n}\n"
  },
  {
    "path": "app/dashboard/projects/[projectId]/domains/[domain]/page.tsx",
    "content": "import { Metadata } from 'next'\nimport { DomainSettingsClient } from './client'\n\nexport const metadata: Metadata = {\n    title: 'Domain Settings',\n    description: 'Manage custom domain settings',\n}\n\ninterface DomainSettingsPageProps {\n    params: Promise<{\n        projectId: string\n        domain: string\n    }>\n}\n\nexport default async function DomainSettingsPage({ params }: DomainSettingsPageProps) {\n    const { projectId, domain } = await params\n\n    return <DomainSettingsClient projectId={projectId} domain={decodeURIComponent(domain)} />\n}\n"
  },
  {
    "path": "app/dashboard/projects/[projectId]/domains/components/DomainCard.tsx",
    "content": "'use client'\n\nimport { useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Alert, AlertDescription } from '@/components/ui/alert'\nimport {\n    DropdownMenu,\n    DropdownMenuContent,\n    DropdownMenuItem,\n    DropdownMenuSeparator,\n    DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\nimport {\n    Globe,\n    Shield,\n    CheckCircle,\n    Clock,\n    AlertTriangle,\n    MoreVertical,\n    Settings,\n    Trash2,\n    ExternalLink,\n    Lock,\n    Zap,\n} from 'lucide-react'\nimport type { CustomDomain } from '@/lib/types/custom-domains'\n\ninterface DomainCardProps {\n    domain: CustomDomain\n    projectId: string\n    sslEnabled: boolean\n    onUpdate: () => void\n    onDelete: (domain: string) => void\n    onError: (error: string) => void\n    onSuccess: (message: string) => void\n}\n\nexport function DomainCard({\n    domain,\n    projectId,\n    sslEnabled,\n    onUpdate,\n    onDelete,\n    onError,\n    onSuccess,\n}: DomainCardProps) {\n    const router = useRouter()\n\n    const activeCert = domain.certificates?.find(c => c.status === 'ISSUED')\n    const pendingCert = domain.certificates?.find(c =>\n        c.status === 'PENDING_HTTP01' || c.status === 'PENDING_DNS01'\n    )\n\n    const isExpiringSoon = activeCert?.expiresAt\n        ? new Date(activeCert.expiresAt).getTime() - Date.now() < 30 * 24 * 60 * 60 * 1000\n        : false\n\n    const getStatusBadge = () => {\n        if (!domain.verified) {\n            return (\n                <Badge variant=\"secondary\" className=\"flex items-center gap-1\">\n                    <Clock className=\"w-3 h-3\" />\n                    Pending Verification\n                </Badge>\n            )\n        }\n        return (\n            <Badge variant=\"default\" className=\"flex items-center gap-1 bg-green-600\">\n                <CheckCircle className=\"w-3 h-3\" />\n                Active\n            </Badge>\n        )\n    }\n\n    const getSSLBadge = () => {\n        if (pendingCert) {\n            return (\n                <Badge variant=\"secondary\" className=\"flex items-center gap-1\">\n                    <Clock className=\"w-3 h-3\" />\n                    SSL Issuing\n                </Badge>\n            )\n        }\n\n        if (activeCert) {\n            if (isExpiringSoon) {\n                return (\n                    <Badge variant=\"default\" className=\"flex items-center gap-1 bg-orange-600\">\n                        <AlertTriangle className=\"w-3 h-3\" />\n                        Expires Soon\n                    </Badge>\n                )\n            }\n            return (\n                <Badge variant=\"default\" className=\"flex items-center gap-1 bg-emerald-600\">\n                    <Shield className=\"w-3 h-3\" />\n                    SSL Active\n                </Badge>\n            )\n        }\n\n        return null\n    }\n\n    return (\n        <>\n            <Card className=\"hover:shadow-md transition-shadow\">\n                <CardContent className=\"p-6\">\n                    <div className=\"flex items-start justify-between\">\n                        <div className=\"flex-1 min-w-0\">\n                            {/* Domain Name */}\n                            <div className=\"flex items-center gap-3 mb-3\">\n                                <div className=\"w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center flex-shrink-0\">\n                                    <Globe className=\"w-5 h-5 text-white\" />\n                                </div>\n                                <div className=\"flex-1 min-w-0\">\n                                    <h3 className=\"text-lg font-semibold truncate\">{domain.domain}</h3>\n                                    <p className=\"text-sm text-muted-foreground\">\n                                        {domain.verified\n                                            ? `Added ${new Date(domain.createdAt).toLocaleDateString()}`\n                                            : 'Awaiting verification'}\n                                    </p>\n                                </div>\n                            </div>\n\n                            {/* Status Badges */}\n                            <div className=\"flex flex-wrap gap-2 mb-4\">\n                                {getStatusBadge()}\n                                {sslEnabled && getSSLBadge()}\n                                {domain.forceHttps && (\n                                    <Badge variant=\"outline\" className=\"flex items-center gap-1\">\n                                        <Lock className=\"w-3 h-3\" />\n                                        Force HTTPS\n                                    </Badge>\n                                )}\n                            </div>\n\n                            {/* Quick Actions */}\n                            <div className=\"flex flex-wrap gap-2\">\n                                {domain.verified && (\n                                    <Button\n                                        variant=\"outline\"\n                                        size=\"sm\"\n                                        onClick={() => window.open(`https://${domain.domain}`, '_blank')}\n                                    >\n                                        <ExternalLink className=\"w-4 h-4 mr-2\" />\n                                        Visit Site\n                                    </Button>\n                                )}\n\n                                {sslEnabled &&\n                                    domain.verified &&\n                                    !activeCert &&\n                                    !pendingCert && (\n                                        <Button\n                                            variant=\"default\"\n                                            size=\"sm\"\n                                            onClick={() =>\n                                                router.push(`/dashboard/projects/${projectId}/domains/${domain.domain}`)\n                                            }\n                                        >\n                                            <Zap className=\"w-4 h-4 mr-2\" />\n                                            Enable SSL\n                                        </Button>\n                                    )}\n\n                                <Button\n                                    variant=\"outline\"\n                                    size=\"sm\"\n                                    onClick={() =>\n                                        router.push(`/dashboard/projects/${projectId}/domains/${domain.domain}`)\n                                    }\n                                >\n                                    <Settings className=\"w-4 h-4 mr-2\" />\n                                    Settings\n                                </Button>\n                            </div>\n                        </div>\n\n                        {/* More Actions Menu */}\n                        <DropdownMenu>\n                            <DropdownMenuTrigger asChild>\n                                <Button variant=\"ghost\" size=\"sm\" className=\"ml-2\">\n                                    <MoreVertical className=\"w-4 h-4\" />\n                                </Button>\n                            </DropdownMenuTrigger>\n                            <DropdownMenuContent align=\"end\">\n                                <DropdownMenuItem\n                                    onClick={() =>\n                                        router.push(`/dashboard/projects/${projectId}/domains/${domain.domain}`)\n                                    }\n                                >\n                                    <Settings className=\"w-4 h-4 mr-2\" />\n                                    Settings\n                                </DropdownMenuItem>\n                                <DropdownMenuSeparator />\n                                <DropdownMenuItem\n                                    onClick={() => onDelete(domain.domain)}\n                                    className=\"text-destructive focus:text-destructive\"\n                                >\n                                    <Trash2 className=\"w-4 h-4 mr-2\" />\n                                    Delete Domain\n                                </DropdownMenuItem>\n                            </DropdownMenuContent>\n                        </DropdownMenu>\n                    </div>\n\n                    {/* Expiration Warning */}\n                    {isExpiringSoon && activeCert && (\n                        <Alert variant=\"warning\" className=\"mt-4\">\n                            <AlertDescription>\n                                <p className=\"font-medium\">Certificate Expiring Soon</p>\n                                <p className=\"text-xs mt-1\">\n                                    Expires on {activeCert.expiresAt && new Date(activeCert.expiresAt).toLocaleDateString()}.\n                                    {' '}Renewal will happen automatically.\n                                </p>\n                            </AlertDescription>\n                        </Alert>\n                    )}\n                </CardContent>\n            </Card>\n        </>\n    )\n}\n"
  },
  {
    "path": "app/dashboard/projects/[projectId]/domains/components/InlineSSLSetup.tsx",
    "content": "'use client'\n\nimport { useState, useEffect } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { Badge } from '@/components/ui/badge'\nimport { Alert, AlertDescription } from '@/components/ui/alert'\nimport { Label } from '@/components/ui/label'\nimport { Separator } from '@/components/ui/separator'\nimport {\n    Shield,\n    Zap,\n    Globe,\n    Check,\n    Copy,\n    RefreshCw,\n    ArrowRight,\n    ArrowLeft,\n} from 'lucide-react'\nimport { motion, AnimatePresence } from 'framer-motion'\nimport type { CustomDomain } from '@/lib/types/custom-domains'\n\ninterface InlineSSLSetupProps {\n    domain: CustomDomain\n    onUpdate: () => void\n    onError: (error: string) => void\n    onSuccess: (message: string) => void\n}\n\ntype SSLStep = 'choose' | 'http01-progress' | 'dns01-instructions' | 'dns01-progress'\n\nexport function InlineSSLSetup({\n    domain,\n    onUpdate,\n    onError,\n    onSuccess,\n}: InlineSSLSetupProps) {\n    const [step, setStep] = useState<SSLStep>('choose')\n    const [isProcessing, setIsProcessing] = useState(false)\n    const [dnsChallenge, setDnsChallenge] = useState<{ txtName: string; txtValue: string } | null>(null)\n    const [certId, setCertId] = useState<string | null>(null)\n    const [pollingInterval, setPollingInterval] = useState<NodeJS.Timeout | null>(null)\n\n    useEffect(() => {\n        return () => {\n            if (pollingInterval) clearInterval(pollingInterval)\n        }\n    }, [pollingInterval])\n\n    const copyToClipboard = async (text: string) => {\n        try {\n            await navigator.clipboard.writeText(text)\n            onSuccess('Copied to clipboard!')\n        } catch {\n            onError('Failed to copy to clipboard')\n        }\n    }\n\n    const startCertificateIssuance = async (method: 'HTTP01' | 'DNS01') => {\n        setIsProcessing(true)\n\n        try {\n            const response = await fetch('/api/acme/issue', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({\n                    domainId: domain.id,\n                    challengeType: method,\n                }),\n            })\n\n            const result = await response.json()\n            if (response.ok) {\n                setCertId(result.certId)\n\n                if (method === 'HTTP01') {\n                    setStep('http01-progress')\n                    startPolling(result.certId)\n                } else {\n                    setDnsChallenge({ txtName: result.txtName, txtValue: result.txtValue })\n                    setStep('dns01-instructions')\n                }\n            } else {\n                onError(result.error || 'Failed to start certificate issuance')\n                setStep('choose')\n            }\n        } catch (error) {\n            onError('Failed to start certificate issuance')\n            setStep('choose')\n        } finally {\n            setIsProcessing(false)\n        }\n    }\n\n    const startPolling = (id: string) => {\n        const interval = setInterval(async () => {\n            try {\n                const response = await fetch(`/api/acme/status/${id}`)\n                const result = await response.json()\n\n                if (result.status === 'ISSUED') {\n                    clearInterval(interval)\n                    setPollingInterval(null)\n                    await onUpdate()\n                    onSuccess('SSL certificate issued successfully!')\n                    resetSetup()\n                } else if (result.status === 'FAILED') {\n                    clearInterval(interval)\n                    setPollingInterval(null)\n                    onError(result.lastError || 'Certificate issuance failed')\n                    resetSetup()\n                }\n            } catch (error) {\n                console.error('Polling error:', error)\n            }\n        }, 3000)\n\n        setPollingInterval(interval)\n    }\n\n    const verifyDnsChallenge = async () => {\n        if (!certId) return\n\n        setIsProcessing(true)\n        try {\n            const response = await fetch('/api/acme/verify-dns', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({ certId }),\n            })\n\n            const result = await response.json()\n            if (response.ok) {\n                setStep('dns01-progress')\n                startPolling(certId)\n            } else {\n                onError(result.error || 'DNS verification failed. Please ensure the TXT record is added and propagated.')\n            }\n        } catch (error) {\n            onError('Failed to verify DNS challenge')\n        } finally {\n            setIsProcessing(false)\n        }\n    }\n\n    const resetSetup = () => {\n        setStep('choose')\n        setDnsChallenge(null)\n        setCertId(null)\n        if (pollingInterval) {\n            clearInterval(pollingInterval)\n            setPollingInterval(null)\n        }\n    }\n\n    const cancelSetup = async () => {\n        if (certId && pollingInterval) {\n            try {\n                await fetch(`/api/acme/cancel/${certId}`, { method: 'POST' })\n            } catch (error) {\n                console.error('Failed to cancel:', error)\n            }\n        }\n        resetSetup()\n    }\n\n    return (\n        <div className=\"space-y-4\">\n            <AnimatePresence mode=\"wait\">\n                <motion.div\n                    key={step}\n                    initial={{ opacity: 0, y: 10 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    exit={{ opacity: 0, y: -10 }}\n                    transition={{ duration: 0.2 }}\n                >\n                    {step === 'choose' && (\n                        <div className=\"space-y-3\">\n                            <p className=\"text-sm text-muted-foreground mb-4\">\n                                Choose how to verify domain ownership for the SSL certificate\n                            </p>\n\n                            {/* HTTP-01 Option */}\n                            <button\n                                onClick={() => startCertificateIssuance('HTTP01')}\n                                disabled={isProcessing}\n                                className=\"w-full text-left p-4 rounded-lg border-2 border-border hover:border-primary transition-all group\"\n                            >\n                                <div className=\"flex items-start gap-3\">\n                                    <div className=\"w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/20 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform\">\n                                        <Zap className=\"w-5 h-5 text-blue-600 dark:text-blue-400\" />\n                                    </div>\n                                    <div className=\"flex-1 min-w-0\">\n                                        <div className=\"flex items-center gap-2 mb-1\">\n                                            <p className=\"font-semibold\">Automatic (HTTP-01)</p>\n                                            <Badge variant=\"secondary\" className=\"text-xs\">Recommended</Badge>\n                                        </div>\n                                        <p className=\"text-sm text-muted-foreground mb-2\">\n                                            Fully automatic verification. No manual steps required.\n                                        </p>\n                                        <div className=\"flex gap-2\">\n                                            <Badge variant=\"outline\" className=\"text-xs\">\n                                                <Check className=\"w-3 h-3 mr-1\" />\n                                                Instant\n                                            </Badge>\n                                            <Badge variant=\"outline\" className=\"text-xs\">\n                                                <Check className=\"w-3 h-3 mr-1\" />\n                                                No DNS changes\n                                            </Badge>\n                                        </div>\n                                    </div>\n                                </div>\n                            </button>\n\n                            {/* DNS-01 Option */}\n                            <button\n                                onClick={() => startCertificateIssuance('DNS01')}\n                                disabled={isProcessing}\n                                className=\"w-full text-left p-4 rounded-lg border-2 border-border hover:border-primary transition-all group\"\n                            >\n                                <div className=\"flex items-start gap-3\">\n                                    <div className=\"w-10 h-10 rounded-lg bg-purple-100 dark:bg-purple-900/20 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform\">\n                                        <Globe className=\"w-5 h-5 text-purple-600 dark:text-purple-400\" />\n                                    </div>\n                                    <div className=\"flex-1 min-w-0\">\n                                        <div className=\"flex items-center gap-2 mb-1\">\n                                            <p className=\"font-semibold\">Manual (DNS-01)</p>\n                                            <Badge variant=\"outline\" className=\"text-xs\">Advanced</Badge>\n                                        </div>\n                                        <p className=\"text-sm text-muted-foreground mb-2\">\n                                            Verify by adding a DNS TXT record\n                                        </p>\n                                        <div className=\"flex gap-2\">\n                                            <Badge variant=\"outline\" className=\"text-xs\">\n                                                Requires DNS access\n                                            </Badge>\n                                            <Badge variant=\"outline\" className=\"text-xs\">\n                                                Manual setup\n                                            </Badge>\n                                        </div>\n                                    </div>\n                                </div>\n                            </button>\n                        </div>\n                    )}\n\n                    {step === 'http01-progress' && (\n                        <div className=\"space-y-4\">\n                            <Alert variant=\"info\" icon={<RefreshCw className=\"h-4 w-4 animate-spin\" />}>\n                                <AlertDescription>\n                                    <p className=\"font-medium\">Verifying Domain...</p>\n                                    <p className=\"text-sm mt-1\">\n                                        Automatically verifying your domain. This usually takes 10-30 seconds.\n                                    </p>\n                                </AlertDescription>\n                            </Alert>\n\n                            <Button\n                                variant=\"outline\"\n                                onClick={cancelSetup}\n                                className=\"w-full\"\n                            >\n                                Cancel\n                            </Button>\n                        </div>\n                    )}\n\n                    {step === 'dns01-instructions' && dnsChallenge && (\n                        <div className=\"space-y-4\">\n                            <div>\n                                <Label className=\"text-sm font-semibold mb-2 block\">Add DNS TXT Record</Label>\n                                <p className=\"text-sm text-muted-foreground mb-3\">\n                                    Add this TXT record to your DNS provider\n                                </p>\n                            </div>\n\n                            <div className=\"bg-muted rounded-lg p-4 space-y-3\">\n                                <div className=\"grid grid-cols-3 gap-3 text-xs\">\n                                    <div>\n                                        <span className=\"text-muted-foreground block mb-1\">Type</span>\n                                        <code className=\"font-mono\">TXT</code>\n                                    </div>\n                                    <div className=\"col-span-2\">\n                                        <span className=\"text-muted-foreground block mb-1\">Name</span>\n                                        <code className=\"font-mono break-all\">{dnsChallenge.txtName}</code>\n                                    </div>\n                                </div>\n                                <Separator />\n                                <div>\n                                    <span className=\"text-muted-foreground block mb-1 text-xs\">Value</span>\n                                    <code className=\"font-mono text-xs break-all block\">{dnsChallenge.txtValue}</code>\n                                </div>\n                                <Button\n                                    variant=\"ghost\"\n                                    size=\"sm\"\n                                    onClick={() => copyToClipboard(dnsChallenge.txtValue)}\n                                    className=\"w-full\"\n                                >\n                                    <Copy className=\"w-3 h-3 mr-2\" />\n                                    Copy TXT Value\n                                </Button>\n                            </div>\n\n                            <Alert variant=\"info\">\n                                <AlertDescription className=\"text-xs\">\n                                    DNS changes can take 5-60 minutes to propagate. Click \"Verify\" once you've added the TXT record.\n                                </AlertDescription>\n                            </Alert>\n\n                            <div className=\"flex gap-2\">\n                                <Button\n                                    variant=\"outline\"\n                                    onClick={cancelSetup}\n                                    className=\"flex-1\"\n                                >\n                                    <ArrowLeft className=\"w-4 h-4 mr-2\" />\n                                    Cancel\n                                </Button>\n                                <Button\n                                    onClick={verifyDnsChallenge}\n                                    disabled={isProcessing}\n                                    className=\"flex-1\"\n                                >\n                                    {isProcessing ? 'Verifying...' : 'Verify DNS Record'}\n                                    <ArrowRight className=\"w-4 h-4 ml-2\" />\n                                </Button>\n                            </div>\n                        </div>\n                    )}\n\n                    {step === 'dns01-progress' && (\n                        <div className=\"space-y-4\">\n                            <Alert variant=\"info\" icon={<RefreshCw className=\"h-4 w-4 animate-spin\" />}>\n                                <AlertDescription>\n                                    <p className=\"font-medium\">Verifying DNS Record...</p>\n                                    <p className=\"text-sm mt-1\">\n                                        Checking DNS records and issuing certificate. This may take a minute.\n                                    </p>\n                                </AlertDescription>\n                            </Alert>\n\n                            <Button\n                                variant=\"outline\"\n                                onClick={cancelSetup}\n                                className=\"w-full\"\n                            >\n                                Cancel\n                            </Button>\n                        </div>\n                    )}\n                </motion.div>\n            </AnimatePresence>\n        </div>\n    )\n}\n"
  },
  {
    "path": "app/dashboard/projects/[projectId]/domains/components/SSLCertificateCard.tsx",
    "content": "'use client'\n\nimport { useState } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Alert, AlertDescription } from '@/components/ui/alert'\nimport { Switch } from '@/components/ui/switch'\nimport { Label } from '@/components/ui/label'\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogHeader,\n    DialogTitle,\n    DialogTrigger,\n} from '@/components/ui/dialog'\nimport {\n    Lock,\n    Shield,\n    RefreshCw,\n    CheckCircle,\n    AlertTriangle,\n    Clock,\n    Copy,\n    Zap,\n    ExternalLink,\n    ShieldCheck,\n} from 'lucide-react'\nimport { motion, AnimatePresence } from 'framer-motion'\nimport type { CustomDomain, DomainCertificate } from '@/lib/types/custom-domains'\n\ninterface SSLCertificateCardProps {\n    domain: CustomDomain\n    onUpdate: () => void\n    onError: (error: string) => void\n    onSuccess: (message: string) => void\n}\n\nexport function SSLCertificateCard({ domain, onUpdate, onError, onSuccess }: SSLCertificateCardProps) {\n    const [isIssuing, setIsIssuing] = useState(false)\n    const [isTogglingHttps, setIsTogglingHttps] = useState(false)\n    const [showDnsChallenge, setShowDnsChallenge] = useState(false)\n    const [dnsChallenge, setDnsChallenge] = useState<{ txtName: string; txtValue: string } | null>(null)\n    const [selectedChallengeType, setSelectedChallengeType] = useState<'HTTP01' | 'DNS01'>('HTTP01')\n\n    const activeCert = domain.certificates?.find(c => c.status === 'ISSUED')\n    const pendingCert = domain.certificates?.find(c =>\n        c.status === 'PENDING_HTTP01' || c.status === 'PENDING_DNS01'\n    )\n\n    const isExpiringSoon = activeCert?.expiresAt\n        ? new Date(activeCert.expiresAt).getTime() - Date.now() < 30 * 24 * 60 * 60 * 1000\n        : false\n\n    const handleIssueCertificate = async () => {\n        if (!domain.verified) {\n            onError('Domain must be verified before issuing a certificate')\n            return\n        }\n\n        setIsIssuing(true)\n        try {\n            const response = await fetch('/api/acme/issue', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({\n                    domainId: domain.id,\n                    challengeType: selectedChallengeType,\n                }),\n            })\n\n            const result = await response.json()\n            if (response.ok) {\n                if (selectedChallengeType === 'DNS01' && result.txtName && result.txtValue) {\n                    setDnsChallenge({ txtName: result.txtName, txtValue: result.txtValue })\n                    setShowDnsChallenge(true)\n                    onSuccess('Certificate issuance initiated! Add the DNS TXT record shown below.')\n                } else {\n                    onSuccess('Certificate issuance initiated! This may take a few minutes.')\n                }\n                await onUpdate()\n            } else {\n                onError(result.error || 'Failed to issue certificate')\n            }\n        } catch (error) {\n            onError('Failed to issue certificate')\n            console.error(error)\n        } finally {\n            setIsIssuing(false)\n        }\n    }\n\n    const handleToggleForceHttps = async (enabled: boolean) => {\n        setIsTogglingHttps(true)\n        try {\n            const response = await fetch(`/api/custom-domains/${domain.domain}/ssl/toggle-https`, {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({ forceHttps: enabled }),\n            })\n\n            const result = await response.json()\n            if (response.ok) {\n                onSuccess(enabled ? 'Force HTTPS enabled' : 'Force HTTPS disabled')\n                await onUpdate()\n            } else {\n                onError(result.error || 'Failed to toggle HTTPS')\n            }\n        } catch (error) {\n            onError('Failed to toggle HTTPS')\n            console.error(error)\n        } finally {\n            setIsTogglingHttps(false)\n        }\n    }\n\n    const handleCompleteDnsChallenge = async (certId: string) => {\n        try {\n            const response = await fetch('/api/acme/verify-dns', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({ certId }),\n            })\n\n            const result = await response.json()\n            if (response.status === 202) {\n                onError('DNS TXT record not yet propagated. Please wait a few minutes and try again.')\n            } else if (response.ok) {\n                onSuccess('Certificate issued successfully!')\n                setShowDnsChallenge(false)\n                setDnsChallenge(null)\n                await onUpdate()\n            } else {\n                onError(result.error || 'Failed to verify DNS challenge')\n            }\n        } catch (error) {\n            onError('Failed to verify DNS challenge')\n            console.error(error)\n        }\n    }\n\n    const copyToClipboard = async (text: string) => {\n        try {\n            await navigator.clipboard.writeText(text)\n            onSuccess('Copied to clipboard!')\n        } catch {\n            onError('Failed to copy to clipboard')\n        }\n    }\n\n    const formatDate = (date: Date | string) => {\n        return new Date(date).toLocaleDateString('en-US', {\n            year: 'numeric',\n            month: 'short',\n            day: 'numeric',\n        })\n    }\n\n    const getSSLStatusBadge = () => {\n        if (domain.sslMode === 'NONE') {\n            return (\n                <Badge variant=\"secondary\" className=\"gap-1\">\n                    <Shield className=\"w-3 h-3\" />\n                    No SSL\n                </Badge>\n            )\n        }\n\n        if (activeCert) {\n            if (isExpiringSoon) {\n                return (\n                    <Badge variant=\"default\" className=\"gap-1 bg-yellow-500\">\n                        <AlertTriangle className=\"w-3 h-3\" />\n                        Expiring Soon\n                    </Badge>\n                )\n            }\n            return (\n                <Badge variant=\"default\" className=\"gap-1 bg-green-500\">\n                    <ShieldCheck className=\"w-3 h-3\" />\n                    SSL Active\n                </Badge>\n            )\n        }\n\n        if (pendingCert) {\n            return (\n                <Badge variant=\"secondary\" className=\"gap-1\">\n                    <Clock className=\"w-3 h-3\" />\n                    Pending\n                </Badge>\n            )\n        }\n\n        return (\n            <Badge variant=\"secondary\" className=\"gap-1\">\n                <Shield className=\"w-3 h-3\" />\n                {domain.sslMode}\n            </Badge>\n        )\n    }\n\n    return (\n        <>\n            <Card className=\"border-2\">\n                <CardHeader>\n                    <div className=\"flex items-center justify-between\">\n                        <div className=\"flex items-center gap-3\">\n                            <div className=\"w-10 h-10 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center\">\n                                <Lock className=\"w-5 h-5 text-green-600 dark:text-green-400\" />\n                            </div>\n                            <div>\n                                <CardTitle className=\"text-lg\">SSL Certificate</CardTitle>\n                                <p className=\"text-sm text-muted-foreground mt-1\">\n                                    Secure your domain with HTTPS\n                                </p>\n                            </div>\n                        </div>\n                        {getSSLStatusBadge()}\n                    </div>\n                </CardHeader>\n                <CardContent className=\"space-y-4\">\n                    {/* Force HTTPS Toggle */}\n                    {domain.sslMode === 'LETS_ENCRYPT' && activeCert && (\n                        <div className=\"flex items-center justify-between p-4 border rounded-lg\">\n                            <div className=\"space-y-0.5\">\n                                <Label htmlFor=\"force-https\" className=\"text-base font-medium\">\n                                    Force HTTPS\n                                </Label>\n                                <p className=\"text-sm text-muted-foreground\">\n                                    Automatically redirect HTTP to HTTPS\n                                </p>\n                            </div>\n                            <Switch\n                                id=\"force-https\"\n                                checked={domain.forceHttps}\n                                onCheckedChange={handleToggleForceHttps}\n                                disabled={isTogglingHttps}\n                            />\n                        </div>\n                    )}\n\n                    {/* Certificate Info */}\n                    {activeCert && (\n                        <motion.div\n                            initial={{ opacity: 0, y: 10 }}\n                            animate={{ opacity: 1, y: 0 }}\n                            className=\"p-4 border rounded-lg bg-muted/30 space-y-3\"\n                        >\n                            <div className=\"flex items-start justify-between\">\n                                <div className=\"space-y-2 flex-1\">\n                                    <div className=\"flex items-center gap-2\">\n                                        <CheckCircle className=\"w-4 h-4 text-green-600\" />\n                                        <span className=\"text-sm font-medium\">Certificate Active</span>\n                                    </div>\n                                    <div className=\"grid grid-cols-2 gap-4 text-sm\">\n                                        <div>\n                                            <span className=\"text-muted-foreground\">Issued:</span>\n                                            <p className=\"font-medium\">\n                                                {activeCert.issuedAt ? formatDate(activeCert.issuedAt) : 'Unknown'}\n                                            </p>\n                                        </div>\n                                        <div>\n                                            <span className=\"text-muted-foreground\">Expires:</span>\n                                            <p className=\"font-medium\">\n                                                {activeCert.expiresAt ? formatDate(activeCert.expiresAt) : 'Unknown'}\n                                            </p>\n                                        </div>\n                                        <div>\n                                            <span className=\"text-muted-foreground\">Type:</span>\n                                            <p className=\"font-medium\">{activeCert.challengeType}</p>\n                                        </div>\n                                        <div>\n                                            <span className=\"text-muted-foreground\">Provider:</span>\n                                            <p className=\"font-medium\">Let's Encrypt</p>\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n                            {isExpiringSoon && (\n                                <Alert variant=\"warning\">\n                                    <AlertDescription className=\"text-yellow-800 dark:text-yellow-200\">\n                                        This certificate expires soon. Renewal will happen automatically.\n                                    </AlertDescription>\n                                </Alert>\n                            )}\n                        </motion.div>\n                    )}\n\n                    {/* Pending Certificate */}\n                    {pendingCert && (\n                        <Alert className=\"border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950\" icon={<Clock className=\"h-4 w-4 text-blue-600\" />}>\n                            <AlertDescription className=\"text-blue-800 dark:text-blue-200\">\n                                Certificate issuance in progress ({pendingCert.challengeType}). This may take a few minutes.\n                                {pendingCert.status === 'PENDING_DNS01' && pendingCert.dnsTxtValue && (\n                                    <Button\n                                        variant=\"link\"\n                                        size=\"sm\"\n                                        className=\"p-0 h-auto ml-1\"\n                                        onClick={() => {\n                                            setDnsChallenge({\n                                                txtName: `_acme-challenge.${domain.domain}`,\n                                                txtValue: pendingCert.dnsTxtValue!,\n                                            })\n                                            setShowDnsChallenge(true)\n                                        }}\n                                    >\n                                        View DNS instructions\n                                    </Button>\n                                )}\n                            </AlertDescription>\n                        </Alert>\n                    )}\n\n                    {/* Issue Certificate Button */}\n                    {!activeCert && !pendingCert && domain.verified && (\n                        <Dialog>\n                            <DialogTrigger asChild>\n                                <Button className=\"w-full gap-2\">\n                                    <Zap className=\"w-4 h-4\" />\n                                    Issue SSL Certificate\n                                </Button>\n                            </DialogTrigger>\n                            <DialogContent>\n                                <DialogHeader>\n                                    <DialogTitle>Issue SSL Certificate</DialogTitle>\n                                    <DialogDescription>\n                                        Choose how you'd like to verify domain ownership\n                                    </DialogDescription>\n                                </DialogHeader>\n                                <div className=\"space-y-4\">\n                                    <div className=\"space-y-3\">\n                                        <button\n                                            onClick={() => setSelectedChallengeType('HTTP01')}\n                                            className={`w-full p-4 border-2 rounded-lg text-left transition-all ${\n                                                selectedChallengeType === 'HTTP01'\n                                                    ? 'border-primary bg-primary/5'\n                                                    : 'border-border hover:border-primary/50'\n                                            }`}\n                                        >\n                                            <div className=\"flex items-start gap-3\">\n                                                <div className=\"w-10 h-10 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center flex-shrink-0\">\n                                                    <Zap className=\"w-5 h-5 text-blue-600 dark:text-blue-400\" />\n                                                </div>\n                                                <div className=\"flex-1\">\n                                                    <h4 className=\"font-semibold mb-1\">HTTP-01 (Automatic)</h4>\n                                                    <p className=\"text-sm text-muted-foreground\">\n                                                        Verification happens automatically. Certificate will be issued in a few minutes.\n                                                        <strong className=\"block mt-1\">Recommended for most users</strong>\n                                                    </p>\n                                                </div>\n                                                {selectedChallengeType === 'HTTP01' && (\n                                                    <CheckCircle className=\"w-5 h-5 text-primary\" />\n                                                )}\n                                            </div>\n                                        </button>\n\n                                        <button\n                                            onClick={() => setSelectedChallengeType('DNS01')}\n                                            className={`w-full p-4 border-2 rounded-lg text-left transition-all ${\n                                                selectedChallengeType === 'DNS01'\n                                                    ? 'border-primary bg-primary/5'\n                                                    : 'border-border hover:border-primary/50'\n                                            }`}\n                                        >\n                                            <div className=\"flex items-start gap-3\">\n                                                <div className=\"w-10 h-10 bg-purple-100 dark:bg-purple-900 rounded-lg flex items-center justify-center flex-shrink-0\">\n                                                    <Shield className=\"w-5 h-5 text-purple-600 dark:text-purple-400\" />\n                                                </div>\n                                                <div className=\"flex-1\">\n                                                    <h4 className=\"font-semibold mb-1\">DNS-01 (Manual)</h4>\n                                                    <p className=\"text-sm text-muted-foreground\">\n                                                        You'll need to add a TXT record to your DNS. Use this if HTTP-01 doesn't work.\n                                                    </p>\n                                                </div>\n                                                {selectedChallengeType === 'DNS01' && (\n                                                    <CheckCircle className=\"w-5 h-5 text-primary\" />\n                                                )}\n                                            </div>\n                                        </button>\n                                    </div>\n\n                                    <Button\n                                        onClick={handleIssueCertificate}\n                                        disabled={isIssuing}\n                                        className=\"w-full gap-2\"\n                                    >\n                                        {isIssuing ? (\n                                            <>\n                                                <RefreshCw className=\"w-4 h-4 animate-spin\" />\n                                                Initiating...\n                                            </>\n                                        ) : (\n                                            <>\n                                                <Lock className=\"w-4 h-4\" />\n                                                Issue Certificate\n                                            </>\n                                        )}\n                                    </Button>\n                                </div>\n                            </DialogContent>\n                        </Dialog>\n                    )}\n\n                    {/* Info for unverified domains */}\n                    {!domain.verified && (\n                        <Alert>\n                            <AlertDescription>\n                                Domain must be verified before you can issue an SSL certificate.\n                            </AlertDescription>\n                        </Alert>\n                    )}\n\n                    {/* Learn More Link */}\n                    <Button variant=\"ghost\" size=\"sm\" asChild className=\"w-full gap-2\">\n                        <a\n                            href=\"https://letsencrypt.org/docs/\"\n                            target=\"_blank\"\n                            rel=\"noopener noreferrer\"\n                        >\n                            <ExternalLink className=\"w-4 h-4\" />\n                            Learn about SSL certificates\n                        </a>\n                    </Button>\n                </CardContent>\n            </Card>\n\n            {/* DNS Challenge Dialog */}\n            <Dialog open={showDnsChallenge} onOpenChange={setShowDnsChallenge}>\n                <DialogContent className=\"sm:max-w-2xl\">\n                    <DialogHeader>\n                        <DialogTitle>DNS-01 Challenge</DialogTitle>\n                        <DialogDescription>\n                            Add this TXT record to your DNS provider to verify domain ownership\n                        </DialogDescription>\n                    </DialogHeader>\n                    {dnsChallenge && (\n                        <div className=\"space-y-4\">\n                            <div className=\"p-4 border rounded-lg bg-muted/30 space-y-3\">\n                                <div>\n                                    <Label className=\"text-sm font-medium\">Record Type</Label>\n                                    <div className=\"mt-1 font-mono bg-background px-3 py-2 rounded border\">\n                                        TXT\n                                    </div>\n                                </div>\n                                <div>\n                                    <div className=\"flex items-center justify-between mb-1\">\n                                        <Label className=\"text-sm font-medium\">Name</Label>\n                                        <Button\n                                            variant=\"ghost\"\n                                            size=\"sm\"\n                                            onClick={() => copyToClipboard(dnsChallenge.txtName)}\n                                            className=\"h-6 px-2\"\n                                        >\n                                            <Copy className=\"w-3 h-3\" />\n                                        </Button>\n                                    </div>\n                                    <div className=\"font-mono bg-background px-3 py-2 rounded border break-all text-sm\">\n                                        {dnsChallenge.txtName}\n                                    </div>\n                                </div>\n                                <div>\n                                    <div className=\"flex items-center justify-between mb-1\">\n                                        <Label className=\"text-sm font-medium\">Value</Label>\n                                        <Button\n                                            variant=\"ghost\"\n                                            size=\"sm\"\n                                            onClick={() => copyToClipboard(dnsChallenge.txtValue)}\n                                            className=\"h-6 px-2\"\n                                        >\n                                            <Copy className=\"w-3 h-3\" />\n                                        </Button>\n                                    </div>\n                                    <div className=\"font-mono bg-background px-3 py-2 rounded border break-all text-sm\">\n                                        {dnsChallenge.txtValue}\n                                    </div>\n                                </div>\n                            </div>\n\n                            <Alert className=\"border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950\">\n                                <AlertDescription className=\"text-blue-800 dark:text-blue-200 text-sm\">\n                                    <strong>Important:</strong> DNS propagation can take up to 48 hours. After adding the record,\n                                    wait a few minutes before clicking \"Verify DNS Record\" below.\n                                </AlertDescription>\n                            </Alert>\n\n                            {pendingCert && (\n                                <Button\n                                    onClick={() => handleCompleteDnsChallenge(pendingCert.id)}\n                                    className=\"w-full gap-2\"\n                                >\n                                    <CheckCircle className=\"w-4 h-4\" />\n                                    Verify DNS Record & Issue Certificate\n                                </Button>\n                            )}\n                        </div>\n                    )}\n                </DialogContent>\n            </Dialog>\n        </>\n    )\n}\n"
  },
  {
    "path": "app/dashboard/projects/[projectId]/domains/components/SSLCertificateWizard.tsx",
    "content": "'use client'\n\nimport { useState, useEffect } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Alert, AlertDescription } from '@/components/ui/alert'\nimport { Label } from '@/components/ui/label'\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogHeader,\n    DialogTitle,\n} from '@/components/ui/dialog'\nimport {\n    Lock,\n    Shield,\n    CheckCircle,\n    AlertTriangle,\n    Copy,\n    Zap,\n    Globe,\n    Check,\n    ArrowRight,\n} from 'lucide-react'\nimport { motion, AnimatePresence } from 'framer-motion'\nimport type { CustomDomain } from '@/lib/types/custom-domains'\n\ninterface SSLCertificateWizardProps {\n    domain: CustomDomain\n    open: boolean\n    onOpenChange: (open: boolean) => void\n    onUpdate: () => void\n    onError: (error: string) => void\n    onSuccess: (message: string) => void\n    showCancelButton?: boolean  // Only show cancel on settings page\n}\n\ntype Step = 'select-method' | 'http01-progress' | 'dns01-instructions' | 'dns01-verify' | 'complete'\n\nexport function SSLCertificateWizard({\n    domain,\n    open,\n    onOpenChange,\n    onUpdate,\n    onError,\n    onSuccess,\n    showCancelButton = false,\n}: SSLCertificateWizardProps) {\n    const [step, setStep] = useState<Step>('select-method')\n    const [selectedMethod, setSelectedMethod] = useState<'HTTP01' | 'DNS01' | null>(null)\n    const [isProcessing, setIsProcessing] = useState(false)\n    const [dnsChallenge, setDnsChallenge] = useState<{ txtName: string; txtValue: string } | null>(null)\n    const [certId, setCertId] = useState<string | null>(null)\n    const [pollingInterval, setPollingInterval] = useState<NodeJS.Timeout | null>(null)\n\n    // Clean up polling on unmount\n    useEffect(() => {\n        return () => {\n            if (pollingInterval) clearInterval(pollingInterval)\n        }\n    }, [pollingInterval])\n\n    // Reset wizard when dialog closes\n    useEffect(() => {\n        if (!open) {\n            setStep('select-method')\n            setSelectedMethod(null)\n            setDnsChallenge(null)\n            setCertId(null)\n            if (pollingInterval) {\n                clearInterval(pollingInterval)\n                setPollingInterval(null)\n            }\n        }\n    }, [open])\n\n    const copyToClipboard = async (text: string) => {\n        try {\n            await navigator.clipboard.writeText(text)\n            onSuccess('Copied to clipboard!')\n        } catch {\n            onError('Failed to copy to clipboard')\n        }\n    }\n\n    const startCertificateIssuance = async (method: 'HTTP01' | 'DNS01') => {\n        setIsProcessing(true)\n        setSelectedMethod(method)\n\n        try {\n            const response = await fetch('/api/acme/issue', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({\n                    domainId: domain.id,\n                    challengeType: method,\n                }),\n            })\n\n            const result = await response.json()\n            if (response.ok) {\n                setCertId(result.certId)\n\n                if (method === 'HTTP01') {\n                    setStep('http01-progress')\n                    startPolling(result.certId)\n                } else {\n                    setDnsChallenge({ txtName: result.txtName, txtValue: result.txtValue })\n                    setStep('dns01-instructions')\n                }\n            } else {\n                onError(result.error || 'Failed to start certificate issuance')\n                onOpenChange(false)\n            }\n        } catch (error) {\n            onError('Failed to start certificate issuance')\n            onOpenChange(false)\n        } finally {\n            setIsProcessing(false)\n        }\n    }\n\n    const startPolling = (id: string) => {\n        const interval = setInterval(async () => {\n            try {\n                const response = await fetch(`/api/acme/status/${id}`)\n                const result = await response.json()\n\n                if (result.status === 'ISSUED') {\n                    clearInterval(interval)\n                    setPollingInterval(null)\n                    setStep('complete')\n                    await onUpdate()\n                    onSuccess('SSL certificate issued successfully!')\n                } else if (result.status === 'FAILED') {\n                    clearInterval(interval)\n                    setPollingInterval(null)\n                    onError(result.lastError || 'Certificate issuance failed')\n                    onOpenChange(false)\n                }\n            } catch (error) {\n                console.error('Polling error:', error)\n            }\n        }, 3000)\n\n        setPollingInterval(interval)\n    }\n\n    const verifyDnsChallenge = async () => {\n        if (!certId) return\n\n        setIsProcessing(true)\n        try {\n            const response = await fetch('/api/acme/verify-dns', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({ certId }),\n            })\n\n            const result = await response.json()\n            if (response.ok) {\n                setStep('dns01-verify')\n                startPolling(certId)\n            } else {\n                onError(result.error || 'DNS verification failed. Please ensure the TXT record is added and propagated.')\n            }\n        } catch (error) {\n            onError('Failed to verify DNS challenge')\n        } finally {\n            setIsProcessing(false)\n        }\n    }\n\n    const renderStepContent = () => {\n        switch (step) {\n            case 'select-method':\n                return (\n                    <div className=\"space-y-6\">\n                        <div className=\"space-y-2\">\n                            <h3 className=\"text-lg font-semibold\">Choose Verification Method</h3>\n                            <p className=\"text-sm text-muted-foreground\">\n                                Select how you want to verify domain ownership for the SSL certificate\n                            </p>\n                        </div>\n\n                        <div className=\"grid gap-4\">\n                            {/* HTTP-01 Option */}\n                            <Card\n                                className=\"cursor-pointer border-2 hover:border-primary transition-all\"\n                                onClick={() => !isProcessing && startCertificateIssuance('HTTP01')}\n                            >\n                                <CardContent className=\"p-6\">\n                                    <div className=\"flex items-start gap-4\">\n                                        <div className=\"w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0\">\n                                            <Zap className=\"w-6 h-6 text-blue-600\" />\n                                        </div>\n                                        <div className=\"flex-1\">\n                                            <div className=\"flex items-center gap-2 mb-2\">\n                                                <h4 className=\"font-semibold\">Automatic (HTTP-01)</h4>\n                                                <Badge variant=\"secondary\" className=\"text-xs\">Recommended</Badge>\n                                            </div>\n                                            <p className=\"text-sm text-muted-foreground mb-3\">\n                                                Fully automatic verification through HTTP. No manual steps required.\n                                            </p>\n                                            <div className=\"flex flex-wrap gap-2\">\n                                                <Badge variant=\"outline\" className=\"text-xs\">\n                                                    <Check className=\"w-3 h-3 mr-1\" />\n                                                    Instant setup\n                                                </Badge>\n                                                <Badge variant=\"outline\" className=\"text-xs\">\n                                                    <Check className=\"w-3 h-3 mr-1\" />\n                                                    No DNS changes\n                                                </Badge>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </CardContent>\n                            </Card>\n\n                            {/* DNS-01 Option */}\n                            <Card\n                                className=\"cursor-pointer border-2 hover:border-primary transition-all\"\n                                onClick={() => !isProcessing && startCertificateIssuance('DNS01')}\n                            >\n                                <CardContent className=\"p-6\">\n                                    <div className=\"flex items-start gap-4\">\n                                        <div className=\"w-12 h-12 rounded-full bg-purple-100 flex items-center justify-center flex-shrink-0\">\n                                            <Globe className=\"w-6 h-6 text-purple-600\" />\n                                        </div>\n                                        <div className=\"flex-1\">\n                                            <div className=\"flex items-center gap-2 mb-2\">\n                                                <h4 className=\"font-semibold\">Manual (DNS-01)</h4>\n                                                <Badge variant=\"outline\" className=\"text-xs\">Advanced</Badge>\n                                            </div>\n                                            <p className=\"text-sm text-muted-foreground mb-3\">\n                                                Verify ownership by adding a DNS TXT record. Required for some configurations.\n                                            </p>\n                                            <div className=\"flex flex-wrap gap-2\">\n                                                <Badge variant=\"outline\" className=\"text-xs\">\n                                                    Requires DNS access\n                                                </Badge>\n                                                <Badge variant=\"outline\" className=\"text-xs\">\n                                                    Manual setup\n                                                </Badge>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </CardContent>\n                            </Card>\n                        </div>\n                    </div>\n                )\n\n            case 'http01-progress':\n                return (\n                    <div className=\"space-y-6\">\n                        <div className=\"flex flex-col items-center justify-center py-8\">\n                            <motion.div\n                                animate={{ rotate: 360 }}\n                                transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}\n                                className=\"w-16 h-16 rounded-full bg-gradient-to-tr from-blue-500 to-purple-600 flex items-center justify-center mb-4\"\n                            >\n                                <Shield className=\"w-8 h-8 text-white\" />\n                            </motion.div>\n                            <h3 className=\"text-lg font-semibold mb-2\">Verifying Domain...</h3>\n                            <p className=\"text-sm text-muted-foreground text-center max-w-md\">\n                                We're automatically verifying your domain ownership. This usually takes 10-30 seconds.\n                            </p>\n                        </div>\n\n                        <Alert variant=\"warning\">\n                            <AlertDescription>\n                                Please keep this dialog open while we verify your domain.\n                            </AlertDescription>\n                        </Alert>\n\n                        {showCancelButton && (\n                            <Button\n                                variant=\"outline\"\n                                className=\"w-full\"\n                                onClick={() => {\n                                    if (pollingInterval) {\n                                        clearInterval(pollingInterval)\n                                        setPollingInterval(null)\n                                    }\n                                    onOpenChange(false)\n                                }}\n                            >\n                                Cancel\n                            </Button>\n                        )}\n                    </div>\n                )\n\n            case 'dns01-instructions':\n                return (\n                    <div className=\"space-y-6\">\n                        <div className=\"space-y-2\">\n                            <h3 className=\"text-lg font-semibold\">Add DNS TXT Record</h3>\n                            <p className=\"text-sm text-muted-foreground\">\n                                Add the following TXT record to your DNS provider to verify domain ownership\n                            </p>\n                        </div>\n\n                        {dnsChallenge && (\n                            <div className=\"space-y-4\">\n                                <div className=\"bg-muted rounded-lg p-4 space-y-3\">\n                                    <div>\n                                        <Label className=\"text-xs text-muted-foreground mb-1\">Record Type</Label>\n                                        <div className=\"flex items-center justify-between\">\n                                            <code className=\"text-sm font-mono\">TXT</code>\n                                        </div>\n                                    </div>\n                                    <div>\n                                        <Label className=\"text-xs text-muted-foreground mb-1\">Name</Label>\n                                        <div className=\"flex items-center justify-between gap-2\">\n                                            <code className=\"text-sm font-mono break-all\">{dnsChallenge.txtName}</code>\n                                            <Button\n                                                variant=\"ghost\"\n                                                size=\"sm\"\n                                                onClick={() => copyToClipboard(dnsChallenge.txtName)}\n                                            >\n                                                <Copy className=\"w-4 h-4\" />\n                                            </Button>\n                                        </div>\n                                    </div>\n                                    <div>\n                                        <Label className=\"text-xs text-muted-foreground mb-1\">Value</Label>\n                                        <div className=\"flex items-center justify-between gap-2\">\n                                            <code className=\"text-sm font-mono break-all\">{dnsChallenge.txtValue}</code>\n                                            <Button\n                                                variant=\"ghost\"\n                                                size=\"sm\"\n                                                onClick={() => copyToClipboard(dnsChallenge.txtValue)}\n                                            >\n                                                <Copy className=\"w-4 h-4\" />\n                                            </Button>\n                                        </div>\n                                    </div>\n                                </div>\n\n                                <Alert variant=\"info\">\n                                    <AlertDescription>\n                                        DNS changes can take 5-60 minutes to propagate. Click \"Verify DNS Record\" once you've added the TXT record.\n                                    </AlertDescription>\n                                </Alert>\n\n                                <Button\n                                    className=\"w-full\"\n                                    onClick={verifyDnsChallenge}\n                                    disabled={isProcessing}\n                                >\n                                    {isProcessing ? 'Verifying...' : 'Verify DNS Record'}\n                                    <ArrowRight className=\"w-4 h-4 ml-2\" />\n                                </Button>\n                            </div>\n                        )}\n                    </div>\n                )\n\n            case 'dns01-verify':\n                return (\n                    <div className=\"space-y-6\">\n                        <div className=\"flex flex-col items-center justify-center py-8\">\n                            <motion.div\n                                animate={{ rotate: 360 }}\n                                transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}\n                                className=\"w-16 h-16 rounded-full bg-gradient-to-tr from-purple-500 to-pink-600 flex items-center justify-center mb-4\"\n                            >\n                                <Globe className=\"w-8 h-8 text-white\" />\n                            </motion.div>\n                            <h3 className=\"text-lg font-semibold mb-2\">Verifying DNS Record...</h3>\n                            <p className=\"text-sm text-muted-foreground text-center max-w-md\">\n                                We're checking your DNS records and issuing the certificate. This may take a minute.\n                            </p>\n                        </div>\n\n                        <Alert variant=\"warning\">\n                            <AlertDescription>\n                                Please keep this dialog open while we verify your DNS record.\n                            </AlertDescription>\n                        </Alert>\n\n                        {showCancelButton && (\n                            <Button\n                                variant=\"outline\"\n                                className=\"w-full\"\n                                onClick={() => {\n                                    if (pollingInterval) {\n                                        clearInterval(pollingInterval)\n                                        setPollingInterval(null)\n                                    }\n                                    onOpenChange(false)\n                                }}\n                            >\n                                Cancel\n                            </Button>\n                        )}\n                    </div>\n                )\n\n            case 'complete':\n                return (\n                    <div className=\"space-y-6\">\n                        <div className=\"flex flex-col items-center justify-center py-8\">\n                            <motion.div\n                                initial={{ scale: 0 }}\n                                animate={{ scale: 1 }}\n                                transition={{ type: 'spring', stiffness: 200, damping: 15 }}\n                                className=\"w-16 h-16 rounded-full bg-gradient-to-tr from-green-500 to-emerald-600 flex items-center justify-center mb-4\"\n                            >\n                                <CheckCircle className=\"w-8 h-8 text-white\" />\n                            </motion.div>\n                            <h3 className=\"text-lg font-semibold mb-2\">Certificate Issued!</h3>\n                            <p className=\"text-sm text-muted-foreground text-center max-w-md\">\n                                Your SSL certificate has been successfully issued and is now active. Your site is secured with HTTPS.\n                            </p>\n                        </div>\n\n                        <Button className=\"w-full\" onClick={() => onOpenChange(false)}>\n                            Done\n                        </Button>\n                    </div>\n                )\n        }\n    }\n\n    return (\n        <Dialog open={open} onOpenChange={onOpenChange}>\n            <DialogContent className=\"sm:max-w-[600px]\">\n                <DialogHeader>\n                    <DialogTitle className=\"flex items-center gap-2\">\n                        <Lock className=\"w-5 h-5\" />\n                        Issue SSL Certificate\n                    </DialogTitle>\n                    <DialogDescription>\n                        Secure {domain.domain} with a free SSL certificate from Let's Encrypt\n                    </DialogDescription>\n                </DialogHeader>\n\n                <AnimatePresence mode=\"wait\">\n                    <motion.div\n                        key={step}\n                        initial={{ opacity: 0, x: 20 }}\n                        animate={{ opacity: 1, x: 0 }}\n                        exit={{ opacity: 0, x: -20 }}\n                        transition={{ duration: 0.2 }}\n                    >\n                        {renderStepContent()}\n                    </motion.div>\n                </AnimatePresence>\n            </DialogContent>\n        </Dialog>\n    )\n}\n"
  },
  {
    "path": "app/dashboard/projects/[projectId]/domains/components/SSLManagement.tsx",
    "content": "'use client'\n\nimport { useState, useEffect } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { Badge } from '@/components/ui/badge'\nimport { Alert, AlertDescription } from '@/components/ui/alert'\nimport { Label } from '@/components/ui/label'\nimport { Separator } from '@/components/ui/separator'\nimport { Switch } from '@/components/ui/switch'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'\nimport {\n    Shield,\n    Zap,\n    Globe,\n    Check,\n    Copy,\n    RefreshCw,\n    ArrowRight,\n    ArrowLeft,\n    Lock,\n    Upload,\n    FileText,\n    Calendar,\n    RotateCw,\n    ShieldOff,\n    Info,\n    CheckCircle2,\n    AlertTriangle,\n    X,\n} from 'lucide-react'\nimport { motion, AnimatePresence } from 'framer-motion'\nimport type { CustomDomain } from '@/lib/types/custom-domains'\n\ninterface SSLManagementProps {\n    domain: CustomDomain\n    onUpdate: () => void\n    onError: (error: string) => void\n    onSuccess: (message: string) => void\n}\n\ntype SSLMode = 'LETS_ENCRYPT' | 'EXTERNAL' | 'NONE'\ntype SetupStep = 'choose-mode' | 'choose-method' | 'http01-progress' | 'dns01-instructions' | 'dns01-progress' | 'external-upload'\n\nexport function SSLManagement({\n    domain,\n    onUpdate,\n    onError,\n    onSuccess,\n}: SSLManagementProps) {\n    const [step, setStep] = useState<SetupStep>('choose-mode')\n    const [selectedMode, setSelectedMode] = useState<SSLMode>(domain.sslMode as SSLMode || 'NONE')\n    const [isProcessing, setIsProcessing] = useState(false)\n    const [dnsChallenge, setDnsChallenge] = useState<{ txtName: string; txtValue: string } | null>(null)\n    const [certId, setCertId] = useState<string | null>(null)\n    const [pollingInterval, setPollingInterval] = useState<NodeJS.Timeout | null>(null)\n    const [isTogglingHttps, setIsTogglingHttps] = useState(false)\n    const [isRenewing, setIsRenewing] = useState(false)\n    const [isRevoking, setIsRevoking] = useState(false)\n\n    const activeCert = domain.certificates?.find(c => c.status === 'ISSUED')\n    const pendingCert = domain.certificates?.find(c => c.status === 'PENDING_HTTP01' || c.status === 'PENDING_DNS01')\n    const isExpiringSoon = activeCert?.expiresAt\n        ? new Date(activeCert.expiresAt).getTime() - Date.now() < 30 * 24 * 60 * 60 * 1000\n        : false\n\n    useEffect(() => {\n        return () => {\n            if (pollingInterval) clearInterval(pollingInterval)\n        }\n    }, [pollingInterval])\n\n    const copyToClipboard = async (text: string) => {\n        try {\n            await navigator.clipboard.writeText(text)\n            onSuccess('Copied to clipboard!')\n        } catch {\n            onError('Failed to copy to clipboard')\n        }\n    }\n\n    const startCertificateIssuance = async (method: 'HTTP01' | 'DNS01') => {\n        setIsProcessing(true)\n\n        try {\n            const response = await fetch('/api/acme/issue', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({\n                    domainId: domain.id,\n                    challengeType: method,\n                }),\n            })\n\n            const result = await response.json()\n            if (response.ok) {\n                setCertId(result.certId)\n\n                if (method === 'HTTP01') {\n                    setStep('http01-progress')\n                    startPolling(result.certId)\n                } else {\n                    setDnsChallenge({ txtName: result.txtName, txtValue: result.txtValue })\n                    setStep('dns01-instructions')\n                }\n            } else {\n                onError(result.error || 'Failed to start certificate issuance')\n                setStep('choose-method')\n            }\n        } catch (error) {\n            onError('Failed to start certificate issuance')\n            setStep('choose-method')\n        } finally {\n            setIsProcessing(false)\n        }\n    }\n\n    const startPolling = (id: string) => {\n        const interval = setInterval(async () => {\n            try {\n                const response = await fetch(`/api/acme/status/${id}`)\n                const result = await response.json()\n\n                if (result.status === 'ISSUED') {\n                    clearInterval(interval)\n                    setPollingInterval(null)\n                    await onUpdate()\n                    onSuccess('SSL certificate issued successfully!')\n                    resetSetup()\n                } else if (result.status === 'FAILED') {\n                    clearInterval(interval)\n                    setPollingInterval(null)\n                    onError(result.lastError || 'Certificate issuance failed')\n                    resetSetup()\n                }\n            } catch (error) {\n                console.error('Polling error:', error)\n            }\n        }, 3000)\n\n        setPollingInterval(interval)\n    }\n\n    const verifyDnsChallenge = async () => {\n        if (!certId) return\n\n        setIsProcessing(true)\n        try {\n            const response = await fetch('/api/acme/verify-dns', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({ certId }),\n            })\n\n            const result = await response.json()\n            if (response.ok) {\n                setStep('dns01-progress')\n                startPolling(certId)\n            } else {\n                onError(result.error || 'DNS verification failed. Please ensure the TXT record is added and propagated.')\n            }\n        } catch (error) {\n            onError('Failed to verify DNS challenge')\n        } finally {\n            setIsProcessing(false)\n        }\n    }\n\n    const handleToggleForceHttps = async (enabled: boolean) => {\n        setIsTogglingHttps(true)\n        try {\n            const response = await fetch(`/api/custom-domains/${domain.domain}/ssl/toggle-https`, {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({ forceHttps: enabled }),\n            })\n\n            const result = await response.json()\n            if (response.ok) {\n                onSuccess(enabled ? 'Force HTTPS enabled' : 'Force HTTPS disabled')\n                await onUpdate()\n            } else {\n                onError(result.error || 'Failed to toggle Force HTTPS')\n            }\n        } catch (error) {\n            onError('Failed to toggle Force HTTPS')\n        } finally {\n            setIsTogglingHttps(false)\n        }\n    }\n\n    const handleRenewCertificate = async () => {\n        if (!activeCert) return\n\n        setIsRenewing(true)\n        try {\n            const response = await fetch(`/api/acme/renew/${activeCert.id}`, {\n                method: 'POST',\n            })\n\n            const result = await response.json()\n            if (response.ok) {\n                onSuccess('Certificate renewal initiated')\n                await onUpdate()\n            } else {\n                onError(result.error || 'Failed to renew certificate')\n            }\n        } catch (error) {\n            onError('Failed to renew certificate')\n        } finally {\n            setIsRenewing(false)\n        }\n    }\n\n    const handleRevokeCertificate = async () => {\n        if (!activeCert) return\n\n        if (!confirm('Are you sure you want to revoke this certificate?')) {\n            return\n        }\n\n        setIsRevoking(true)\n        try {\n            const response = await fetch(`/api/acme/revoke/${activeCert.id}`, {\n                method: 'POST',\n            })\n\n            const result = await response.json()\n            if (response.ok) {\n                onSuccess('Certificate revoked successfully')\n                await onUpdate()\n            } else {\n                onError(result.error || 'Failed to revoke certificate')\n            }\n        } catch (error) {\n            onError('Failed to revoke certificate')\n        } finally {\n            setIsRevoking(false)\n        }\n    }\n\n    const resetSetup = () => {\n        setStep('choose-mode')\n        setDnsChallenge(null)\n        setCertId(null)\n        if (pollingInterval) {\n            clearInterval(pollingInterval)\n            setPollingInterval(null)\n        }\n    }\n\n    const cancelSetup = async () => {\n        if (certId && pollingInterval) {\n            try {\n                await fetch(`/api/acme/cancel/${certId}`, { method: 'POST' })\n            } catch (error) {\n                console.error('Failed to cancel:', error)\n            }\n        }\n        resetSetup()\n    }\n\n    // If domain has SSL configured, show management interface\n    if (domain.sslMode !== 'NONE' && (activeCert || pendingCert)) {\n        return (\n            <div className=\"space-y-6\">\n                {/* Certificate Status Header */}\n                <div className=\"flex items-start gap-4\">\n                    <div className=\"w-14 h-14 rounded-xl bg-gradient-to-br from-green-500 to-emerald-600 flex items-center justify-center flex-shrink-0 shadow-lg\">\n                        <Shield className=\"w-7 h-7 text-white\" />\n                    </div>\n                    <div className=\"flex-1 min-w-0\">\n                        <div className=\"flex items-center gap-2 mb-1\">\n                            <h3 className=\"text-lg font-semibold\">SSL Certificate</h3>\n                            {activeCert && (\n                                <Badge variant=\"default\" className=\"bg-green-600\">\n                                    <CheckCircle2 className=\"w-3 h-3 mr-1\" />\n                                    Active\n                                </Badge>\n                            )}\n                            {pendingCert && (\n                                <Badge variant=\"secondary\">\n                                    <RefreshCw className=\"w-3 h-3 mr-1 animate-spin\" />\n                                    Issuing\n                                </Badge>\n                            )}\n                            {isExpiringSoon && (\n                                <Badge variant=\"default\" className=\"bg-orange-600\">\n                                    <AlertTriangle className=\"w-3 h-3 mr-1\" />\n                                    Expiring Soon\n                                </Badge>\n                            )}\n                        </div>\n                        <p className=\"text-sm text-muted-foreground\">\n                            {domain.sslMode === 'LETS_ENCRYPT' ? \"Let's Encrypt (Automatic)\" : 'External Certificate'}\n                        </p>\n                    </div>\n                </div>\n\n                {/* Certificate Details */}\n                {activeCert && (\n                    <Card>\n                        <CardContent className=\"p-4\">\n                            <div className=\"grid grid-cols-2 gap-4 text-sm\">\n                                <div>\n                                    <Label className=\"text-xs text-muted-foreground\">Issued</Label>\n                                    <p className=\"font-medium mt-1\">\n                                        {new Date(activeCert.createdAt).toLocaleDateString()}\n                                    </p>\n                                </div>\n                                <div>\n                                    <Label className=\"text-xs text-muted-foreground\">Expires</Label>\n                                    <p className=\"font-medium mt-1\">\n                                        {activeCert.expiresAt && new Date(activeCert.expiresAt).toLocaleDateString()}\n                                    </p>\n                                </div>\n                                <div>\n                                    <Label className=\"text-xs text-muted-foreground\">Type</Label>\n                                    <p className=\"font-medium mt-1\">{activeCert.challengeType}</p>\n                                </div>\n                                <div>\n                                    <Label className=\"text-xs text-muted-foreground\">Auto-Renewal</Label>\n                                    <p className=\"font-medium mt-1\">\n                                        {activeCert.challengeType === 'HTTP01' ? (\n                                            <span className=\"text-green-600\">Enabled</span>\n                                        ) : (\n                                            <span className=\"text-orange-600\">Manual</span>\n                                        )}\n                                    </p>\n                                </div>\n                            </div>\n                        </CardContent>\n                    </Card>\n                )}\n\n                {/* Pending Certificate */}\n                {pendingCert && (\n                    <Alert variant=\"info\" icon={<RefreshCw className=\"h-4 w-4 animate-spin\" />}>\n                        <AlertDescription>\n                            <p className=\"font-medium\">Certificate Issuing</p>\n                            <p className=\"text-sm mt-1\">\n                                Your SSL certificate is being issued. This usually takes 30-60 seconds.\n                            </p>\n                        </AlertDescription>\n                    </Alert>\n                )}\n\n                <Separator />\n\n                {/* Force HTTPS Toggle */}\n                {activeCert && (\n                    <div className=\"flex items-center justify-between p-4 rounded-lg border bg-card\">\n                        <div className=\"flex items-start gap-3\">\n                            <div className=\"w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/20 flex items-center justify-center\">\n                                <Lock className=\"w-5 h-5 text-blue-600 dark:text-blue-400\" />\n                            </div>\n                            <div>\n                                <Label htmlFor=\"force-https\" className=\"font-medium cursor-pointer\">\n                                    Force HTTPS\n                                </Label>\n                                <p className=\"text-sm text-muted-foreground mt-0.5\">\n                                    Automatically redirect all HTTP traffic to HTTPS\n                                </p>\n                            </div>\n                        </div>\n                        <Switch\n                            id=\"force-https\"\n                            checked={domain.forceHttps}\n                            onCheckedChange={handleToggleForceHttps}\n                            disabled={isTogglingHttps}\n                        />\n                    </div>\n                )}\n\n                {/* Certificate Actions */}\n                {activeCert && (\n                    <div className=\"grid grid-cols-2 gap-3\">\n                        <Button\n                            variant=\"outline\"\n                            onClick={handleRenewCertificate}\n                            disabled={isRenewing}\n                        >\n                            {isRenewing ? (\n                                <>\n                                    <RefreshCw className=\"w-4 h-4 mr-2 animate-spin\" />\n                                    Renewing...\n                                </>\n                            ) : (\n                                <>\n                                    <RotateCw className=\"w-4 h-4 mr-2\" />\n                                    Renew Now\n                                </>\n                            )}\n                        </Button>\n                        <Button\n                            variant=\"outline\"\n                            onClick={handleRevokeCertificate}\n                            disabled={isRevoking}\n                            className=\"text-destructive hover:text-destructive\"\n                        >\n                            {isRevoking ? (\n                                <>\n                                    <RefreshCw className=\"w-4 h-4 mr-2 animate-spin\" />\n                                    Revoking...\n                                </>\n                            ) : (\n                                <>\n                                    <ShieldOff className=\"w-4 h-4 mr-2\" />\n                                    Revoke\n                                </>\n                            )}\n                        </Button>\n                    </div>\n                )}\n\n                {/* Cancel Pending */}\n                {pendingCert && (\n                    <Button\n                        variant=\"outline\"\n                        onClick={async () => {\n                            try {\n                                await fetch(`/api/acme/cancel/${pendingCert.id}`, { method: 'POST' })\n                                onSuccess('Certificate issuance cancelled')\n                                await onUpdate()\n                            } catch {\n                                onError('Failed to cancel')\n                            }\n                        }}\n                        className=\"w-full\"\n                    >\n                        Cancel Issuance\n                    </Button>\n                )}\n            </div>\n        )\n    }\n\n    // Setup flow for new SSL configuration\n    return (\n        <div className=\"space-y-4\">\n            <AnimatePresence mode=\"wait\">\n                <motion.div\n                    key={step}\n                    initial={{ opacity: 0, y: 10 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    exit={{ opacity: 0, y: -10 }}\n                    transition={{ duration: 0.2 }}\n                >\n                    {step === 'choose-mode' && (\n                        <div className=\"space-y-3\">\n                            <div className=\"text-center mb-6\">\n                                <div className=\"w-16 h-16 rounded-2xl bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center mx-auto mb-4 shadow-lg\">\n                                    <Shield className=\"w-8 h-8 text-white\" />\n                                </div>\n                                <h3 className=\"text-xl font-bold mb-2\">Enable SSL Certificate</h3>\n                                <p className=\"text-sm text-muted-foreground max-w-md mx-auto\">\n                                    Secure your domain with HTTPS. Choose how you want to manage your SSL certificate.\n                                </p>\n                            </div>\n\n                            {/* Let's Encrypt Option */}\n                            <button\n                                onClick={() => {\n                                    setSelectedMode('LETS_ENCRYPT')\n                                    setStep('choose-method')\n                                }}\n                                className=\"w-full text-left p-5 rounded-xl border-2 border-border hover:border-blue-500 hover:shadow-lg transition-all group bg-card\"\n                            >\n                                <div className=\"flex items-start gap-4\">\n                                    <div className=\"w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform shadow-md\">\n                                        <Zap className=\"w-6 h-6 text-white\" />\n                                    </div>\n                                    <div className=\"flex-1 min-w-0\">\n                                        <div className=\"flex items-center gap-2 mb-2\">\n                                            <p className=\"font-bold text-lg\">Let's Encrypt (Automatic)</p>\n                                            <Badge className=\"bg-blue-600\">Recommended</Badge>\n                                        </div>\n                                        <p className=\"text-sm text-muted-foreground mb-3\">\n                                            Free SSL certificates with automatic renewal. Perfect for most use cases.\n                                        </p>\n                                        <div className=\"flex flex-wrap gap-2\">\n                                            <Badge variant=\"outline\" className=\"text-xs\">\n                                                <Check className=\"w-3 h-3 mr-1\" />\n                                                Free Forever\n                                            </Badge>\n                                            <Badge variant=\"outline\" className=\"text-xs\">\n                                                <Check className=\"w-3 h-3 mr-1\" />\n                                                Auto-Renewal\n                                            </Badge>\n                                            <Badge variant=\"outline\" className=\"text-xs\">\n                                                <Check className=\"w-3 h-3 mr-1\" />\n                                                Trusted by Browsers\n                                            </Badge>\n                                        </div>\n                                    </div>\n                                    <ArrowRight className=\"w-5 h-5 text-muted-foreground group-hover:text-blue-600 group-hover:translate-x-1 transition-all\" />\n                                </div>\n                            </button>\n\n                            {/* External Certificate Option */}\n                            <button\n                                onClick={() => {\n                                    setSelectedMode('EXTERNAL')\n                                    setStep('external-upload')\n                                }}\n                                className=\"w-full text-left p-5 rounded-xl border-2 border-border hover:border-purple-500 hover:shadow-lg transition-all group bg-card\"\n                            >\n                                <div className=\"flex items-start gap-4\">\n                                    <div className=\"w-12 h-12 rounded-xl bg-gradient-to-br from-purple-500 to-purple-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform shadow-md\">\n                                        <Upload className=\"w-6 h-6 text-white\" />\n                                    </div>\n                                    <div className=\"flex-1 min-w-0\">\n                                        <div className=\"flex items-center gap-2 mb-2\">\n                                            <p className=\"font-bold text-lg\">External Certificate</p>\n                                            <Badge variant=\"outline\">Advanced</Badge>\n                                        </div>\n                                        <p className=\"text-sm text-muted-foreground mb-3\">\n                                            Upload your own SSL certificate from another provider or CA.\n                                        </p>\n                                        <div className=\"flex flex-wrap gap-2\">\n                                            <Badge variant=\"outline\" className=\"text-xs\">\n                                                Custom CA\n                                            </Badge>\n                                            <Badge variant=\"outline\" className=\"text-xs\">\n                                                Manual Management\n                                            </Badge>\n                                            <Badge variant=\"outline\" className=\"text-xs\">\n                                                EV Certificates\n                                            </Badge>\n                                        </div>\n                                    </div>\n                                    <ArrowRight className=\"w-5 h-5 text-muted-foreground group-hover:text-purple-600 group-hover:translate-x-1 transition-all\" />\n                                </div>\n                            </button>\n                        </div>\n                    )}\n\n                    {step === 'choose-method' && selectedMode === 'LETS_ENCRYPT' && (\n                        <div className=\"space-y-4\">\n                            <Button\n                                variant=\"ghost\"\n                                size=\"sm\"\n                                onClick={() => setStep('choose-mode')}\n                                className=\"mb-2\"\n                            >\n                                <ArrowLeft className=\"w-4 h-4 mr-2\" />\n                                Back\n                            </Button>\n\n                            <div className=\"text-center mb-6\">\n                                <h3 className=\"text-lg font-bold mb-2\">Choose Verification Method</h3>\n                                <p className=\"text-sm text-muted-foreground\">\n                                    How would you like to verify domain ownership?\n                                </p>\n                            </div>\n\n                            {/* HTTP-01 Option */}\n                            <button\n                                onClick={() => startCertificateIssuance('HTTP01')}\n                                disabled={isProcessing}\n                                className=\"w-full text-left p-4 rounded-xl border-2 border-border hover:border-blue-500 transition-all group bg-card disabled:opacity-50\"\n                            >\n                                <div className=\"flex items-start gap-3\">\n                                    <div className=\"w-11 h-11 rounded-lg bg-blue-100 dark:bg-blue-900/20 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform\">\n                                        <Zap className=\"w-5 h-5 text-blue-600 dark:text-blue-400\" />\n                                    </div>\n                                    <div className=\"flex-1 min-w-0\">\n                                        <div className=\"flex items-center gap-2 mb-1\">\n                                            <p className=\"font-semibold\">HTTP-01 (Automatic)</p>\n                                            <Badge variant=\"secondary\" className=\"text-xs\">Recommended</Badge>\n                                        </div>\n                                        <p className=\"text-sm text-muted-foreground mb-2\">\n                                            Fully automatic verification. Completes in seconds with no manual steps.\n                                        </p>\n                                        <div className=\"flex gap-2\">\n                                            <Badge variant=\"outline\" className=\"text-xs\">\n                                                <Check className=\"w-3 h-3 mr-1\" />\n                                                Instant\n                                            </Badge>\n                                            <Badge variant=\"outline\" className=\"text-xs\">\n                                                <Check className=\"w-3 h-3 mr-1\" />\n                                                Auto-Renewal\n                                            </Badge>\n                                        </div>\n                                    </div>\n                                </div>\n                            </button>\n\n                            {/* DNS-01 Option */}\n                            <button\n                                onClick={() => startCertificateIssuance('DNS01')}\n                                disabled={isProcessing}\n                                className=\"w-full text-left p-4 rounded-xl border-2 border-border hover:border-purple-500 transition-all group bg-card disabled:opacity-50\"\n                            >\n                                <div className=\"flex items-start gap-3\">\n                                    <div className=\"w-11 h-11 rounded-lg bg-purple-100 dark:bg-purple-900/20 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform\">\n                                        <Globe className=\"w-5 h-5 text-purple-600 dark:text-purple-400\" />\n                                    </div>\n                                    <div className=\"flex-1 min-w-0\">\n                                        <div className=\"flex items-center gap-2 mb-1\">\n                                            <p className=\"font-semibold\">DNS-01 (Manual)</p>\n                                            <Badge variant=\"outline\" className=\"text-xs\">Advanced</Badge>\n                                        </div>\n                                        <p className=\"text-sm text-muted-foreground mb-2\">\n                                            Verify via DNS TXT record. Required for wildcard certificates.\n                                        </p>\n                                        <div className=\"flex gap-2\">\n                                            <Badge variant=\"outline\" className=\"text-xs\">\n                                                DNS Access Required\n                                            </Badge>\n                                            <Badge variant=\"outline\" className=\"text-xs\">\n                                                Manual Renewal\n                                            </Badge>\n                                        </div>\n                                    </div>\n                                </div>\n                            </button>\n                        </div>\n                    )}\n\n                    {step === 'http01-progress' && (\n                        <div className=\"space-y-4\">\n                            <Alert variant=\"info\" icon={<RefreshCw className=\"h-4 w-4 animate-spin\" />}>\n                                <AlertDescription>\n                                    <p className=\"font-medium\">Verifying Domain...</p>\n                                    <p className=\"text-sm mt-1\">\n                                        Automatically verifying your domain ownership. This usually takes 10-30 seconds.\n                                    </p>\n                                </AlertDescription>\n                            </Alert>\n\n                            <div className=\"flex gap-2\">\n                                <Button variant=\"outline\" onClick={cancelSetup} className=\"flex-1\">\n                                    <X className=\"w-4 h-4 mr-2\" />\n                                    Cancel\n                                </Button>\n                            </div>\n                        </div>\n                    )}\n\n                    {step === 'dns01-instructions' && dnsChallenge && (\n                        <div className=\"space-y-4\">\n                            <Button\n                                variant=\"ghost\"\n                                size=\"sm\"\n                                onClick={cancelSetup}\n                                className=\"mb-2\"\n                            >\n                                <ArrowLeft className=\"w-4 h-4 mr-2\" />\n                                Back\n                            </Button>\n\n                            <div className=\"space-y-3\">\n                                <div className=\"flex items-start gap-3\">\n                                    <div className=\"w-10 h-10 rounded-lg bg-purple-100 dark:bg-purple-900/20 flex items-center justify-center flex-shrink-0\">\n                                        <FileText className=\"w-5 h-5 text-purple-600 dark:text-purple-400\" />\n                                    </div>\n                                    <div>\n                                        <h3 className=\"font-semibold mb-1\">Add DNS TXT Record</h3>\n                                        <p className=\"text-sm text-muted-foreground\">\n                                            Add this TXT record to your DNS provider to verify ownership\n                                        </p>\n                                    </div>\n                                </div>\n\n                                <Card>\n                                    <CardContent className=\"p-4 space-y-3\">\n                                        <div className=\"grid grid-cols-3 gap-3 text-xs\">\n                                            <div>\n                                                <span className=\"text-muted-foreground block mb-1\">Type</span>\n                                                <code className=\"font-mono font-medium\">TXT</code>\n                                            </div>\n                                            <div className=\"col-span-2\">\n                                                <span className=\"text-muted-foreground block mb-1\">Name</span>\n                                                <code className=\"font-mono text-xs break-all\">{dnsChallenge.txtName}</code>\n                                            </div>\n                                        </div>\n                                        <Separator />\n                                        <div>\n                                            <span className=\"text-muted-foreground block mb-1 text-xs\">Value</span>\n                                            <div className=\"bg-muted rounded p-2 font-mono text-xs break-all\">\n                                                {dnsChallenge.txtValue}\n                                            </div>\n                                        </div>\n                                        <Button\n                                            variant=\"ghost\"\n                                            size=\"sm\"\n                                            onClick={() => copyToClipboard(dnsChallenge.txtValue)}\n                                            className=\"w-full\"\n                                        >\n                                            <Copy className=\"w-3 h-3 mr-2\" />\n                                            Copy Value\n                                        </Button>\n                                    </CardContent>\n                                </Card>\n\n                                <Alert variant=\"info\">\n                                    <AlertDescription className=\"text-xs\">\n                                        DNS changes can take 5-60 minutes to propagate. Click \"Verify\" once you've added the record.\n                                    </AlertDescription>\n                                </Alert>\n                            </div>\n\n                            <Button\n                                onClick={verifyDnsChallenge}\n                                disabled={isProcessing}\n                                className=\"w-full\"\n                                size=\"lg\"\n                            >\n                                {isProcessing ? 'Verifying...' : 'Verify DNS Record'}\n                                <ArrowRight className=\"w-4 h-4 ml-2\" />\n                            </Button>\n                        </div>\n                    )}\n\n                    {step === 'dns01-progress' && (\n                        <div className=\"space-y-4\">\n                            <Alert variant=\"info\" icon={<RefreshCw className=\"h-4 w-4 animate-spin\" />}>\n                                <AlertDescription>\n                                    <p className=\"font-medium\">Verifying DNS Record...</p>\n                                    <p className=\"text-sm mt-1\">\n                                        Checking DNS records and issuing certificate. This may take a minute.\n                                    </p>\n                                </AlertDescription>\n                            </Alert>\n\n                            <Button variant=\"outline\" onClick={cancelSetup} className=\"w-full\">\n                                <X className=\"w-4 h-4 mr-2\" />\n                                Cancel\n                            </Button>\n                        </div>\n                    )}\n\n                    {step === 'external-upload' && (\n                        <div className=\"space-y-4\">\n                            <Button\n                                variant=\"ghost\"\n                                size=\"sm\"\n                                onClick={() => setStep('choose-mode')}\n                                className=\"mb-2\"\n                            >\n                                <ArrowLeft className=\"w-4 h-4 mr-2\" />\n                                Back\n                            </Button>\n\n                            <Alert variant=\"info\">\n                                <AlertDescription>\n                                    <p className=\"font-medium mb-2\">External Certificate Upload</p>\n                                    <p className=\"text-sm\">\n                                        This feature is coming soon! You'll be able to upload your own SSL certificates from external providers.\n                                    </p>\n                                </AlertDescription>\n                            </Alert>\n                        </div>\n                    )}\n                </motion.div>\n            </AnimatePresence>\n        </div>\n    )\n}\n"
  },
  {
    "path": "app/dashboard/projects/[projectId]/domains/components/ssl/ExternalSSLManagement.tsx",
    "content": "'use client'\n\nimport { useState, useEffect } from 'react'\nimport { useDebounce } from 'use-debounce'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Label } from '@/components/ui/label'\nimport { Input } from '@/components/ui/input'\nimport { Switch } from '@/components/ui/switch'\nimport { Separator } from '@/components/ui/separator'\nimport { Alert, AlertDescription } from '@/components/ui/alert'\nimport {\n    Shield,\n    Globe,\n    Plus,\n    Trash2,\n    CheckCircle2,\n    AlertCircle,\n    Lock,\n    Zap,\n    Info,\n} from 'lucide-react'\nimport {\n    Select,\n    SelectContent,\n    SelectItem,\n    SelectTrigger,\n    SelectValue,\n} from '@/components/ui/select'\n\ninterface BrowserRule {\n    id: string\n    userAgentPattern: string\n    ruleType: 'BLOCK' | 'ALLOW'\n    isEnabled: boolean\n}\n\ninterface ThrottleConfig {\n    enabled: boolean\n    requestsPerSecond: number\n    burstSize: number\n}\n\ninterface ExternalSSLManagementProps {\n    domain: string\n    browserRules?: BrowserRule[]\n    throttleConfig?: ThrottleConfig | null\n    onUpdate: () => void\n    onError: (error: string) => void\n    onSuccess: (message: string) => void\n    onChangeMode?: () => void\n}\n\nexport function ExternalSSLManagement({\n    domain,\n    browserRules = [],\n    throttleConfig,\n    onUpdate,\n    onError,\n    onSuccess,\n    onChangeMode,\n}: ExternalSSLManagementProps) {\n    const [showAddRule, setShowAddRule] = useState(false)\n    const [newRulePattern, setNewRulePattern] = useState('')\n    const [newRuleType, setNewRuleType] = useState<'BLOCK' | 'ALLOW'>('BLOCK')\n\n    // Local state for throttle inputs with debouncing\n    const [requestsPerSecond, setRequestsPerSecond] = useState(throttleConfig?.requestsPerSecond || 60)\n    const [burstSize, setBurstSize] = useState(throttleConfig?.burstSize || 20)\n\n    // Debounced values (wait 1 second after user stops typing)\n    const [debouncedRequestsPerSecond] = useDebounce(requestsPerSecond, 1000)\n    const [debouncedBurstSize] = useDebounce(burstSize, 1000)\n\n    // Sync local state when props change\n    useEffect(() => {\n        if (throttleConfig) {\n            setRequestsPerSecond(throttleConfig.requestsPerSecond)\n            setBurstSize(throttleConfig.burstSize)\n        }\n    }, [throttleConfig])\n\n    // Update API when debounced values change\n    useEffect(() => {\n        if (!throttleConfig?.enabled) return\n\n        const updateThrottle = async () => {\n            try {\n                const response = await fetch(`/api/custom-domains/${domain}/throttle`, {\n                    method: 'POST',\n                    headers: { 'Content-Type': 'application/json' },\n                    body: JSON.stringify({\n                        enabled: true,\n                        requestsPerSecond: debouncedRequestsPerSecond,\n                        burstSize: debouncedBurstSize,\n                    }),\n                })\n\n                if (response.ok) {\n                    // Silently update - no need to call onUpdate() which causes scroll\n                    // The debounced values are already shown in the UI\n                } else {\n                    onError('Failed to update rate limiting')\n                }\n            } catch (error) {\n                onError('Failed to update rate limiting')\n            }\n        }\n\n        // Only update if values actually changed from the server\n        if (debouncedRequestsPerSecond !== throttleConfig.requestsPerSecond ||\n            debouncedBurstSize !== throttleConfig.burstSize) {\n            updateThrottle()\n        }\n    }, [debouncedRequestsPerSecond, debouncedBurstSize])\n\n    return (\n        <div className=\"space-y-6\">\n            <Separator />\n\n            {/* Browser Rules */}\n            <Card>\n                <CardHeader>\n                    <div className=\"flex items-center justify-between\">\n                        <div className=\"flex items-center gap-3\">\n                            <div className=\"w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/20 flex items-center justify-center\">\n                                <Globe className=\"w-5 h-5 text-blue-600 dark:text-blue-400\" />\n                            </div>\n                            <div>\n                                <CardTitle className=\"text-base\">Browser Access Rules</CardTitle>\n                                <CardDescription>Control which browsers can access your domain</CardDescription>\n                            </div>\n                        </div>\n                        <Button\n                            size=\"sm\"\n                            onClick={() => setShowAddRule(!showAddRule)}\n                            variant={showAddRule ? 'outline' : 'default'}\n                        >\n                            <Plus className=\"w-4 h-4 mr-2\" />\n                            {showAddRule ? 'Cancel' : 'Add Rule'}\n                        </Button>\n                    </div>\n                </CardHeader>\n                <CardContent className=\"space-y-4\">\n                    {/* Add Rule Form */}\n                    {showAddRule && (\n                        <div className=\"p-4 rounded-lg border bg-muted/50 space-y-3\">\n                            <div className=\"space-y-2\">\n                                <Label>User-Agent Pattern (Regex)</Label>\n                                <Input\n                                    placeholder=\"e.g., .*Chrome.*|.*Firefox.*\"\n                                    value={newRulePattern}\n                                    onChange={(e) => setNewRulePattern(e.target.value)}\n                                />\n                                <p className=\"text-xs text-muted-foreground\">\n                                    Use regex patterns to match browser user agents\n                                </p>\n                            </div>\n\n                            <div className=\"space-y-2\">\n                                <Label>Rule Type</Label>\n                                <Select value={newRuleType} onValueChange={(v) => setNewRuleType(v as any)}>\n                                    <SelectTrigger>\n                                        <SelectValue />\n                                    </SelectTrigger>\n                                    <SelectContent>\n                                        <SelectItem value=\"BLOCK\">\n                                            <div className=\"flex items-center gap-2\">\n                                                <AlertCircle className=\"w-4 h-4 text-destructive\" />\n                                                Block Access\n                                            </div>\n                                        </SelectItem>\n                                        <SelectItem value=\"ALLOW\">\n                                            <div className=\"flex items-center gap-2\">\n                                                <CheckCircle2 className=\"w-4 h-4 text-green-600\" />\n                                                Allow Access\n                                            </div>\n                                        </SelectItem>\n                                    </SelectContent>\n                                </Select>\n                            </div>\n\n                            <Button\n                                size=\"sm\"\n                                onClick={async () => {\n                                    if (!newRulePattern.trim()) {\n                                        onError('Pattern cannot be empty')\n                                        return\n                                    }\n\n                                    try {\n                                        const response = await fetch(`/api/custom-domains/${domain}/browser-rules`, {\n                                            method: 'POST',\n                                            headers: { 'Content-Type': 'application/json' },\n                                            body: JSON.stringify({\n                                                userAgentPattern: newRulePattern,\n                                                ruleType: newRuleType,\n                                            }),\n                                        })\n\n                                        const result = await response.json()\n                                        if (response.ok) {\n                                            onSuccess('Browser rule added')\n                                            setNewRulePattern('')\n                                            setShowAddRule(false)\n                                            onUpdate()\n                                        } else {\n                                            onError(result.error || 'Failed to add rule')\n                                        }\n                                    } catch (error) {\n                                        onError('Failed to add browser rule')\n                                    }\n                                }}\n                                className=\"w-full\"\n                            >\n                                Add Rule\n                            </Button>\n                        </div>\n                    )}\n\n                    {/* Existing Rules */}\n                    {browserRules.length === 0 ? (\n                        <div className=\"text-center py-8 text-muted-foreground\">\n                            <Globe className=\"w-12 h-12 mx-auto mb-3 opacity-20\" />\n                            <p className=\"text-sm\">No browser rules configured</p>\n                        </div>\n                    ) : (\n                        <div className=\"space-y-2\">\n                            {browserRules.map((rule) => (\n                                <div key={rule.id} className=\"flex items-center gap-3 p-3 rounded-lg border bg-card\">\n                                    <div className=\"flex-1 min-w-0\">\n                                        <div className=\"flex items-center gap-2 mb-1\">\n                                            <code className=\"text-sm font-mono\">{rule.userAgentPattern}</code>\n                                            <Badge\n                                                variant={rule.ruleType === 'BLOCK' ? 'destructive' : 'default'}\n                                                className={rule.ruleType === 'ALLOW' ? 'bg-green-600' : ''}\n                                            >\n                                                {rule.ruleType}\n                                            </Badge>\n                                        </div>\n                                    </div>\n                                    <Switch\n                                        checked={rule.isEnabled}\n                                        onCheckedChange={async (enabled) => {\n                                            try {\n                                                const response = await fetch(\n                                                    `/api/custom-domains/${domain}/browser-rules/${rule.id}`,\n                                                    {\n                                                        method: 'PATCH',\n                                                        headers: { 'Content-Type': 'application/json' },\n                                                        body: JSON.stringify({ isEnabled: enabled }),\n                                                    }\n                                                )\n\n                                                if (response.ok) {\n                                                    onSuccess(enabled ? 'Rule enabled' : 'Rule disabled')\n                                                    onUpdate()\n                                                } else {\n                                                    onError('Failed to update rule')\n                                                }\n                                            } catch (error) {\n                                                onError('Failed to update rule')\n                                            }\n                                        }}\n                                    />\n                                    <Button\n                                        size=\"sm\"\n                                        variant=\"ghost\"\n                                        onClick={async () => {\n                                            if (!confirm('Delete this rule?')) return\n\n                                            try {\n                                                const response = await fetch(\n                                                    `/api/custom-domains/${domain}/browser-rules/${rule.id}`,\n                                                    { method: 'DELETE' }\n                                                )\n\n                                                if (response.ok) {\n                                                    onSuccess('Rule deleted')\n                                                    onUpdate()\n                                                } else {\n                                                    onError('Failed to delete rule')\n                                                }\n                                            } catch (error) {\n                                                onError('Failed to delete rule')\n                                            }\n                                        }}\n                                    >\n                                        <Trash2 className=\"w-4 h-4\" />\n                                    </Button>\n                                </div>\n                            ))}\n                        </div>\n                    )}\n                </CardContent>\n            </Card>\n\n            {/* Rate Limiting / Throttle */}\n            <Card>\n                <CardHeader>\n                    <div className=\"flex items-center gap-3\">\n                        <div className=\"w-10 h-10 rounded-lg bg-orange-100 dark:bg-orange-900/20 flex items-center justify-center\">\n                            <Zap className=\"w-5 h-5 text-orange-600 dark:text-orange-400\" />\n                        </div>\n                        <div>\n                            <CardTitle className=\"text-base\">Rate Limiting</CardTitle>\n                            <CardDescription>Control request rate limits for this domain</CardDescription>\n                        </div>\n                    </div>\n                </CardHeader>\n                <CardContent className=\"space-y-4\">\n                    <div className=\"flex items-center justify-between p-4 rounded-lg border bg-card\">\n                        <div className=\"flex items-start gap-3\">\n                            <div>\n                                <Label htmlFor=\"throttle-enabled\" className=\"font-medium cursor-pointer\">\n                                    Enable Rate Limiting\n                                </Label>\n                                <p className=\"text-sm text-muted-foreground mt-0.5\">\n                                    Protect your domain from excessive requests\n                                </p>\n                            </div>\n                        </div>\n                        <Switch\n                            id=\"throttle-enabled\"\n                            checked={throttleConfig?.enabled || false}\n                            onCheckedChange={async (enabled) => {\n                                try {\n                                    const response = await fetch(`/api/custom-domains/${domain}/throttle`, {\n                                        method: 'POST',\n                                        headers: { 'Content-Type': 'application/json' },\n                                        body: JSON.stringify({\n                                            enabled,\n                                            requestsPerSecond: throttleConfig?.requestsPerSecond || 60,\n                                            burstSize: throttleConfig?.burstSize || 20,\n                                        }),\n                                    })\n\n                                    if (response.ok) {\n                                        onSuccess(enabled ? 'Rate limiting enabled' : 'Rate limiting disabled')\n                                        onUpdate()\n                                    } else {\n                                        onError('Failed to update rate limiting')\n                                    }\n                                } catch (error) {\n                                    onError('Failed to update rate limiting')\n                                }\n                            }}\n                        />\n                    </div>\n\n                    {throttleConfig?.enabled && (\n                        <div className=\"grid grid-cols-2 gap-4 p-4 rounded-lg border bg-muted/50\">\n                            <div className=\"space-y-2\">\n                                <Label>Requests Per Second</Label>\n                                <Input\n                                    type=\"number\"\n                                    value={requestsPerSecond}\n                                    onChange={(e) => {\n                                        const value = parseInt(e.target.value)\n                                        if (value >= 1) {\n                                            setRequestsPerSecond(value)\n                                        }\n                                    }}\n                                    min={1}\n                                />\n                                <p className=\"text-xs text-muted-foreground\">\n                                    Changes save automatically after 1 second\n                                </p>\n                            </div>\n                            <div className=\"space-y-2\">\n                                <Label>Burst Size</Label>\n                                <Input\n                                    type=\"number\"\n                                    value={burstSize}\n                                    onChange={(e) => {\n                                        const value = parseInt(e.target.value)\n                                        if (value >= 1) {\n                                            setBurstSize(value)\n                                        }\n                                    }}\n                                    min={1}\n                                />\n                                <p className=\"text-xs text-muted-foreground\">\n                                    Changes save automatically after 1 second\n                                </p>\n                            </div>\n                            <div className=\"col-span-2\">\n                                <Alert>\n                                    <AlertDescription className=\"text-xs\">\n                                        Requests exceeding {requestsPerSecond}/sec will be rate limited.\n                                        Burst allows temporary spikes up to {burstSize} requests.\n                                    </AlertDescription>\n                                </Alert>\n                            </div>\n                        </div>\n                    )}\n                </CardContent>\n            </Card>\n        </div>\n    )\n}\n"
  },
  {
    "path": "app/dashboard/projects/[projectId]/domains/components/ssl/SSLCertificateActions.tsx",
    "content": "'use client'\n\nimport { useState } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { Switch } from '@/components/ui/switch'\nimport { Label } from '@/components/ui/label'\nimport { Separator } from '@/components/ui/separator'\nimport { Lock, RotateCw, ShieldOff, RefreshCw } from 'lucide-react'\n\ninterface SSLCertificateActionsProps {\n    domain: string\n    forceHttps: boolean\n    onToggleForceHttps: (enabled: boolean) => Promise<void>\n    onRenew: () => Promise<void>\n    onRevoke: () => Promise<void>\n    onError: (error: string) => void\n    onSuccess: (message: string) => void\n}\n\nexport function SSLCertificateActions({\n    domain,\n    forceHttps,\n    onToggleForceHttps,\n    onRenew,\n    onRevoke,\n    onError,\n    onSuccess,\n}: SSLCertificateActionsProps) {\n    const [isTogglingHttps, setIsTogglingHttps] = useState(false)\n    const [isRenewing, setIsRenewing] = useState(false)\n    const [isRevoking, setIsRevoking] = useState(false)\n\n    const handleToggleForceHttps = async (enabled: boolean) => {\n        setIsTogglingHttps(true)\n        try {\n            await onToggleForceHttps(enabled)\n        } finally {\n            setIsTogglingHttps(false)\n        }\n    }\n\n    const handleRenew = async () => {\n        setIsRenewing(true)\n        try {\n            await onRenew()\n        } finally {\n            setIsRenewing(false)\n        }\n    }\n\n    const handleRevoke = async () => {\n        if (!confirm('Are you sure you want to revoke this certificate?')) {\n            return\n        }\n        setIsRevoking(true)\n        try {\n            await onRevoke()\n        } finally {\n            setIsRevoking(false)\n        }\n    }\n\n    return (\n        <div className=\"space-y-4\">\n            <Separator />\n\n            {/* Force HTTPS Toggle */}\n            <div className=\"flex items-center justify-between p-4 rounded-lg border bg-card\">\n                <div className=\"flex items-start gap-3\">\n                    <div className=\"w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/20 flex items-center justify-center\">\n                        <Lock className=\"w-5 h-5 text-blue-600 dark:text-blue-400\" />\n                    </div>\n                    <div>\n                        <Label htmlFor=\"force-https\" className=\"font-medium cursor-pointer\">\n                            Force HTTPS\n                        </Label>\n                        <p className=\"text-sm text-muted-foreground mt-0.5\">\n                            Automatically redirect all HTTP traffic to HTTPS\n                        </p>\n                    </div>\n                </div>\n                <Switch\n                    id=\"force-https\"\n                    checked={forceHttps}\n                    onCheckedChange={handleToggleForceHttps}\n                    disabled={isTogglingHttps}\n                />\n            </div>\n\n            {/* Certificate Actions */}\n            <div className=\"grid grid-cols-2 gap-3\">\n                <Button\n                    variant=\"outline\"\n                    onClick={handleRenew}\n                    disabled={isRenewing}\n                >\n                    {isRenewing ? (\n                        <>\n                            <RefreshCw className=\"w-4 h-4 mr-2 animate-spin\" />\n                            Renewing...\n                        </>\n                    ) : (\n                        <>\n                            <RotateCw className=\"w-4 h-4 mr-2\" />\n                            Renew Now\n                        </>\n                    )}\n                </Button>\n                <Button\n                    variant=\"outline\"\n                    onClick={handleRevoke}\n                    disabled={isRevoking}\n                    className=\"text-destructive hover:text-destructive\"\n                >\n                    {isRevoking ? (\n                        <>\n                            <RefreshCw className=\"w-4 h-4 mr-2 animate-spin\" />\n                            Revoking...\n                        </>\n                    ) : (\n                        <>\n                            <ShieldOff className=\"w-4 h-4 mr-2\" />\n                            Revoke\n                        </>\n                    )}\n                </Button>\n            </div>\n        </div>\n    )\n}\n"
  },
  {
    "path": "app/dashboard/projects/[projectId]/domains/components/ssl/SSLCertificateStatus.tsx",
    "content": "'use client'\n\nimport { Badge } from '@/components/ui/badge'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Label } from '@/components/ui/label'\nimport { Alert, AlertDescription } from '@/components/ui/alert'\nimport { Shield, CheckCircle2, RefreshCw, AlertTriangle } from 'lucide-react'\nimport type { DomainCertificate } from '@prisma/client'\n\ninterface SSLCertificateStatusProps {\n    sslMode: 'LETS_ENCRYPT' | 'EXTERNAL' | 'NONE'\n    activeCert?: DomainCertificate | null\n    pendingCert?: DomainCertificate | null\n    isExpiringSoon?: boolean\n}\n\nexport function SSLCertificateStatus({\n    sslMode,\n    activeCert,\n    pendingCert,\n    isExpiringSoon,\n}: SSLCertificateStatusProps) {\n    return (\n        <div className=\"space-y-4\">\n            {/* Certificate Status Header */}\n            <div className=\"flex items-start gap-4\">\n                <div className=\"w-14 h-14 rounded-xl bg-gradient-to-br from-green-500 to-emerald-600 flex items-center justify-center flex-shrink-0 shadow-lg\">\n                    <Shield className=\"w-7 h-7 text-white\" />\n                </div>\n                <div className=\"flex-1 min-w-0\">\n                    <div className=\"flex items-center gap-2 mb-1\">\n                        <h3 className=\"text-lg font-semibold\">SSL Certificate</h3>\n                        {activeCert && (\n                            <Badge variant=\"default\" className=\"bg-green-600\">\n                                <CheckCircle2 className=\"w-3 h-3 mr-1\" />\n                                Active\n                            </Badge>\n                        )}\n                        {pendingCert && (\n                            <Badge variant=\"secondary\">\n                                <RefreshCw className=\"w-3 h-3 mr-1 animate-spin\" />\n                                Issuing\n                            </Badge>\n                        )}\n                        {isExpiringSoon && (\n                            <Badge variant=\"default\" className=\"bg-orange-600\">\n                                <AlertTriangle className=\"w-3 h-3 mr-1\" />\n                                Expiring Soon\n                            </Badge>\n                        )}\n                    </div>\n                    <p className=\"text-sm text-muted-foreground\">\n                        {sslMode === 'LETS_ENCRYPT' ? \"Let's Encrypt (Automatic)\" : 'External Certificate'}\n                    </p>\n                </div>\n            </div>\n\n            {/* Certificate Details */}\n            {activeCert && (\n                <Card>\n                    <CardContent className=\"p-4\">\n                        <div className=\"grid grid-cols-2 gap-4 text-sm\">\n                            <div>\n                                <Label className=\"text-xs text-muted-foreground\">Issued</Label>\n                                <p className=\"font-medium mt-1\">\n                                    {new Date(activeCert.createdAt).toLocaleDateString()}\n                                </p>\n                            </div>\n                            <div>\n                                <Label className=\"text-xs text-muted-foreground\">Expires</Label>\n                                <p className=\"font-medium mt-1\">\n                                    {activeCert.expiresAt && new Date(activeCert.expiresAt).toLocaleDateString()}\n                                </p>\n                            </div>\n                            <div>\n                                <Label className=\"text-xs text-muted-foreground\">Type</Label>\n                                <p className=\"font-medium mt-1\">{activeCert.challengeType}</p>\n                            </div>\n                            <div>\n                                <Label className=\"text-xs text-muted-foreground\">Auto-Renewal</Label>\n                                <p className=\"font-medium mt-1\">\n                                    {activeCert.challengeType === 'HTTP01' ? (\n                                        <span className=\"text-green-600\">Enabled</span>\n                                    ) : (\n                                        <span className=\"text-orange-600\">Manual</span>\n                                    )}\n                                </p>\n                            </div>\n                        </div>\n                    </CardContent>\n                </Card>\n            )}\n\n            {/* Pending Certificate */}\n            {pendingCert && (\n                <Alert variant=\"info\" icon={<RefreshCw className=\"h-4 w-4 animate-spin\" />}>\n                    <AlertDescription>\n                        <p className=\"font-medium\">Certificate Issuing</p>\n                        <p className=\"text-sm mt-1\">\n                            Your SSL certificate is being issued. This usually takes 30-60 seconds.\n                        </p>\n                    </AlertDescription>\n                </Alert>\n            )}\n        </div>\n    )\n}\n"
  },
  {
    "path": "app/dashboard/projects/[projectId]/domains/components/ssl/SSLDNSInstructions.tsx",
    "content": "'use client'\n\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Alert, AlertDescription } from '@/components/ui/alert'\nimport { Separator } from '@/components/ui/separator'\nimport { FileText, Copy, X, ArrowRight } from 'lucide-react'\n\ninterface SSLDNSInstructionsProps {\n    txtName: string\n    txtValue: string\n    onVerify: () => void\n    onBack: () => void\n    onCopy: (text: string) => void\n    isProcessing: boolean\n    isCancelling?: boolean\n}\n\nexport function SSLDNSInstructions({\n    txtName,\n    txtValue,\n    onVerify,\n    onBack,\n    onCopy,\n    isProcessing,\n    isCancelling = false,\n}: SSLDNSInstructionsProps) {\n    return (\n        <div className=\"space-y-4\">\n            <Button\n                variant=\"ghost\"\n                size=\"sm\"\n                onClick={onBack}\n                disabled={isCancelling || isProcessing}\n                className=\"mb-2\"\n            >\n                {isCancelling ? (\n                    <>\n                        <div className=\"w-4 h-4 mr-2 animate-spin rounded-full border-2 border-current border-t-transparent\" />\n                        Cancelling...\n                    </>\n                ) : (\n                    <>\n                        <X className=\"w-4 h-4 mr-2\" />\n                        Cancel\n                    </>\n                )}\n            </Button>\n\n            <div className=\"space-y-3\">\n                <div className=\"flex items-start gap-3\">\n                    <div className=\"w-10 h-10 rounded-lg bg-purple-100 dark:bg-purple-900/20 flex items-center justify-center flex-shrink-0\">\n                        <FileText className=\"w-5 h-5 text-purple-600 dark:text-purple-400\" />\n                    </div>\n                    <div>\n                        <h3 className=\"font-semibold mb-1\">Add DNS TXT Record</h3>\n                        <p className=\"text-sm text-muted-foreground\">\n                            Add this TXT record to your DNS provider to verify ownership\n                        </p>\n                    </div>\n                </div>\n\n                <Card>\n                    <CardContent className=\"p-4 space-y-3\">\n                        <div className=\"grid grid-cols-3 gap-3 text-xs\">\n                            <div>\n                                <span className=\"text-muted-foreground block mb-1\">Type</span>\n                                <code className=\"font-mono font-medium\">TXT</code>\n                            </div>\n                            <div className=\"col-span-2\">\n                                <span className=\"text-muted-foreground block mb-1\">Name</span>\n                                <code className=\"font-mono text-xs break-all\">{txtName}</code>\n                            </div>\n                        </div>\n                        <Separator />\n                        <div>\n                            <span className=\"text-muted-foreground block mb-1 text-xs\">Value</span>\n                            <div className=\"bg-muted rounded p-2 font-mono text-xs break-all\">\n                                {txtValue}\n                            </div>\n                        </div>\n                        <Button\n                            variant=\"ghost\"\n                            size=\"sm\"\n                            onClick={() => onCopy(txtValue)}\n                            className=\"w-full\"\n                        >\n                            <Copy className=\"w-3 h-3 mr-2\" />\n                            Copy Value\n                        </Button>\n                    </CardContent>\n                </Card>\n\n                <Alert>\n                    <AlertDescription className=\"text-xs\">\n                        DNS changes can take 5-60 minutes to propagate. Click \"Verify\" once you've added the record.\n                    </AlertDescription>\n                </Alert>\n            </div>\n\n            <Button\n                onClick={onVerify}\n                disabled={isProcessing}\n                className=\"w-full\"\n                size=\"lg\"\n            >\n                {isProcessing ? 'Verifying...' : 'Verify DNS Record'}\n                <ArrowRight className=\"w-4 h-4 ml-2\" />\n            </Button>\n        </div>\n    )\n}\n"
  },
  {
    "path": "app/dashboard/projects/[projectId]/domains/components/ssl/SSLModeSelector.tsx",
    "content": "'use client'\n\nimport { Badge } from '@/components/ui/badge'\nimport { Shield, Zap, Upload, Check, ArrowRight } from 'lucide-react'\n\ninterface SSLModeSelectorProps {\n    onSelectMode: (mode: 'LETS_ENCRYPT' | 'EXTERNAL') => void\n}\n\nexport function SSLModeSelector({ onSelectMode }: SSLModeSelectorProps) {\n    return (\n        <div className=\"space-y-3\">\n            <div className=\"text-center mb-6\">\n                <div className=\"w-16 h-16 rounded-2xl bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center mx-auto mb-4 shadow-lg\">\n                    <Shield className=\"w-8 h-8 text-white\" />\n                </div>\n                <h3 className=\"text-xl font-bold mb-2\">Enable SSL Certificate</h3>\n                <p className=\"text-sm text-muted-foreground max-w-md mx-auto\">\n                    Secure your domain with HTTPS. Choose how you want to manage your SSL certificate.\n                </p>\n            </div>\n\n            {/* Let's Encrypt Option */}\n            <button\n                onClick={() => onSelectMode('LETS_ENCRYPT')}\n                className=\"w-full text-left p-5 rounded-xl border-2 border-border hover:border-blue-500 hover:shadow-lg transition-all group bg-card\"\n            >\n                <div className=\"flex items-start gap-4\">\n                    <div className=\"w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform shadow-md\">\n                        <Zap className=\"w-6 h-6 text-white\" />\n                    </div>\n                    <div className=\"flex-1 min-w-0\">\n                        <div className=\"flex items-center gap-2 mb-2\">\n                            <p className=\"font-bold text-lg\">Let's Encrypt</p>\n                            <Badge className=\"bg-blue-600\">Recommended</Badge>\n                        </div>\n                        <p className=\"text-sm text-muted-foreground mb-3\">\n                            Free SSL certificates with automatic renewal. Perfect for most use cases.\n                        </p>\n                        <div className=\"flex flex-wrap gap-2\">\n                            <Badge variant=\"outline\" className=\"text-xs\">\n                                <Check className=\"w-3 h-3 mr-1\" />\n                                Free Forever\n                            </Badge>\n                            <Badge variant=\"outline\" className=\"text-xs\">\n                                <Check className=\"w-3 h-3 mr-1\" />\n                                Auto-Renewal\n                            </Badge>\n                            <Badge variant=\"outline\" className=\"text-xs\">\n                                <Check className=\"w-3 h-3 mr-1\" />\n                                Trusted by Browsers\n                            </Badge>\n                        </div>\n                    </div>\n                    <ArrowRight className=\"w-5 h-5 text-muted-foreground group-hover:text-blue-600 group-hover:translate-x-1 transition-all\" />\n                </div>\n            </button>\n\n            {/* External Certificate Option */}\n            <button\n                onClick={() => onSelectMode('EXTERNAL')}\n                className=\"w-full text-left p-5 rounded-xl border-2 border-border hover:border-purple-500 hover:shadow-lg transition-all group bg-card\"\n            >\n                <div className=\"flex items-start gap-4\">\n                    <div className=\"w-12 h-12 rounded-xl bg-gradient-to-br from-purple-500 to-purple-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform shadow-md\">\n                        <Shield className=\"w-6 h-6 text-white\" />\n                    </div>\n                    <div className=\"flex-1 min-w-0\">\n                        <div className=\"flex items-center gap-2 mb-2\">\n                            <p className=\"font-bold text-lg\">Provider-Managed SSL</p>\n                            <Badge variant=\"outline\">External</Badge>\n                        </div>\n                        <p className=\"text-sm text-muted-foreground mb-3\">\n                            SSL certificate managed by your hosting provider or CDN.\n                        </p>\n                        <div className=\"flex flex-wrap gap-2\">\n                            <Badge variant=\"outline\" className=\"text-xs\">\n                                Provider Managed\n                            </Badge>\n                            <Badge variant=\"outline\" className=\"text-xs\">\n                                No Configuration\n                            </Badge>\n                            <Badge variant=\"outline\" className=\"text-xs\">\n                                Third-Party SSL\n                            </Badge>\n                        </div>\n                    </div>\n                    <ArrowRight className=\"w-5 h-5 text-muted-foreground group-hover:text-purple-600 group-hover:translate-x-1 transition-all\" />\n                </div>\n            </button>\n        </div>\n    )\n}\n"
  },
  {
    "path": "app/dashboard/projects/[projectId]/domains/components/ssl/SSLVerificationMethod.tsx",
    "content": "'use client'\n\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Zap, Globe, Check, ArrowLeft, Loader2 } from 'lucide-react'\n\ninterface SSLVerificationMethodProps {\n    onSelectMethod: (method: 'HTTP01' | 'DNS01') => void\n    onBack: () => void\n    isProcessing: boolean\n}\n\nexport function SSLVerificationMethod({\n    onSelectMethod,\n    onBack,\n    isProcessing,\n}: SSLVerificationMethodProps) {\n    return (\n        <div className=\"space-y-4\">\n            <Button\n                variant=\"ghost\"\n                size=\"sm\"\n                onClick={onBack}\n                disabled={isProcessing}\n                className=\"mb-2\"\n            >\n                <ArrowLeft className=\"w-4 h-4 mr-2\" />\n                Back\n            </Button>\n\n            <div className=\"text-center mb-6\">\n                <h3 className=\"text-lg font-bold mb-2\">\n                    {isProcessing ? 'Starting Certificate Issuance...' : 'Choose Verification Method'}\n                </h3>\n                <p className=\"text-sm text-muted-foreground\">\n                    {isProcessing\n                        ? 'Please wait while we initiate the SSL certificate process'\n                        : 'How would you like to verify domain ownership?'}\n                </p>\n            </div>\n\n            {isProcessing ? (\n                <div className=\"flex items-center justify-center py-8\">\n                    <Loader2 className=\"w-8 h-8 animate-spin text-blue-600\" />\n                </div>\n            ) : (\n                <>\n                    {/* HTTP-01 Option - TEMPORARILY DISABLED */}\n                    {/*<button*/}\n                    {/*    onClick={() => onSelectMethod('HTTP01')}*/}\n                    {/*    className=\"w-full text-left p-4 rounded-xl border-2 border-border hover:border-blue-500 transition-all group bg-card\"*/}\n                    {/*>*/}\n                    {/*<div className=\"flex items-start gap-3\">*/}\n                    {/*    <div className=\"w-11 h-11 rounded-lg bg-blue-100 dark:bg-blue-900/20 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform\">*/}\n                    {/*        <Zap className=\"w-5 h-5 text-blue-600 dark:text-blue-400\" />*/}\n                    {/*    </div>*/}\n                    {/*    <div className=\"flex-1 min-w-0\">*/}\n                    {/*        <div className=\"flex items-center gap-2 mb-1\">*/}\n                    {/*            <p className=\"font-semibold\">HTTP-01 (Automatic)</p>*/}\n                    {/*            <Badge variant=\"secondary\" className=\"text-xs\">Recommended</Badge>*/}\n                    {/*        </div>*/}\n                    {/*        <p className=\"text-sm text-muted-foreground mb-2\">*/}\n                    {/*            Fully automatic verification. Completes in seconds with no manual steps.*/}\n                    {/*        </p>*/}\n                    {/*        <div className=\"flex gap-2\">*/}\n                    {/*            <Badge variant=\"outline\" className=\"text-xs\">*/}\n                    {/*                <Check className=\"w-3 h-3 mr-1\" />*/}\n                    {/*                Instant*/}\n                    {/*            </Badge>*/}\n                    {/*            <Badge variant=\"outline\" className=\"text-xs\">*/}\n                    {/*                <Check className=\"w-3 h-3 mr-1\" />*/}\n                    {/*                Auto-Renewal*/}\n                    {/*            </Badge>*/}\n                    {/*        </div>*/}\n                    {/*    </div>*/}\n                    {/*</div>*/}\n                    {/*</button>*/}\n\n                    {/* DNS-01 Option */}\n                    <button\n                        onClick={() => onSelectMethod('DNS01')}\n                        className=\"w-full text-left p-4 rounded-xl border-2 border-border hover:border-purple-500 transition-all group bg-card\"\n                    >\n                        <div className=\"flex items-start gap-3\">\n                            <div className=\"w-11 h-11 rounded-lg bg-purple-100 dark:bg-purple-900/20 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform\">\n                                <Globe className=\"w-5 h-5 text-purple-600 dark:text-purple-400\" />\n                            </div>\n                            <div className=\"flex-1 min-w-0\">\n                                <div className=\"flex items-center gap-2 mb-1\">\n                                    <p className=\"font-semibold\">DNS-01 (Manual)</p>\n                                    <Badge variant=\"outline\" className=\"text-xs\">Advanced</Badge>\n                                </div>\n                                <p className=\"text-sm text-muted-foreground mb-2\">\n                                    Verify via DNS TXT record. Required for wildcard certificates.\n                                </p>\n                                <div className=\"flex gap-2\">\n                                    <Badge variant=\"outline\" className=\"text-xs\">\n                                        DNS Access Required\n                                    </Badge>\n                                    <Badge variant=\"outline\" className=\"text-xs\">\n                                        Manual Renewal\n                                    </Badge>\n                                </div>\n                            </div>\n                        </div>\n                    </button>\n                </>\n            )}\n        </div>\n    )\n}\n"
  },
  {
    "path": "app/dashboard/projects/[projectId]/domains/components/ssl/SSLVerificationProgress.tsx",
    "content": "'use client'\n\nimport { Button } from '@/components/ui/button'\nimport { Alert, AlertDescription } from '@/components/ui/alert'\nimport { RefreshCw, X } from 'lucide-react'\n\ninterface SSLVerificationProgressProps {\n    type: 'http01' | 'dns01'\n    onCancel: () => void\n}\n\nexport function SSLVerificationProgress({ type, onCancel }: SSLVerificationProgressProps) {\n    return (\n        <div className=\"space-y-4\">\n            <Alert variant=\"info\" icon={<RefreshCw className=\"h-4 w-4 animate-spin\" />}>\n                <AlertDescription>\n                    <p className=\"font-medium\">\n                        {type === 'http01' ? 'Verifying Domain...' : 'Verifying DNS Record...'}\n                    </p>\n                    <p className=\"text-sm mt-1\">\n                        {type === 'http01'\n                            ? 'Automatically verifying your domain ownership. This usually takes 10-30 seconds.'\n                            : 'Checking DNS records and issuing certificate. This may take a minute.'\n                        }\n                    </p>\n                </AlertDescription>\n            </Alert>\n\n            <Button variant=\"outline\" onClick={onCancel} className=\"w-full\">\n                <X className=\"w-4 h-4 mr-2\" />\n                Cancel\n            </Button>\n        </div>\n    )\n}\n"
  },
  {
    "path": "app/dashboard/projects/[projectId]/domains/components/ssl/index.tsx",
    "content": "'use client'\n\nimport { useState, useEffect } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { Badge } from '@/components/ui/badge'\nimport { Alert, AlertDescription } from '@/components/ui/alert'\nimport { Shield } from 'lucide-react'\nimport { motion, AnimatePresence } from 'framer-motion'\nimport type { CustomDomain } from '@/lib/types/custom-domains'\n\n// Sub-components\nimport { SSLModeSelector } from './SSLModeSelector'\nimport { SSLCertificateStatus } from './SSLCertificateStatus'\nimport { SSLVerificationMethod } from './SSLVerificationMethod'\nimport { SSLDNSInstructions } from './SSLDNSInstructions'\nimport { SSLVerificationProgress } from './SSLVerificationProgress'\nimport { SSLCertificateActions } from './SSLCertificateActions'\nimport { ExternalSSLManagement } from './ExternalSSLManagement'\n\ninterface SSLManagementProps {\n    domain: CustomDomain\n    onUpdate: () => void\n    onError: (error: string) => void\n    onSuccess: (message: string) => void\n}\n\ntype FlowStep = 'mode-select' | 'method-select' | 'dns-instructions' | 'http01-progress' | 'dns01-progress' | 'external-info'\n\nexport function SSLManagement({ domain, onUpdate, onError, onSuccess }: SSLManagementProps) {\n    const [step, setStep] = useState<FlowStep>('mode-select')\n    const [isProcessing, setIsProcessing] = useState(false)\n    const [isCancelling, setIsCancelling] = useState(false)\n    const [certId, setCertId] = useState<string | null>(null)\n    const [dnsChallenge, setDnsChallenge] = useState<{ txtName: string; txtValue: string } | null>(null)\n    const [pollingInterval, setPollingInterval] = useState<NodeJS.Timeout | null>(null)\n\n    const activeCert = domain.certificates?.find(c => c.status === 'ISSUED')\n    const pendingCert = domain.certificates?.find(c => c.status === 'PENDING_HTTP01' || c.status === 'PENDING_DNS01')\n    const isExpiringSoon = activeCert?.expiresAt\n        ? new Date(activeCert.expiresAt).getTime() - Date.now() < 30 * 24 * 60 * 60 * 1000\n        : false\n\n    useEffect(() => {\n        return () => {\n            if (pollingInterval) clearInterval(pollingInterval)\n        }\n    }, [pollingInterval])\n\n    // Auto-detect pending certificate and show progress UI (only if no active cert exists)\n    useEffect(() => {\n        if (pendingCert && !activeCert && step === 'mode-select') {\n            setCertId(pendingCert.id)\n\n            if (pendingCert.status === 'PENDING_HTTP01') {\n                setStep('http01-progress')\n                startPolling(pendingCert.id)\n            } else if (pendingCert.status === 'PENDING_DNS01') {\n                // For DNS challenges, show the DNS instructions if we have the TXT value\n                if (pendingCert.dnsTxtValue) {\n                    setDnsChallenge({\n                        txtName: `_acme-challenge.${domain.domain}`,\n                        txtValue: pendingCert.dnsTxtValue\n                    })\n                    setStep('dns-instructions')\n                } else {\n                    setStep('dns01-progress')\n                    startPolling(pendingCert.id)\n                }\n            }\n        }\n    }, [pendingCert, activeCert, step])\n\n    const startPolling = (id: string) => {\n        const interval = setInterval(async () => {\n            try {\n                const response = await fetch(`/api/acme/status/${id}`)\n                const result = await response.json()\n\n                if (result.status === 'ISSUED') {\n                    clearInterval(interval)\n                    setPollingInterval(null)\n                    await onUpdate()\n                    onSuccess('SSL certificate issued successfully!')\n                    setStep('mode-select')\n                    setCertId(null)\n                } else if (result.status === 'FAILED') {\n                    clearInterval(interval)\n                    setPollingInterval(null)\n                    onError(result.lastError || 'Certificate issuance failed')\n                    setStep('mode-select')\n                    setCertId(null)\n                }\n            } catch (error) {\n                console.error('Polling error:', error)\n            }\n        }, 3000)\n\n        setPollingInterval(interval)\n    }\n\n    const handleSelectMode = async (mode: 'LETS_ENCRYPT' | 'EXTERNAL') => {\n        if (mode === 'LETS_ENCRYPT') {\n            setStep('method-select')\n        } else {\n            // Update to EXTERNAL mode\n            setIsProcessing(true)\n            try {\n                const response = await fetch(`/api/custom-domains/${domain.domain}/ssl/mode`, {\n                    method: 'POST',\n                    headers: { 'Content-Type': 'application/json' },\n                    body: JSON.stringify({ sslMode: 'EXTERNAL' }),\n                })\n\n                const result = await response.json()\n                if (response.ok) {\n                    onSuccess('SSL mode set to Provider-Managed')\n                    await onUpdate()\n                } else {\n                    onError(result.error || 'Failed to update SSL mode')\n                }\n            } catch (error) {\n                onError('Failed to update SSL mode')\n            } finally {\n                setIsProcessing(false)\n            }\n        }\n    }\n\n    const handleSelectMethod = async (method: 'HTTP01' | 'DNS01') => {\n        setIsProcessing(true)\n\n        try {\n            const response = await fetch('/api/acme/issue', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({\n                    domainId: domain.id,\n                    challengeType: method,\n                }),\n            })\n\n            const result = await response.json()\n            if (response.ok) {\n                setCertId(result.certId)\n\n                if (method === 'HTTP01') {\n                    setStep('http01-progress')\n                    startPolling(result.certId)\n                } else {\n                    setDnsChallenge({ txtName: result.txtName, txtValue: result.txtValue })\n                    setStep('dns-instructions')\n                }\n            } else {\n                // Check if this is a \"certificate already exists\" error\n                if (result.canForceDelete && result.certificateId) {\n                    const shouldForceDelete = confirm(\n                        'A certificate already exists for this domain. This might be a stale certificate from a previous attempt.\\n\\n' +\n                        'Would you like to force delete the existing certificate and try again?'\n                    )\n\n                    if (shouldForceDelete) {\n                        // Force delete and retry\n                        await handleRevoke()\n                        // Retry the issuance after a short delay\n                        setTimeout(() => handleSelectMethod(method), 1000)\n                        return\n                    }\n                }\n\n                onError(result.error || 'Failed to start certificate issuance')\n                setStep('method-select')\n            }\n        } catch (error) {\n            onError('Failed to start certificate issuance')\n            setStep('method-select')\n        } finally {\n            setIsProcessing(false)\n        }\n    }\n\n    const handleVerifyDNS = async (id?: string) => {\n        const targetCertId = id || certId\n        console.log('[SSL UI] handleVerifyDNS called with certId:', targetCertId)\n\n        if (!targetCertId) {\n            console.error('[SSL UI] No certId provided to handleVerifyDNS')\n            return\n        }\n\n        setIsProcessing(true)\n        setStep('dns01-progress') // Show progress UI immediately\n\n        try {\n            console.log('[SSL UI] Sending DNS verification request...')\n            const response = await fetch('/api/acme/verify-dns', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({ certId: targetCertId }),\n            })\n\n            console.log('[SSL UI] DNS verification response:', response.status, response.statusText)\n            const result = await response.json()\n            console.log('[SSL UI] DNS verification result:', result)\n\n            // Check for 202 (retry) BEFORE checking response.ok\n            if (response.status === 202 && result.retry) {\n                // Check if TXT value changed\n                if (result.txtValueChanged && result.newTxtValue) {\n                    console.log('[SSL UI] TXT value changed, updating dnsChallenge state')\n                    setDnsChallenge({\n                        txtName: dnsChallenge?.txtName || `_acme-challenge.${domain.domain}`,\n                        txtValue: result.newTxtValue,\n                    })\n                    onError(result.message || 'DNS TXT value has changed. Please update your DNS record.')\n                } else {\n                    // DNS not propagated yet - show friendly retry message\n                    console.log('[SSL UI] DNS not propagated, user should retry')\n                    onError(result.message || 'DNS TXT record not yet propagated. Please wait and try again.')\n                }\n                setStep('dns-instructions') // Go back to instructions\n            } else if (response.ok && result.success) {\n                console.log('[SSL UI] DNS verification successful, certificate issued!')\n                onSuccess('SSL certificate issued successfully!')\n                await onUpdate() // Refresh the domain data\n                setStep('mode-select')\n            } else {\n                console.error('[SSL UI] DNS verification failed:', result.error)\n                onError(result.error || result.message || 'DNS verification failed')\n                setStep('dns-instructions') // Go back to instructions\n            }\n        } catch (error) {\n            console.error('[SSL UI] DNS verification exception:', error)\n            onError('Failed to verify DNS challenge - please check your network connection')\n            setStep('dns-instructions') // Go back to instructions\n        } finally {\n            setIsProcessing(false)\n        }\n    }\n\n    const handleCancel = async () => {\n        console.log('[SSL UI] Cancel button clicked')\n        setIsCancelling(true)\n\n        try {\n            // Stop polling first\n            if (pollingInterval) {\n                console.log('[SSL UI] Clearing polling interval')\n                clearInterval(pollingInterval)\n                setPollingInterval(null)\n            }\n\n            // Cancel the certificate if we have a certId\n            if (certId) {\n                console.log(`[SSL UI] Cancelling certificate: ${certId}`)\n                try {\n                    const response = await fetch(`/api/acme/cancel/${certId}`, { method: 'POST' })\n                    console.log('[SSL UI] Cancel response:', response.status)\n\n                    if (!response.ok) {\n                        const result = await response.json()\n                        console.error('[SSL UI] Cancel failed:', result)\n                    } else {\n                        console.log('[SSL UI] Certificate cancelled successfully')\n                    }\n                } catch (error) {\n                    console.error('[SSL UI] Cancel request error:', error)\n                }\n            }\n\n            // Reset all state\n            console.log('[SSL UI] Resetting state')\n            setStep('mode-select')\n            setCertId(null)\n            setDnsChallenge(null)\n\n            // Refresh domain data to update UI\n            console.log('[SSL UI] Refreshing domain data')\n            await onUpdate()\n\n            onSuccess('Certificate issuance cancelled')\n        } catch (error) {\n            console.error('[SSL UI] Error during cancel:', error)\n            onError('Failed to cancel certificate issuance')\n        } finally {\n            setIsCancelling(false)\n        }\n    }\n\n    const handleToggleForceHttps = async (enabled: boolean) => {\n        const response = await fetch(`/api/custom-domains/${domain.domain}/ssl/toggle-https`, {\n            method: 'POST',\n            headers: { 'Content-Type': 'application/json' },\n            body: JSON.stringify({ forceHttps: enabled }),\n        })\n\n        const result = await response.json()\n        if (response.ok) {\n            onSuccess(enabled ? 'Force HTTPS enabled' : 'Force HTTPS disabled')\n            await onUpdate()\n        } else {\n            throw new Error(result.error || 'Failed to toggle Force HTTPS')\n        }\n    }\n\n    const handleRenew = async () => {\n        if (!activeCert) return\n\n        const response = await fetch(`/api/acme/renew/${activeCert.id}`, {\n            method: 'POST',\n        })\n\n        const result = await response.json()\n        if (response.ok) {\n            onSuccess('Certificate renewal initiated')\n            await onUpdate()\n        } else {\n            throw new Error(result.error || 'Failed to renew certificate')\n        }\n    }\n\n    const handleRevoke = async () => {\n        // Nuke all certificates for this domain to allow fresh re-issuance\n        const response = await fetch(`/api/custom-domains/${encodeURIComponent(domain.domain)}/ssl/revoke`, {\n            method: 'DELETE',\n        })\n\n        const result = await response.json()\n        if (response.ok) {\n            // Reset local state immediately\n            setStep('mode-select')\n            setCertId(null)\n            setDnsChallenge(null)\n            if (pollingInterval) {\n                clearInterval(pollingInterval)\n                setPollingInterval(null)\n            }\n\n            // Wait a moment for database transaction to complete, then refresh\n            await new Promise(resolve => setTimeout(resolve, 500))\n            await onUpdate()\n\n            onSuccess(`Certificate removed successfully. You can now issue a new certificate.`)\n        } else {\n            throw new Error(result.error || 'Failed to remove certificate')\n        }\n    }\n\n    // Show EXTERNAL SSL management UI\n    if (domain.sslMode === 'EXTERNAL') {\n        return (\n            <div className=\"space-y-6\">\n                {/* External SSL Header */}\n                <div className=\"flex items-start gap-4\">\n                    <div className=\"w-14 h-14 rounded-xl bg-gradient-to-br from-purple-500 to-purple-600 flex items-center justify-center flex-shrink-0 shadow-lg\">\n                        <Shield className=\"w-7 h-7 text-white\" />\n                    </div>\n                    <div className=\"flex-1 min-w-0\">\n                        <div className=\"flex items-center gap-2 mb-1\">\n                            <h3 className=\"text-lg font-semibold\">Provider-Managed SSL</h3>\n                            <Badge variant=\"outline\">External</Badge>\n                        </div>\n                        <p className=\"text-sm text-muted-foreground\">\n                            SSL certificate is managed by your hosting provider or CDN\n                        </p>\n                    </div>\n                </div>\n\n                <Alert variant=\"info\">\n                    <AlertDescription>\n                        <div className=\"flex items-start justify-between gap-4\">\n                            <div>\n                                <p className=\"font-medium mb-1\">External SSL Mode Active</p>\n                                <p className=\"text-sm\">\n                                    Your SSL certificate is managed externally. Configure domain-specific settings below.\n                                </p>\n                            </div>\n                            <Button\n                                variant=\"outline\"\n                                size=\"sm\"\n                                onClick={async () => {\n                                    const response = await fetch(`/api/custom-domains/${domain.domain}/ssl/mode`, {\n                                        method: 'POST',\n                                        headers: { 'Content-Type': 'application/json' },\n                                        body: JSON.stringify({ sslMode: 'NONE' }),\n                                    })\n\n                                    const result = await response.json()\n                                    if (response.ok) {\n                                        onSuccess('SSL mode changed to None')\n                                        await onUpdate()\n                                    } else {\n                                        onError(result.error || 'Failed to change SSL mode')\n                                    }\n                                }}\n                                className=\"flex-shrink-0\"\n                            >\n                                Change Mode\n                            </Button>\n                        </div>\n                    </AlertDescription>\n                </Alert>\n\n                <ExternalSSLManagement\n                    domain={domain.domain}\n                    browserRules={domain.browserRules || []}\n                    throttleConfig={domain.throttleConfig}\n                    onUpdate={onUpdate}\n                    onError={onError}\n                    onSuccess={onSuccess}\n                />\n            </div>\n        )\n    }\n\n    // PRIORITY 1: If there's an active certificate, always show management UI (regardless of sslMode)\n    if (activeCert) {\n        return (\n            <div className=\"space-y-6\">\n                <SSLCertificateStatus\n                    sslMode=\"LETS_ENCRYPT\"\n                    activeCert={activeCert}\n                    pendingCert={pendingCert}\n                    isExpiringSoon={isExpiringSoon}\n                />\n\n                <SSLCertificateActions\n                    domain={domain.domain}\n                    forceHttps={domain.forceHttps}\n                    onToggleForceHttps={handleToggleForceHttps}\n                    onRenew={handleRenew}\n                    onRevoke={handleRevoke}\n                    onError={onError}\n                    onSuccess={onSuccess}\n                />\n\n                {pendingCert && (\n                    <>\n                        {pendingCert.status === 'PENDING_DNS01' && pendingCert.dnsTxtValue && (\n                            <SSLDNSInstructions\n                                txtName={`_acme-challenge.${domain.domain}`}\n                                txtValue={pendingCert.dnsTxtValue}\n                                onVerify={() => handleVerifyDNS(pendingCert.id)}\n                                onBack={handleCancel}\n                                onCopy={(text) => {\n                                    navigator.clipboard.writeText(text)\n                                    onSuccess('Copied to clipboard!')\n                                }}\n                                isProcessing={isProcessing}\n                            />\n                        )}\n                        {pendingCert.status === 'PENDING_HTTP01' && (\n                            <SSLVerificationProgress\n                                type=\"http01\"\n                                onCancel={handleCancel}\n                            />\n                        )}\n                        <Button\n                            variant=\"outline\"\n                            onClick={async () => {\n                                try {\n                                    await fetch(`/api/acme/cancel/${pendingCert.id}`, { method: 'POST' })\n                                    onSuccess('Certificate issuance cancelled')\n                                    await onUpdate()\n                                } catch {\n                                    onError('Failed to cancel')\n                                }\n                            }}\n                            className=\"w-full\"\n                        >\n                            Cancel Issuance\n                        </Button>\n                    </>\n                )}\n\n                {/* Domain-specific features (browser rules, rate limiting) */}\n                <ExternalSSLManagement\n                    domain={domain.domain}\n                    browserRules={domain.browserRules || []}\n                    throttleConfig={domain.throttleConfig}\n                    onUpdate={onUpdate}\n                    onError={onError}\n                    onSuccess={onSuccess}\n                />\n            </div>\n        )\n    }\n\n    // PRIORITY 2: Show pending certificate progress (during initial setup)\n    if (pendingCert && !activeCert && step !== 'mode-select') {\n        // Let the step-based flow handle this (http01-progress, dns01-progress, etc.)\n        // This section intentionally falls through to the setup flow below\n    }\n\n    // Setup flow\n    return (\n        <AnimatePresence mode=\"wait\">\n            <motion.div\n                key={step}\n                initial={{ opacity: 0, y: 10 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={{ opacity: 0, y: -10 }}\n                transition={{ duration: 0.2 }}\n            >\n                {step === 'mode-select' && (\n                    <SSLModeSelector onSelectMode={handleSelectMode} />\n                )}\n\n                {step === 'method-select' && (\n                    <SSLVerificationMethod\n                        onSelectMethod={handleSelectMethod}\n                        onBack={() => setStep('mode-select')}\n                        isProcessing={isProcessing}\n                    />\n                )}\n\n                {step === 'dns-instructions' && dnsChallenge && (\n                    <SSLDNSInstructions\n                        txtName={dnsChallenge.txtName}\n                        txtValue={dnsChallenge.txtValue}\n                        onVerify={() => handleVerifyDNS()}\n                        onBack={handleCancel}\n                        onCopy={(text) => {\n                            navigator.clipboard.writeText(text)\n                            onSuccess('Copied to clipboard!')\n                        }}\n                        isProcessing={isProcessing}\n                        isCancelling={isCancelling}\n                    />\n                )}\n\n                {step === 'http01-progress' && (\n                    <SSLVerificationProgress type=\"http01\" onCancel={handleCancel} />\n                )}\n\n                {step === 'dns01-progress' && (\n                    <SSLVerificationProgress type=\"dns01\" onCancel={handleCancel} />\n                )}\n\n            </motion.div>\n        </AnimatePresence>\n    )\n}\n"
  },
  {
    "path": "app/dashboard/projects/[projectId]/domains/page.tsx",
    "content": "'use client'\n\nimport { useState, useEffect, use } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { Alert, AlertDescription } from '@/components/ui/alert'\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogHeader,\n    DialogTitle,\n    DialogTrigger,\n} from '@/components/ui/dialog'\nimport {\n    Globe,\n    Plus,\n    RefreshCw,\n    Sparkles,\n} from 'lucide-react'\nimport { motion, AnimatePresence } from 'framer-motion'\nimport type { CustomDomain, DNSInstructions } from '@/lib/types/custom-domains'\nimport { DomainCard } from './components/DomainCard'\n\ninterface ProjectDomainSettingsProps {\n    params: Promise<{\n        projectId: string\n    }>\n}\n\n// Confetti component for celebrations\nconst ConfettiAnimation = ({ show }: { show: boolean }) => {\n    if (!show) return null\n\n    const confettiPieces = Array.from({ length: 50 }, (_, i) => (\n        <motion.div\n            key={i}\n            className=\"absolute w-2 h-2 rounded-full\"\n            style={{\n                backgroundColor: ['#ef4444', '#f97316', '#eab308', '#22c55e', '#3b82f6', '#8b5cf6', '#ec4899'][i % 7],\n                left: `${Math.random() * 100}%`,\n                top: '-10px',\n            }}\n            initial={{ y: -10, rotate: 0, scale: 0 }}\n            animate={{\n                y: window.innerHeight + 100,\n                rotate: Math.random() * 360,\n                scale: [0, 1, 0.8, 0],\n                x: Math.random() * 200 - 100,\n            }}\n            transition={{\n                duration: 3 + Math.random() * 2,\n                ease: 'easeOut',\n                delay: Math.random() * 0.5,\n            }}\n        />\n    ))\n\n    return (\n        <div className=\"fixed inset-0 pointer-events-none z-50\">\n            {confettiPieces}\n        </div>\n    )\n}\n\nexport default function ProjectDomainSettings({ params }: ProjectDomainSettingsProps) {\n    const { projectId } = use(params)\n    const [domains, setDomains] = useState<CustomDomain[]>([])\n    const [isLoading, setIsLoading] = useState(true)\n    const [isAddingDomain, setIsAddingDomain] = useState(false)\n    const [newDomain, setNewDomain] = useState('')\n    const [error, setError] = useState<string | null>(null)\n    const [success, setSuccess] = useState<string | null>(null)\n    const [showConfetti, setShowConfetti] = useState(false)\n    const [showAddDialog, setShowAddDialog] = useState(false)\n    const [sslEnabled, setSslEnabled] = useState(false)\n\n    useEffect(() => {\n        loadDomains()\n        loadRuntimeConfig()\n    }, [projectId])\n\n    const loadRuntimeConfig = async () => {\n        try {\n            const response = await fetch('/api/config/runtime')\n            const config = await response.json()\n            setSslEnabled(config.sslEnabled)\n        } catch (error) {\n            console.error('Failed to load runtime config:', error)\n            setSslEnabled(false)\n        }\n    }\n\n    useEffect(() => {\n        if (success || error) {\n            const timer = setTimeout(() => {\n                setSuccess(null)\n                setError(null)\n            }, 5000)\n            return () => clearTimeout(timer)\n        }\n    }, [success, error])\n\n    const loadDomains = async () => {\n        try {\n            setIsLoading(true)\n            setError(null)\n            const response = await fetch(`/api/custom-domains/list?scope=project&projectId=${projectId}`)\n            const result = await response.json()\n\n            if (result.success) {\n                setDomains(result.domains || [])\n            } else {\n                setError(result.error || 'Failed to load domains')\n            }\n        } catch (error) {\n            setError('Failed to load domains')\n            console.error('Failed to load domains:', error)\n        } finally {\n            setIsLoading(false)\n        }\n    }\n\n    const handleAddDomain = async (e: React.FormEvent) => {\n        e.preventDefault()\n        if (!newDomain) return\n\n        setIsAddingDomain(true)\n        setError(null)\n\n        try {\n            const response = await fetch('/api/custom-domains/add', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({\n                    domain: newDomain,\n                    projectId,\n                }),\n            })\n\n            const result = await response.json()\n            if (result.success) {\n                setNewDomain('')\n                setShowAddDialog(false)\n                setSuccess(`Domain ${newDomain} added successfully! Follow the DNS instructions to verify.`)\n                setShowConfetti(true)\n                setTimeout(() => setShowConfetti(false), 5000)\n                await loadDomains()\n            } else {\n                setError(result.error || 'Failed to add domain')\n            }\n        } catch (error) {\n            setError('Failed to add domain')\n            console.error('Failed to add domain:', error)\n        } finally {\n            setIsAddingDomain(false)\n        }\n    }\n\n    const handleDeleteDomain = async (domain: string) => {\n        if (!confirm(`Are you sure you want to delete ${domain}? This action cannot be undone.`)) {\n            return\n        }\n\n        setError(null)\n\n        try {\n            const response = await fetch(`/api/custom-domains/${encodeURIComponent(domain)}`, {\n                method: 'DELETE',\n            })\n\n            const result = await response.json()\n            if (result.success) {\n                setSuccess(`Domain ${domain} deleted successfully`)\n                await loadDomains()\n            } else {\n                setError(result.error || 'Failed to delete domain')\n            }\n        } catch (error) {\n            setError('Failed to delete domain')\n            console.error('Failed to delete domain:', error)\n        }\n    }\n\n    if (isLoading) {\n        return (\n            <div className=\"flex items-center justify-center min-h-[50vh]\">\n                <div className=\"text-center\">\n                    <RefreshCw className=\"w-8 h-8 animate-spin mx-auto mb-4 text-muted-foreground\" />\n                    <p className=\"text-muted-foreground\">Loading domains...</p>\n                </div>\n            </div>\n        )\n    }\n\n    return (\n        <div className=\"space-y-6\">\n            <ConfettiAnimation show={showConfetti} />\n\n            {/* Header */}\n            <div className=\"flex items-center justify-between\">\n                <div className=\"space-y-1\">\n                    <div className=\"flex items-center gap-3\">\n                        <div className=\"w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center\">\n                            <Globe className=\"w-4 h-4 text-white\" />\n                        </div>\n                        <div>\n                            <h1 className=\"text-2xl font-bold tracking-tight\">Custom Domains</h1>\n                            <p className=\"text-sm text-muted-foreground\">\n                                Connect your own domain to your changelog\n                            </p>\n                        </div>\n                    </div>\n                </div>\n                <div className=\"flex items-center gap-2\">\n                    <Button variant=\"outline\" size=\"sm\" onClick={loadDomains}>\n                        <RefreshCw className=\"w-4 h-4 mr-2\" />\n                        Refresh\n                    </Button>\n                    <Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>\n                        <DialogTrigger asChild>\n                            <Button>\n                                <Plus className=\"w-4 h-4 mr-2\" />\n                                Add Domain\n                            </Button>\n                        </DialogTrigger>\n                        <DialogContent>\n                            <DialogHeader>\n                                <DialogTitle>Add Custom Domain</DialogTitle>\n                                <DialogDescription>\n                                    Connect your own domain to host your changelog\n                                </DialogDescription>\n                            </DialogHeader>\n                            <form onSubmit={handleAddDomain} className=\"space-y-4\">\n                                <div>\n                                    <Label htmlFor=\"domain\">Domain Name</Label>\n                                    <Input\n                                        id=\"domain\"\n                                        type=\"text\"\n                                        value={newDomain}\n                                        onChange={(e) => setNewDomain(e.target.value)}\n                                        placeholder=\"changelog.example.com\"\n                                        required\n                                    />\n                                    <p className=\"text-xs text-muted-foreground mt-2\">\n                                        Enter your custom domain or subdomain\n                                    </p>\n                                </div>\n                                <Button type=\"submit\" disabled={isAddingDomain} className=\"w-full\">\n                                    {isAddingDomain ? (\n                                        <>\n                                            <RefreshCw className=\"w-4 h-4 mr-2 animate-spin\" />\n                                            Adding...\n                                        </>\n                                    ) : (\n                                        <>\n                                            <Sparkles className=\"w-4 h-4 mr-2\" />\n                                            Add Domain\n                                        </>\n                                    )}\n                                </Button>\n                            </form>\n                        </DialogContent>\n                    </Dialog>\n                </div>\n            </div>\n\n            {/* Alerts */}\n            <AnimatePresence>\n                {error && (\n                    <motion.div\n                        initial={{ opacity: 0, y: -10 }}\n                        animate={{ opacity: 1, y: 0 }}\n                        exit={{ opacity: 0, y: -10 }}\n                    >\n                        <Alert variant=\"destructive\">\n                            <AlertDescription>{error}</AlertDescription>\n                        </Alert>\n                    </motion.div>\n                )}\n                {success && (\n                    <motion.div\n                        initial={{ opacity: 0, y: -10 }}\n                        animate={{ opacity: 1, y: 0 }}\n                        exit={{ opacity: 0, y: -10 }}\n                    >\n                        <Alert variant=\"success\">\n                            <AlertDescription>{success}</AlertDescription>\n                        </Alert>\n                    </motion.div>\n                )}\n            </AnimatePresence>\n\n            {/* Domains List */}\n            {domains.length === 0 ? (\n                <Card>\n                    <CardContent className=\"flex flex-col items-center justify-center py-12\">\n                        <div className=\"w-16 h-16 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center mb-4\">\n                            <Globe className=\"w-8 h-8 text-white\" />\n                        </div>\n                        <h3 className=\"text-lg font-semibold mb-2\">No Custom Domains</h3>\n                        <p className=\"text-sm text-muted-foreground mb-6 text-center max-w-md\">\n                            Connect your own domain to host your changelog on a custom URL\n                        </p>\n                        <Button onClick={() => setShowAddDialog(true)}>\n                            <Plus className=\"w-4 h-4 mr-2\" />\n                            Add Your First Domain\n                        </Button>\n                    </CardContent>\n                </Card>\n            ) : (\n                <div className=\"grid gap-4\">\n                    {domains.map((domain) => (\n                        <DomainCard\n                            key={domain.id}\n                            domain={domain}\n                            projectId={projectId}\n                            sslEnabled={sslEnabled}\n                            onUpdate={loadDomains}\n                            onDelete={handleDeleteDomain}\n                            onError={setError}\n                            onSuccess={setSuccess}\n                        />\n                    ))}\n                </div>\n            )}\n        </div>\n    )\n}\n"
  },
  {
    "path": "app/dashboard/projects/[projectId]/domains/page.tsx.backup",
    "content": "'use client'\n\nimport {useState, useEffect, useCallback} from 'react'\nimport {useTimezone} from '@/hooks/use-timezone'\nimport {use} from 'react'\nimport {Button} from '@/components/ui/button'\nimport {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'\nimport {Badge} from '@/components/ui/badge'\nimport {Input} from '@/components/ui/input'\nimport {Label} from '@/components/ui/label'\nimport {Alert, AlertDescription} from '@/components/ui/alert'\nimport {Progress} from '@/components/ui/progress'\nimport {\n    Trash2,\n    RefreshCw,\n    ExternalLink,\n    Globe,\n    Copy,\n    CheckCircle,\n    Clock,\n    Zap,\n    Shield,\n    Sparkles,\n    ArrowRight,\n    Link,\n    Settings,\n    Rocket,\n    Check,\n    ChevronDown,\n    ChevronUp,\n    MoreHorizontal\n} from 'lucide-react'\nimport {motion, AnimatePresence} from 'framer-motion'\nimport {SSLCertificateCard} from './components/SSLCertificateCard'\nimport type {CustomDomain, DomainCertificate, DNSInstructions} from '@/lib/types/custom-domains'\n\ninterface ProjectDomainSettingsProps {\n    params: Promise<{\n        projectId: string\n    }>\n}\n\n// Confetti component\nconst ConfettiAnimation = ({show}: { show: boolean }) => {\n    if (!show) return null\n\n    const confettiPieces = Array.from({length: 50}, (_, i) => (\n        <motion.div\n            key={i}\n            className=\"absolute w-2 h-2 rounded-full\"\n            style={{\n                backgroundColor: ['#ef4444', '#f97316', '#eab308', '#22c55e', '#3b82f6', '#8b5cf6', '#ec4899'][i % 7],\n                left: `${Math.random() * 100}%`,\n                top: '-10px'\n            }}\n            initial={{y: -10, rotate: 0, scale: 0}}\n            animate={{\n                y: window.innerHeight + 100,\n                rotate: Math.random() * 360,\n                scale: [0, 1, 0.8, 0],\n                x: Math.random() * 200 - 100\n            }}\n            transition={{\n                duration: 3 + Math.random() * 2,\n                ease: \"easeOut\",\n                delay: Math.random() * 0.5\n            }}\n        />\n    ))\n\n    return (\n        <div className=\"fixed inset-0 pointer-events-none z-50 overflow-hidden\">\n            {confettiPieces}\n        </div>\n    )\n}\n\n// Mobile-optimized DNS Record component\nconst DNSRecord = ({type, data, onCopy}: {\n    type: 'CNAME' | 'TXT'\n    data: { name: string; value: string }\n    onCopy: (text: string) => void\n}) => {\n    const [isExpanded, setIsExpanded] = useState(false)\n\n    return (\n        <Card className=\"bg-background border\">\n            <CardContent className=\"p-3 sm:p-4\">\n                {/* Mobile Header */}\n                <div className=\"flex items-center justify-between mb-3 sm:hidden\">\n                    <div className=\"flex items-center gap-2\">\n                        <div\n                            className=\"w-6 h-6 bg-primary rounded-full flex items-center justify-center text-xs font-bold text-primary-foreground\">\n                            {type === 'CNAME' ? '1' : '2'}\n                        </div>\n                        <h4 className=\"font-semibold text-sm\">{type} Record</h4>\n                    </div>\n                    <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        onClick={() => setIsExpanded(!isExpanded)}\n                        className=\"p-1\"\n                    >\n                        {isExpanded ? <ChevronUp className=\"w-4 h-4\"/> : <ChevronDown className=\"w-4 h-4\"/>}\n                    </Button>\n                </div>\n\n                {/* Desktop/Expanded Mobile Content */}\n                <div className={`space-y-3 ${!isExpanded ? 'hidden sm:block' : 'block'}`}>\n                    <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-2 sm:gap-3 text-sm\">\n                        <div className=\"space-y-1\">\n                            <span className=\"text-muted-foreground text-xs uppercase tracking-wide\">Type</span>\n                            <div className=\"font-mono bg-muted px-2 py-1 rounded text-xs\">{type}</div>\n                        </div>\n                        {type === 'CNAME' && (\n                            <div className=\"space-y-1\">\n                                <span className=\"text-muted-foreground text-xs uppercase tracking-wide\">TTL</span>\n                                <div className=\"font-mono bg-muted px-2 py-1 rounded text-xs\">3600</div>\n                            </div>\n                        )}\n                        <div className=\"space-y-1 col-span-2 sm:col-span-1\">\n                            <span className=\"text-muted-foreground text-xs uppercase tracking-wide\">Name</span>\n                            <div className=\"font-mono bg-muted px-2 py-1 rounded break-all text-xs\">{data.name}</div>\n                        </div>\n                        <div className=\"space-y-1 col-span-2 sm:col-span-1\">\n                            <span className=\"text-muted-foreground text-xs uppercase tracking-wide\">Value</span>\n                            <div className=\"font-mono bg-muted px-2 py-1 rounded break-all text-xs\">{data.value}</div>\n                        </div>\n                    </div>\n                    <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        className=\"gap-2 w-full sm:w-auto\"\n                        onClick={() => onCopy(`${data.name} ${type} ${data.value}`)}\n                    >\n                        <Copy className=\"w-3 h-3\"/>\n                        Copy Record\n                    </Button>\n                </div>\n\n                {/* Mobile Summary */}\n                {!isExpanded && (\n                    <div className=\"sm:hidden space-y-2\">\n                        <div className=\"text-xs text-muted-foreground truncate\">\n                            <span className=\"font-mono\">{data.name}</span>\n                        </div>\n                        <Button\n                            variant=\"ghost\"\n                            size=\"sm\"\n                            className=\"gap-2 w-full\"\n                            onClick={() => onCopy(`${data.name} ${type} ${data.value}`)}\n                        >\n                            <Copy className=\"w-3 h-3\"/>\n                            Copy\n                        </Button>\n                    </div>\n                )}\n            </CardContent>\n        </Card>\n    )\n}\n\nexport default function ProjectDomainSettings({params}: ProjectDomainSettingsProps) {\n    const {projectId} = use(params)\n    const timezone = useTimezone()\n    const [domains, setDomains] = useState<CustomDomain[]>([])\n    const [isLoading, setIsLoading] = useState(true)\n    const [isAddingDomain, setIsAddingDomain] = useState(false)\n    const [newDomain, setNewDomain] = useState('')\n    const [dnsInstructions, setDnsInstructions] = useState<DNSInstructions | null>(null)\n    const [verifyingDomain, setVerifyingDomain] = useState<string | null>(null)\n    const [error, setError] = useState<string | null>(null)\n    const [success, setSuccess] = useState<string | null>(null)\n    const [showConfetti, setShowConfetti] = useState(false)\n    const [expandedDomains, setExpandedDomains] = useState<Set<string>>(new Set())\n\n    useEffect(() => {\n        loadDomains()\n    }, [projectId])\n\n    const triggerConfetti = useCallback(() => {\n        setShowConfetti(true)\n        setTimeout(() => setShowConfetti(false), 4000)\n    }, [])\n\n    const toggleDomainExpansion = (domainId: string) => {\n        const newExpanded = new Set(expandedDomains)\n        if (newExpanded.has(domainId)) {\n            newExpanded.delete(domainId)\n        } else {\n            newExpanded.add(domainId)\n        }\n        setExpandedDomains(newExpanded)\n    }\n\n    const loadDomains = async (): Promise<void> => {\n        try {\n            setIsLoading(true)\n            setError(null)\n            const response = await fetch(`/api/custom-domains/list?projectId=${projectId}`)\n            const result = await response.json()\n\n            if (result.success) {\n                setDomains(result.domains || [])\n            } else {\n                setError(result.error || 'Failed to load domains')\n            }\n        } catch (error) {\n            setError('Failed to load domains')\n            console.error('Failed to load domains:', error)\n        } finally {\n            setIsLoading(false)\n        }\n    }\n\n    const handleAddDomain = async (e: React.FormEvent): Promise<void> => {\n        e.preventDefault()\n        if (!newDomain) return\n\n        setIsAddingDomain(true)\n        setError(null)\n\n        try {\n            const response = await fetch('/api/custom-domains/add', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({\n                    domain: newDomain,\n                    projectId: projectId\n                })\n            })\n\n            const result = await response.json()\n            if (result.success) {\n                setNewDomain('')\n                setDnsInstructions(result.domain.dnsInstructions)\n                setSuccess(`Domain ${newDomain} added successfully! Follow the DNS instructions below.`)\n                await loadDomains()\n            } else {\n                setError(result.error || 'Failed to add domain')\n            }\n        } catch (error) {\n            setError('Failed to add domain')\n            console.error('Failed to add domain:', error)\n        } finally {\n            setIsAddingDomain(false)\n        }\n    }\n\n    const handleVerifyDomain = async (domain: string): Promise<void> => {\n        setVerifyingDomain(domain)\n        setError(null)\n\n        try {\n            const response = await fetch('/api/custom-domains/verify', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({domain})\n            })\n\n            const result = await response.json()\n            if (result.success) {\n                await loadDomains()\n                if (result.verification.verified) {\n                    setSuccess(`🎉 Domain ${domain} verified successfully! Your changelog is now live.`)\n                    triggerConfetti()\n                } else {\n                    setError(`Verification failed: ${result.verification.errors?.join(', ') || 'DNS records not found'}`)\n                }\n            } else {\n                setError(result.error || 'Verification failed')\n            }\n        } catch (error) {\n            setError('Failed to verify domain')\n            console.error('Failed to verify domain:', error)\n        } finally {\n            setVerifyingDomain(null)\n        }\n    }\n\n    const handleDeleteDomain = async (domain: string): Promise<void> => {\n        if (!confirm(`Remove ${domain} from this project? This action cannot be undone.`)) {\n            return\n        }\n\n        setError(null)\n\n        try {\n            const response = await fetch(`/api/custom-domains/${encodeURIComponent(domain)}`, {\n                method: 'DELETE'\n            })\n\n            const result = await response.json()\n            if (result.success) {\n                setSuccess(`Domain ${domain} removed successfully`)\n                await loadDomains()\n            } else {\n                setError(result.error || 'Failed to delete domain')\n            }\n        } catch (error) {\n            setError('Failed to delete domain')\n            console.error('Failed to delete domain:', error)\n        }\n    }\n\n    const copyToClipboard = async (text: string): Promise<void> => {\n        try {\n            await navigator.clipboard.writeText(text)\n            setSuccess('Copied to clipboard!')\n        } catch {\n            setError('Failed to copy to clipboard')\n        }\n    }\n\n    const formatDate = (date: Date | string): string => {\n        return new Date(date).toLocaleDateString('en-US', {\n            year: 'numeric',\n            month: 'short',\n            day: 'numeric',\n            timeZone: timezone,\n        })\n    }\n\n    const getVerificationProgress = (domain: CustomDomain): number => {\n        if (domain.verified) return 100\n        return 40 // DNS setup initiated\n    }\n\n    useEffect(() => {\n        if (success || error) {\n            const timer = setTimeout(() => {\n                setSuccess(null)\n                setError(null)\n            }, 5000)\n            return () => clearTimeout(timer)\n        }\n    }, [success, error])\n\n    if (isLoading) {\n        return (\n            <div className=\"max-w-4xl mx-auto p-4 sm:p-6\">\n                <div className=\"flex items-center justify-center min-h-[40vh]\">\n                    <div className=\"text-center space-y-4\">\n                        <div className=\"w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mx-auto\">\n                            <RefreshCw className=\"w-6 h-6 animate-spin text-primary\"/>\n                        </div>\n                        <div>\n                            <h3 className=\"font-semibold text-foreground\">Loading domain settings</h3>\n                            <p className=\"text-sm text-muted-foreground px-4\">Setting up your custom domain\n                                configuration...</p>\n                        </div>\n                    </div>\n                </div>\n            </div>\n        )\n    }\n\n    return (\n        <div className=\"max-w-4xl mx-auto p-4 sm:p-6 space-y-6 sm:space-y-8\">\n            <ConfettiAnimation show={showConfetti}/>\n\n            {/* Enhanced Header */}\n            <motion.div\n                initial={{opacity: 0, y: 20}}\n                animate={{opacity: 1, y: 0}}\n                className=\"space-y-2\"\n            >\n                <div className=\"flex flex-col sm:flex-row sm:items-center gap-3\">\n                    <div\n                        className=\"w-10 h-10 bg-gradient-to-br from-primary to-primary/70 rounded-xl flex items-center justify-center\">\n                        <Globe className=\"w-5 h-5 text-primary-foreground\"/>\n                    </div>\n                    <div className=\"min-w-0 flex-1\">\n                        <h1 className=\"text-2xl sm:text-3xl font-bold tracking-tight text-foreground\">Custom\n                            Domains</h1>\n                        <p className=\"text-sm sm:text-base text-muted-foreground\">\n                            Connect your own domain to create a professional changelog experience\n                        </p>\n                    </div>\n                </div>\n            </motion.div>\n\n            {/* Alerts */}\n            <AnimatePresence>\n                {error && (\n                    <motion.div\n                        initial={{opacity: 0, scale: 0.95, y: -10}}\n                        animate={{opacity: 1, scale: 1, y: 0}}\n                        exit={{opacity: 0, scale: 0.95, y: -10}}\n                    >\n                        <Alert variant=\"destructive\" className=\"border-destructive/20\">\n                            <AlertDescription className=\"font-medium text-sm\">{error}</AlertDescription>\n                        </Alert>\n                    </motion.div>\n                )}\n                {success && (\n                    <motion.div\n                        initial={{opacity: 0, scale: 0.95, y: -10}}\n                        animate={{opacity: 1, scale: 1, y: 0}}\n                        exit={{opacity: 0, scale: 0.95, y: -10}}\n                    >\n                        <Alert variant=\"success\">\n                            <AlertDescription className=\"text-green-800 dark:text-green-200 font-medium text-sm\">\n                                {success}\n                            </AlertDescription>\n                        </Alert>\n                    </motion.div>\n                )}\n            </AnimatePresence>\n\n            {/* Add Domain Card */}\n            <motion.div\n                initial={{opacity: 0, y: 20}}\n                animate={{opacity: 1, y: 0}}\n                transition={{delay: 0.1}}\n            >\n                <Card\n                    className=\"border-2 border-dashed border-muted-foreground/20 hover:border-primary/30 transition-colors\">\n                    <CardHeader className=\"pb-4\">\n                        <div className=\"flex flex-col sm:flex-row sm:items-center gap-3\">\n                            <div className=\"w-8 h-8 bg-primary/10 rounded-lg flex items-center justify-center\">\n                                <Zap className=\"w-4 h-4 text-primary\"/>\n                            </div>\n                            <div className=\"min-w-0 flex-1\">\n                                <CardTitle className=\"text-lg\">Add Custom Domain</CardTitle>\n                                <p className=\"text-sm text-muted-foreground mt-1\">\n                                    Connect your domain to make your changelog accessible at your URL\n                                </p>\n                            </div>\n                        </div>\n                    </CardHeader>\n                    <CardContent>\n                        <form onSubmit={handleAddDomain} className=\"space-y-4\">\n                            <div className=\"space-y-2\">\n                                <Label htmlFor=\"domain\" className=\"text-sm font-medium\">Domain Name</Label>\n                                <div className=\"relative\">\n                                    <Link\n                                        className=\"absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground\"/>\n                                    <Input\n                                        id=\"domain\"\n                                        type=\"text\"\n                                        value={newDomain}\n                                        onChange={(e) => setNewDomain(e.target.value)}\n                                        placeholder=\"changelog.yourcompany.com\"\n                                        className=\"pl-10\"\n                                        required\n                                    />\n                                </div>\n                                <p className=\"text-xs text-muted-foreground\">\n                                    Use a subdomain like{' '}\n                                    <code className=\"bg-muted px-1 py-0.5 rounded text-xs\">changelog.yoursite.com</code>\n                                    {' '}or{' '}\n                                    <code className=\"bg-muted px-1 py-0.5 rounded text-xs\">updates.yoursite.com</code>\n                                </p>\n                            </div>\n                            <Button type=\"submit\" disabled={isAddingDomain} className=\"w-full\">\n                                {isAddingDomain ? (\n                                    <>\n                                        <RefreshCw className=\"w-4 h-4 mr-2 animate-spin\"/>\n                                        Adding Domain...\n                                    </>\n                                ) : (\n                                    <>\n                                        <Sparkles className=\"w-4 h-4 mr-2\"/>\n                                        Add Domain\n                                    </>\n                                )}\n                            </Button>\n                        </form>\n                    </CardContent>\n                </Card>\n            </motion.div>\n\n            {/* DNS Instructions */}\n            {dnsInstructions && (\n                <motion.div\n                    initial={{opacity: 0, scale: 0.98}}\n                    animate={{opacity: 1, scale: 1}}\n                    className=\"relative\"\n                >\n                    <Card className=\"border-primary/20 bg-primary/5\">\n                        <CardHeader className=\"pb-4\">\n                            <div className=\"flex flex-col sm:flex-row sm:items-center gap-3\">\n                                <div className=\"w-8 h-8 bg-primary/10 rounded-lg flex items-center justify-center\">\n                                    <Shield className=\"w-4 h-4 text-primary\"/>\n                                </div>\n                                <div className=\"min-w-0 flex-1\">\n                                    <CardTitle className=\"text-primary flex flex-col sm:flex-row sm:items-center gap-2\">\n                                        <span>DNS Configuration Required</span>\n                                        <Badge variant=\"secondary\" className=\"text-xs w-fit\">Step 2 of 3</Badge>\n                                    </CardTitle>\n                                    <p className=\"text-sm text-muted-foreground mt-1\">\n                                        Add these DNS records to your domain provider\n                                    </p>\n                                </div>\n                            </div>\n                        </CardHeader>\n                        <CardContent className=\"space-y-6\">\n                            <div className=\"space-y-4\">\n                                {/* CNAME Record */}\n                                <div className=\"space-y-3\">\n                                    <div className=\"hidden sm:flex items-center gap-2\">\n                                        <div\n                                            className=\"w-6 h-6 bg-primary rounded-full flex items-center justify-center text-xs font-bold text-primary-foreground\">\n                                            1\n                                        </div>\n                                        <h4 className=\"font-semibold text-foreground\">CNAME Record</h4>\n                                    </div>\n                                    <DNSRecord\n                                        type=\"CNAME\"\n                                        data={dnsInstructions.cname}\n                                        onCopy={copyToClipboard}\n                                    />\n                                </div>\n\n                                {/* TXT Record */}\n                                <div className=\"space-y-3\">\n                                    <div className=\"hidden sm:flex items-center gap-2\">\n                                        <div\n                                            className=\"w-6 h-6 bg-primary rounded-full flex items-center justify-center text-xs font-bold text-primary-foreground\">\n                                            2\n                                        </div>\n                                        <h4 className=\"font-semibold text-foreground\">TXT Record (Verification)</h4>\n                                    </div>\n                                    <DNSRecord\n                                        type=\"TXT\"\n                                        data={dnsInstructions.txt}\n                                        onCopy={copyToClipboard}\n                                    />\n                                </div>\n                            </div>\n\n                            <Alert className=\"border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950\">\n                                <AlertDescription className=\"text-blue-800 dark:text-blue-200 text-sm\">\n                                    DNS changes can take up to 48 hours to propagate. You can verify your setup using\n                                    the &ldquo;Verify&rdquo; button once the records are added.\n                                </AlertDescription>\n                            </Alert>\n\n                            <div className=\"flex flex-col gap-3\">\n                                <Button variant=\"outline\" onClick={() => setDnsInstructions(null)}\n                                        className=\"gap-2 w-full sm:w-auto\">\n                                    <Check className=\"w-4 h-4\"/>\n                                    I&apos;ve Added These Records\n                                </Button>\n                                <Button variant=\"ghost\" size=\"sm\" asChild className=\"w-full sm:w-auto\">\n                                    <a\n                                        href=\"https://www.cloudways.com/blog/dns-record/\"\n                                        target=\"_blank\"\n                                        rel=\"noopener noreferrer\"\n                                        className=\"gap-2\"\n                                    >\n                                        <ExternalLink className=\"w-4 h-4\" />\n                                        DNS Setup Guide\n                                    </a>\n                                </Button>\n\n                            </div>\n                        </CardContent>\n                    </Card>\n                </motion.div>\n            )}\n\n            {/* Current Domains */}\n            <motion.div\n                initial={{opacity: 0, y: 20}}\n                animate={{opacity: 1, y: 0}}\n                transition={{delay: 0.2}}\n            >\n                <Card>\n                    <CardHeader className=\"pb-4\">\n                        <div className=\"flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3\">\n                            <div className=\"flex items-center gap-3\">\n                                <div className=\"w-8 h-8 bg-muted rounded-lg flex items-center justify-center\">\n                                    <Settings className=\"w-4 h-4 text-muted-foreground\"/>\n                                </div>\n                                <div className=\"min-w-0 flex-1\">\n                                    <CardTitle className=\"text-lg\">Your Domains</CardTitle>\n                                    <p className=\"text-sm text-muted-foreground\">\n                                        Manage your connected custom domains\n                                    </p>\n                                </div>\n                            </div>\n                            <Button onClick={loadDomains} variant=\"outline\" size=\"sm\"\n                                    className=\"gap-2 w-full sm:w-auto\">\n                                <RefreshCw className=\"w-4 h-4\"/>\n                                Refresh\n                            </Button>\n                        </div>\n                    </CardHeader>\n                    <CardContent>\n                        {domains.length === 0 ? (\n                            <div className=\"text-center py-12\">\n                                <div\n                                    className=\"w-16 h-16 bg-muted rounded-full flex items-center justify-center mx-auto mb-4\">\n                                    <Globe className=\"w-8 h-8 text-muted-foreground\"/>\n                                </div>\n                                <h3 className=\"text-lg font-semibold text-foreground mb-2\">No domains connected yet</h3>\n                                <p className=\"text-muted-foreground text-sm max-w-md mx-auto px-4\">\n                                    Add your first custom domain to create a professional changelog experience for your\n                                    users.\n                                </p>\n                            </div>\n                        ) : (\n                            <div className=\"space-y-4\">\n                                {domains.map((domain, index) => (\n                                    <motion.div\n                                        key={domain.id}\n                                        layout\n                                        initial={{opacity: 0, y: 20}}\n                                        animate={{opacity: 1, y: 0}}\n                                        transition={{delay: index * 0.1}}\n                                        className=\"border border-border rounded-xl p-4 sm:p-6 hover:bg-muted/30 transition-all duration-200\"\n                                    >\n                                        {/* Mobile-first domain header */}\n                                        <div className=\"space-y-4\">\n                                            {/* Top row - always visible */}\n                                            <div className=\"flex items-start justify-between gap-3\">\n                                                <div className=\"min-w-0 flex-1\">\n                                                    <div className=\"flex items-center gap-2 mb-2\">\n                                                        <Rocket\n                                                            className=\"w-4 h-4 text-muted-foreground flex-shrink-0\"/>\n                                                        <h3 className=\"font-semibold text-lg text-foreground truncate\">{domain.domain}</h3>\n                                                    </div>\n                                                    <Badge variant={domain.verified ? \"default\" : \"secondary\"}\n                                                           className=\"gap-1 text-xs\">\n                                                        {domain.verified ? (\n                                                            <>\n                                                                <CheckCircle className=\"w-3 h-3\"/>\n                                                                Live & Active\n                                                            </>\n                                                        ) : (\n                                                            <>\n                                                                <Clock className=\"w-3 h-3\"/>\n                                                                Pending\n                                                            </>\n                                                        )}\n                                                    </Badge>\n                                                </div>\n\n                                                {/* Mobile actions */}\n                                                <div className=\"flex items-center gap-1 sm:hidden\">\n                                                    <Button\n                                                        variant=\"ghost\"\n                                                        size=\"sm\"\n                                                        onClick={() => toggleDomainExpansion(domain.id)}\n                                                        className=\"p-2\"\n                                                    >\n                                                        <MoreHorizontal className=\"w-4 h-4\"/>\n                                                    </Button>\n                                                </div>\n\n                                                {/* Desktop actions */}\n                                                <div className=\"hidden sm:flex items-center gap-2\">\n                                                    {domain.verified && (\n                                                        <Button variant=\"ghost\" size=\"sm\" asChild\n                                                                className=\"gap-2 text-primary\">\n                                                            <a\n                                                                href={`https://${domain.domain}`}\n                                                                target=\"_blank\"\n                                                                rel=\"noopener noreferrer\"\n                                                            >\n                                                                <ExternalLink className=\"w-4 h-4\"/>\n                                                                Visit\n                                                            </a>\n                                                        </Button>\n                                                    )}\n                                                    {!domain.verified && (\n                                                        <Button\n                                                            onClick={() => handleVerifyDomain(domain.domain)}\n                                                            disabled={verifyingDomain === domain.domain}\n                                                            size=\"sm\"\n                                                            className=\"gap-2\"\n                                                        >\n                                                            {verifyingDomain === domain.domain ? (\n                                                                <>\n                                                                    <RefreshCw className=\"w-4 h-4 animate-spin\"/>\n                                                                    Verifying...\n                                                                </>\n                                                            ) : (\n                                                                <>\n                                                                    <CheckCircle className=\"w-4 h-4\"/>\n                                                                    Verify\n                                                                </>\n                                                            )}\n                                                        </Button>\n                                                    )}\n                                                    <Button\n                                                        variant=\"ghost\"\n                                                        size=\"sm\"\n                                                        onClick={() => handleDeleteDomain(domain.domain)}\n                                                        className=\"text-destructive hover:text-destructive hover:bg-destructive/10 p-2\"\n                                                    >\n                                                        <Trash2 className=\"w-4 h-4\"/>\n                                                    </Button>\n                                                </div>\n                                            </div>\n\n                                            {/* Progress - visible on mobile and desktop */}\n                                            <div className=\"space-y-2\">\n                                                <div className=\"flex items-center justify-between text-sm\">\n                                                    <span\n                                                        className=\"text-muted-foreground font-medium\">Setup Progress</span>\n                                                    <span\n                                                        className=\"text-muted-foreground\">{getVerificationProgress(domain)}%</span>\n                                                </div>\n                                                <Progress value={getVerificationProgress(domain)} className=\"h-2\"/>\n                                            </div>\n\n                                            {/* Expandable content for mobile */}\n                                            <AnimatePresence>\n                                                {(expandedDomains.has(domain.id) || window.innerWidth >= 640) && (\n                                                    <motion.div\n                                                        initial={{opacity: 0, height: 0}}\n                                                        animate={{opacity: 1, height: 'auto'}}\n                                                        exit={{opacity: 0, height: 0}}\n                                                        transition={{duration: 0.2}}\n                                                        className=\"space-y-4 sm:space-y-0\"\n                                                    >\n                                                        {/* Metadata */}\n                                                        <div\n                                                            className=\"flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-6 text-sm text-muted-foreground\">\n                                                            <div className=\"flex items-center gap-2\">\n                                                                <Clock className=\"w-4 h-4\"/>\n                                                                <span>Added {formatDate(domain.createdAt)}</span>\n                                                            </div>\n                                                            {domain.verifiedAt && (\n                                                                <div className=\"flex items-center gap-2\">\n                                                                    <CheckCircle className=\"w-4 h-4 text-green-600\"/>\n                                                                    <span>Verified {formatDate(domain.verifiedAt)}</span>\n                                                                </div>\n                                                            )}\n                                                        </div>\n\n                                                        {/* Mobile actions */}\n                                                        <div className=\"flex flex-col gap-3 sm:hidden\">\n                                                            {domain.verified && (\n                                                                <Button variant=\"outline\" size=\"sm\" asChild\n                                                                        className=\"gap-2 w-full\">\n                                                                    <a\n                                                                        href={`https://${domain.domain}`}\n                                                                        target=\"_blank\"\n                                                                        rel=\"noopener noreferrer\"\n                                                                    >\n                                                                        <ExternalLink className=\"w-4 h-4\"/>\n                                                                        Visit Site\n                                                                    </a>\n                                                                </Button>\n                                                            )}\n                                                            {!domain.verified && (\n                                                                <Button\n                                                                    onClick={() => handleVerifyDomain(domain.domain)}\n                                                                    disabled={verifyingDomain === domain.domain}\n                                                                    size=\"sm\"\n                                                                    className=\"gap-2 w-full\"\n                                                                >\n                                                                    {verifyingDomain === domain.domain ? (\n                                                                        <>\n                                                                            <RefreshCw\n                                                                                className=\"w-4 h-4 animate-spin\"/>\n                                                                            Verifying...\n                                                                        </>\n                                                                    ) : (\n                                                                        <>\n                                                                            <CheckCircle className=\"w-4 h-4\"/>\n                                                                            Verify Domain\n                                                                        </>\n                                                                    )}\n                                                                </Button>\n                                                            )}\n                                                            <Button\n                                                                variant=\"outline\"\n                                                                size=\"sm\"\n                                                                onClick={() => handleDeleteDomain(domain.domain)}\n                                                                className=\"text-destructive hover:text-destructive hover:bg-destructive/10 gap-2 w-full\"\n                                                            >\n                                                                <Trash2 className=\"w-4 h-4\"/>\n                                                                Remove Domain\n                                                            </Button>\n                                                        </div>\n                                                    </motion.div>\n                                                )}\n                                            </AnimatePresence>\n\n                                            {/* Status Messages */}\n                                            <div className=\"mt-4 space-y-4\">\n                                                {!domain.verified ? (\n                                                    <Alert hasIcon={false}\n                                                        className=\"border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950\">\n                                                        <ArrowRight\n                                                            className=\"h-4 w-4 text-amber-600 dark:text-amber-400\"/>\n                                                        <AlertDescription\n                                                            className=\"text-amber-800 dark:text-amber-200 text-sm\">\n                                                            <strong>Next step:</strong> Add the DNS records shown above,\n                                                            then click &ldquo;Verify Domain&rdquo; to complete setup.\n                                                            If you can&apos;t see the DNS records, restart setup by\n                                                            deleting this domain.\n                                                        </AlertDescription>\n                                                    </Alert>\n                                                ) : (\n                                                    <>\n                                                        <Alert hasIcon={false}\n                                                            className=\"border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950\">\n                                                            <AlertDescription\n                                                                className=\"text-green-800 dark:text-green-200 text-sm\">\n                                                                <strong>🎉 Congratulations!</strong> Your changelog is live\n                                                                at{' '}\n                                                                <code\n                                                                    className=\"bg-green-100 dark:bg-green-900 px-1 py-0.5 rounded text-xs break-all\">\n                                                                    {domain.domain}\n                                                                </code>\n                                                            </AlertDescription>\n                                                        </Alert>\n\n                                                        {/* SSL Certificate Management */}\n                                                        {process.env.NEXT_PUBLIC_SSL_ENABLED === 'true' && (\n                                                            <SSLCertificateCard\n                                                                domain={domain}\n                                                                onUpdate={loadDomains}\n                                                                onError={setError}\n                                                                onSuccess={setSuccess}\n                                                            />\n                                                        )}\n                                                    </>\n                                                )}\n                                            </div>\n                                        </div>\n                                    </motion.div>\n                                ))}\n                            </div>\n                        )}\n                    </CardContent>\n                </Card>\n            </motion.div>\n        </div>\n    )\n}"
  },
  {
    "path": "app/dashboard/projects/[projectId]/import/page.tsx",
    "content": "// app/dashboard/projects/[projectId]/import/page.tsx\n\n'use client';\n\nimport {use, useState} from 'react';\nimport {useQuery, useQueryClient} from '@tanstack/react-query';\nimport {motion} from 'framer-motion';\nimport Link from 'next/link';\nimport {useRouter} from 'next/navigation';\nimport {\n    CheckCircle,\n    Clock,\n    FileText,\n    Upload,\n} from 'lucide-react';\n\nimport {\n    Avatar,\n    AvatarFallback\n} from '@/components/ui/avatar';\nimport {Badge} from '@/components/ui/badge';\nimport {Button} from '@/components/ui/button';\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card';\nimport {useToast} from '@/hooks/use-toast';\n\nimport {ChangelogImportModal} from '@/components/projects/importing/ChangelogImportModal';\nimport {ImportResult} from '@/lib/types/projects/importing';\n\ninterface Project {\n    id: string;\n    name: string;\n    isPublic: boolean;\n    changelog?: {\n        id: string;\n        entries: Array<{\n            id: string;\n            title: string;\n            version?: string;\n            publishedAt?: string;\n            createdAt: string;\n        }>;\n    };\n    createdAt: string;\n    updatedAt: string;\n}\n\ninterface ImportPageProps {\n    params: Promise<{ projectId: string }>;\n}\n\nconst fadeIn = {\n    initial: {opacity: 0, y: 20},\n    animate: {opacity: 1, y: 0},\n    transition: {duration: 0.5}\n};\n\nexport default function ImportPage({params}: ImportPageProps) {\n    const {projectId} = use(params);\n    const router = useRouter();\n    const queryClient = useQueryClient();\n    const {toast} = useToast();\n    const [showImportModal, setShowImportModal] = useState(false);\n\n    // Fetch project data\n    const {data: project, isLoading: isLoadingProject} = useQuery<Project>({\n        queryKey: ['project', projectId],\n        queryFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}`);\n            if (!response.ok) throw new Error('Failed to fetch project');\n            return response.json();\n        }\n    });\n\n    const handleImportComplete = (result: ImportResult) => {\n        setShowImportModal(false);\n\n        toast({\n            title: 'Import completed!',\n            description: `Successfully imported ${result.importedCount} entries.`,\n        });\n\n        // Refresh project data and redirect to changelog\n        queryClient.invalidateQueries({queryKey: ['project', projectId]});\n        router.push(`/dashboard/projects/${projectId}/changelog`);\n    };\n\n    if (isLoadingProject) {\n        return (\n            <div className=\"container max-w-4xl space-y-6 p-4 md:p-8\">\n                <div className=\"space-y-4\">\n                    <div className=\"h-16 bg-muted rounded-lg animate-pulse\"/>\n                    <div className=\"h-64 bg-muted rounded-lg animate-pulse\"/>\n                </div>\n            </div>\n        );\n    }\n\n    if (!project) {\n        return (\n            <div className=\"container max-w-4xl\">\n                <div className=\"text-center py-12\">\n                    <FileText className=\"h-16 w-16 text-muted-foreground mx-auto mb-4\"/>\n                    <h2 className=\"text-2xl font-semibold mb-2\">Project Not Found</h2>\n                    <p className=\"text-muted-foreground mb-6\">\n                        The project you&apos;re looking for doesn&apos;t exist or you don&apos;t have access.\n                    </p>\n                    <Button asChild>\n                        <Link href=\"/dashboard/projects\">Back to Projects</Link>\n                    </Button>\n                </div>\n            </div>\n        );\n    }\n\n    const currentEntryCount = project.changelog?.entries.length || 0;\n    const publishedCount = project.changelog?.entries.filter(e => e.publishedAt).length || 0;\n    const draftCount = currentEntryCount - publishedCount;\n\n    return (\n        <>\n            <div className=\"container items-center justify-center space-y-8 p-4 md:p-8 min-h-[calc(100vh-4rem)]\">\n                {/* Project Header */}\n                <motion.div\n                    initial=\"initial\"\n                    animate=\"animate\"\n                    variants={fadeIn}\n                    className=\"flex items-center gap-4\"\n                >\n                    <Avatar className=\"h-12 w-12 rounded-xl\">\n                        <AvatarFallback className=\"rounded-xl text-lg\">\n                            {project.name.substring(0, 1).toUpperCase()}\n                        </AvatarFallback>\n                    </Avatar>\n                    <div>\n                        <h1 className=\"text-2xl font-bold\">{project.name}</h1>\n                        <p className=\"text-muted-foreground\">Import changelog data</p>\n                    </div>\n                </motion.div>\n\n                {/* Current Stats */}\n                <motion.div\n                    initial=\"initial\"\n                    animate=\"animate\"\n                    variants={fadeIn}\n                    className=\"grid gap-4 md:grid-cols-3\"\n                >\n                    <Card>\n                        <CardContent className=\"p-6\">\n                            <div className=\"flex items-center justify-between\">\n                                <div>\n                                    <p className=\"text-sm font-medium text-muted-foreground\">Current Entries</p>\n                                    <p className=\"text-2xl font-bold\">{currentEntryCount}</p>\n                                </div>\n                                <FileText className=\"h-8 w-8 text-blue-600\"/>\n                            </div>\n                        </CardContent>\n                    </Card>\n\n                    <Card>\n                        <CardContent className=\"p-6\">\n                            <div className=\"flex items-center justify-between\">\n                                <div>\n                                    <p className=\"text-sm font-medium text-muted-foreground\">Published</p>\n                                    <p className=\"text-2xl font-bold\">{publishedCount}</p>\n                                </div>\n                                <CheckCircle className=\"h-8 w-8 text-green-600\"/>\n                            </div>\n                        </CardContent>\n                    </Card>\n\n                    <Card>\n                        <CardContent className=\"p-6\">\n                            <div className=\"flex items-center justify-between\">\n                                <div>\n                                    <p className=\"text-sm font-medium text-muted-foreground\">Drafts</p>\n                                    <p className=\"text-2xl font-bold\">{draftCount}</p>\n                                </div>\n                                <Clock className=\"h-8 w-8 text-yellow-600\"/>\n                            </div>\n                        </CardContent>\n                    </Card>\n                </motion.div>\n\n                {/* Main Import Card */}\n                <motion.div\n                    initial=\"initial\"\n                    animate=\"animate\"\n                    variants={fadeIn}\n                >\n                    <Card className=\"border-dashed border-2\">\n                        <CardHeader className=\"text-center\">\n                            <div className=\"mx-auto p-3 bg-primary/10 rounded-full w-fit mb-4\">\n                                <Upload className=\"h-8 w-8 text-primary\"/>\n                            </div>\n                            <CardTitle className=\"text-xl\">Import Changelog Data</CardTitle>\n                            <CardDescription>\n                                Upload your existing changelog files or paste markdown content to add more entries\n                            </CardDescription>\n                        </CardHeader>\n\n                        <CardContent className=\"space-y-6\">\n                            {/* Supported Formats */}\n                            <div className=\"text-center space-y-3\">\n                                <p className=\"font-medium text-sm\">Supported Formats</p>\n                                <div className=\"flex flex-wrap justify-center gap-2\">\n                                    <Badge variant=\"secondary\">Keep a Changelog</Badge>\n                                    <Badge variant=\"secondary\">GitHub Releases</Badge>\n                                    <Badge variant=\"secondary\">Markdown Files</Badge>\n                                    <Badge variant=\"secondary\">Plain Text</Badge>\n                                    <Badge variant=\"secondary\">Canny</Badge>\n                                </div>\n                            </div>\n\n                            {/* Action Button */}\n                            <div className=\"text-center\">\n                                <Button\n                                    onClick={() => setShowImportModal(true)}\n                                    size=\"lg\"\n                                    className=\"px-8\"\n                                >\n                                    <Upload className=\"h-4 w-4 mr-2\"/>\n                                    Start Import\n                                </Button>\n                            </div>\n                        </CardContent>\n                    </Card>\n                </motion.div>\n            </div>\n\n            {/* Import Modal */}\n            <ChangelogImportModal\n                open={showImportModal}\n                onOpenChange={setShowImportModal}\n                projectId={projectId}\n                onImportComplete={handleImportComplete}\n            />\n        </>\n    );\n}"
  },
  {
    "path": "app/dashboard/projects/[projectId]/integrations/email/page.tsx",
    "content": "'use client';\n\nimport {useState, useEffect} from 'react';\nimport {useParams, useRouter} from 'next/navigation';\nimport {useTimezone} from '@/hooks/use-timezone';\nimport {useQuery, useMutation, useQueryClient} from '@tanstack/react-query';\nimport {motion} from 'framer-motion';\nimport {z} from 'zod';\nimport {useForm} from 'react-hook-form';\nimport {zodResolver} from '@hookform/resolvers/zod';\n\n// UI Components\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card';\nimport {\n    Form,\n    FormControl,\n    FormDescription,\n    FormField,\n    FormItem,\n    FormLabel,\n    FormMessage,\n} from '@/components/ui/form';\nimport {Input} from '@/components/ui/input';\nimport {Button} from '@/components/ui/button';\nimport {Switch} from '@/components/ui/switch';\nimport {\n    AlertDialog,\n    AlertDialogAction,\n    AlertDialogCancel,\n    AlertDialogContent,\n    AlertDialogDescription,\n    AlertDialogFooter,\n    AlertDialogHeader,\n    AlertDialogTitle,\n} from '@/components/ui/alert-dialog';\nimport {useToast} from '@/hooks/use-toast';\nimport {Alert, AlertDescription} from '@/components/ui/alert';\nimport {\n    CheckIcon,\n    Loader2Icon,\n    MailIcon,\n    AlertCircleIcon,\n    ArrowLeftIcon,\n    SendIcon,\n    EyeIcon,\n    EyeOffIcon,\n    XIcon,\n    UsersIcon,\n    CheckCircleIcon,\n    PlusIcon,\n    UserPlusIcon,\n} from 'lucide-react';\nimport {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs';\nimport {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from \"@/components/ui/select\";\nimport {Badge} from \"@/components/ui/badge\";\nimport {RadioGroup, RadioGroupItem} from \"@/components/ui/radio-group\";\nimport {RenderMarkdown} from '@/components/MarkdownEditor';\n\n// Define types\ninterface EmailConfig {\n    id?: string;\n    enabled: boolean;\n    smtpHost: string;\n    smtpPort: number;\n    smtpUser: string | null;\n    smtpPassword?: string | null;\n    smtpSecure: boolean;\n    fromEmail: string;\n    fromName: string | null;\n    replyToEmail: string | null;\n    defaultSubject: string | null;\n    lastTestedAt?: Date | null;\n    testStatus?: string | null;\n}\n\ninterface ChangelogEntry {\n    id: string;\n    title: string;\n    content: string;\n    version?: string | null;\n    publishedAt?: Date | null;\n    createdAt: Date;\n    updatedAt: Date;\n    tags?: { id: string; name: string }[];\n}\n\ninterface Project {\n    id: string;\n    name: string;\n    isPublic: boolean;\n    allowAutoPublish: boolean;\n    requireApproval: boolean;\n    defaultTags: string[];\n    createdAt: Date;\n    updatedAt: Date;\n}\n\n// interface Subscriber {\n//     id: string;\n//     email: string;\n//     name?: string;\n//     subscriptionType: 'ALL_UPDATES' | 'MAJOR_ONLY' | 'DIGEST_ONLY';\n//     createdAt: string;\n//     lastEmailSentAt?: string;\n// }\n\n// Form schema\nconst formSchema = z.object({\n    enabled: z.boolean().default(false),\n    smtpHost: z.string().min(1, 'SMTP host is required'),\n    smtpPort: z.coerce.number().int().min(1).max(65535),\n    smtpUser: z.string().optional().nullable(),\n    smtpPassword: z.string().optional().nullable(),\n    smtpSecure: z.boolean().default(false),\n    fromEmail: z.string().email('Invalid email address'),\n    fromName: z.string().optional().nullable(),\n    replyToEmail: z.string().email('Invalid email address').optional().nullable().or(z.literal('')),\n    defaultSubject: z.string().optional().nullable(),\n});\n\ntype FormValues = z.infer<typeof formSchema>;\n\n// Test email form schema\nconst testEmailSchema = z.object({\n    testEmail: z.string().email('Invalid email address'),\n});\n\ntype TestEmailFormValues = z.infer<typeof testEmailSchema>;\n\n// Send email form schema\nconst sendEmailSchema = z.object({\n    recipientType: z.enum(['MANUAL', 'SUBSCRIBERS', 'BOTH']).default('SUBSCRIBERS'),\n    manualRecipients: z.array(z.string().email(\"Invalid email address\")).optional(),\n    subject: z.string().min(1, \"Subject is required\"),\n    changelogEntryId: z.string().optional(),\n    isDigest: z.boolean().default(false),\n    subscriptionTypes: z.array(z.enum(['ALL_UPDATES', 'MAJOR_ONLY', 'DIGEST_ONLY'])).optional(),\n});\n\ntype SendEmailFormValues = z.infer<typeof sendEmailSchema>;\n\nexport default function EmailIntegrationPage() {\n    const params = useParams();\n    const router = useRouter();\n    const {toast} = useToast();\n    const queryClient = useQueryClient();\n    const projectId = params.projectId as string;\n    const timezone = useTimezone();\n\n    // State\n    const [isTestDialogOpen, setIsTestDialogOpen] = useState(false);\n    const [showPassword, setShowPassword] = useState(false);\n    const [activeTab, setActiveTab] = useState('settings');\n    const [manualRecipients, setManualRecipients] = useState<string[]>([]);\n    const [newRecipient, setNewRecipient] = useState('');\n    const [recentEntries, setRecentEntries] = useState<ChangelogEntry[]>([]);\n    const [isLoadingSubscribers, setIsLoadingSubscribers] = useState(false);\n    const [subscriberCount, setSubscriberCount] = useState<number>(0);\n    const [sendingToSubscribers, setSendingToSubscribers] = useState<boolean>(true);\n    const [selectedSubscriptionTypes, setSelectedSubscriptionTypes] = useState<('ALL_UPDATES' | 'MAJOR_ONLY' | 'DIGEST_ONLY')[]>([\n        'ALL_UPDATES', 'MAJOR_ONLY', 'DIGEST_ONLY'\n    ]);\n\n    const getCustomDomain = (): string | null => {\n        if (typeof window === 'undefined') return null;\n\n        const hostname = window.location.hostname;\n        const appUrl = process.env.NEXT_PUBLIC_APP_URL || '';\n\n        try {\n            const appDomain = new URL(appUrl).hostname;\n\n            if (hostname !== appDomain &&\n                !hostname.includes('localhost') &&\n                !hostname.includes('127.0.0.1')) {\n                return hostname;\n            }\n        } catch (error) {\n            console.error('Error parsing app URL:', error);\n        }\n\n        return null;\n    };\n\n    // Fetch email configuration\n    const {data: emailConfig, isLoading} = useQuery<EmailConfig>({\n        queryKey: ['email-config', projectId],\n        queryFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}/integrations/email`);\n            if (!response.ok) throw new Error('Failed to fetch email configuration');\n            return response.json();\n        },\n    });\n\n    // Fetch project info\n    const {data: project} = useQuery<Project>({\n        queryKey: ['project', projectId],\n        queryFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}`);\n            if (!response.ok) throw new Error('Failed to fetch project');\n            return response.json();\n        },\n    });\n\n    // Fetch recent changelog entries\n    const {data: entriesData} = useQuery({\n        queryKey: ['changelog-entries', projectId],\n        queryFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}/changelog?limit=10`);\n            if (!response.ok) throw new Error('Failed to fetch changelog entries');\n            return response.json();\n        },\n    });\n\n    // Get subscriber count\n    const fetchSubscriberCount = async () => {\n        setIsLoadingSubscribers(true);\n        try {\n            const response = await fetch(`/api/subscribers?projectId=${projectId}&limit=1`);\n            if (response.ok) {\n                const data = await response.json();\n                setSubscriberCount(data.totalCount || 0);\n            }\n        } catch (error) {\n            console.error('Failed to fetch subscribers:', error);\n        } finally {\n            setIsLoadingSubscribers(false);\n        }\n    };\n\n    // Update recent entries when data is loaded\n    useEffect(() => {\n        if (entriesData?.entries) {\n            setRecentEntries(entriesData.entries);\n        }\n    }, [entriesData]);\n\n    // Fetch subscribers count on tab change\n    useEffect(() => {\n        if (activeTab === 'send') {\n            fetchSubscriberCount();\n        }\n    }, [activeTab, projectId]);\n\n    // Configure main settings form\n    const form = useForm<FormValues>({\n        resolver: zodResolver(formSchema),\n        defaultValues: {\n            enabled: false,\n            smtpHost: '',\n            smtpPort: 587,\n            smtpUser: '',\n            smtpPassword: '',\n            smtpSecure: false,\n            fromEmail: '',\n            fromName: '',\n            replyToEmail: '',\n            defaultSubject: 'New Changelog Update',\n        },\n    });\n\n    // Test email form\n    const testEmailForm = useForm<TestEmailFormValues>({\n        resolver: zodResolver(testEmailSchema),\n        defaultValues: {\n            testEmail: '',\n        },\n    });\n\n    // Send email form\n    const sendEmailForm = useForm<SendEmailFormValues>({\n        resolver: zodResolver(sendEmailSchema),\n        defaultValues: {\n            recipientType: 'SUBSCRIBERS',\n            manualRecipients: [],\n            subject: project?.name ? `${project.name} - Changelog Update` : 'Changelog Update',\n            changelogEntryId: undefined,\n            isDigest: false,\n            subscriptionTypes: ['ALL_UPDATES', 'MAJOR_ONLY', 'DIGEST_ONLY'],\n        }\n    });\n\n    // Update form when data is loaded\n    useEffect(() => {\n        if (emailConfig) {\n            form.reset({\n                enabled: emailConfig.enabled,\n                smtpHost: emailConfig.smtpHost,\n                smtpPort: emailConfig.smtpPort,\n                smtpUser: emailConfig.smtpUser || '',\n                smtpPassword: '', // Don't populate password\n                smtpSecure: emailConfig.smtpSecure,\n                fromEmail: emailConfig.fromEmail,\n                fromName: emailConfig.fromName || '',\n                replyToEmail: emailConfig.replyToEmail || '',\n                defaultSubject: emailConfig.defaultSubject || 'New Changelog Update',\n            });\n        }\n    }, [emailConfig, form]);\n\n    // Save configuration mutation\n    const saveConfig = useMutation({\n        mutationFn: async (data: FormValues) => {\n            const response = await fetch(`/api/projects/${projectId}/integrations/email`, {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify(data),\n            });\n\n            if (!response.ok) {\n                const error = await response.json();\n                throw new Error(error.error || 'Failed to save email configuration');\n            }\n\n            return response.json();\n        },\n        onSuccess: () => {\n            toast({\n                title: 'Settings Saved',\n                description: 'Email configuration has been updated successfully.',\n            });\n            queryClient.invalidateQueries({queryKey: ['email-config', projectId]});\n        },\n        onError: (error) => {\n            toast({\n                title: 'Error',\n                description: error.message,\n                variant: 'destructive',\n            });\n        },\n    });\n\n    // Test connection mutation\n    const testConnection = useMutation({\n        mutationFn: async (data: TestEmailFormValues & FormValues) => {\n            const response = await fetch(`/api/projects/${projectId}/integrations/email/test`, {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify(data),\n            });\n\n            if (!response.ok) {\n                const error = await response.json();\n                throw new Error(error.error || error.message || 'Failed to test email connection');\n            }\n\n            return response.json();\n        },\n        onSuccess: () => {\n            toast({\n                title: 'Connection Successful',\n                description: 'The test email was sent successfully.',\n            });\n            setIsTestDialogOpen(false);\n            testEmailForm.reset();\n        },\n        onError: (error) => {\n            toast({\n                title: 'Connection Failed',\n                description: error.message,\n                variant: 'destructive',\n            });\n        },\n    });\n\n    // Email sending mutation\n    const sendEmailMutation = useMutation({\n        mutationFn: async (data: SendEmailFormValues) => {\n            const isDigest = data.changelogEntryId === 'digest';\n            // eslint-disable-next-line @typescript-eslint/no-unused-vars\n            const customDomain = getCustomDomain();\n\n            // Prepare recipients based on selection\n            let recipients: string[] = [];\n\n            if (data.recipientType === 'MANUAL' || data.recipientType === 'BOTH') {\n                recipients = [...manualRecipients];\n            }\n\n            // Build the payload\n            const payload = {\n                recipients: data.recipientType !== 'SUBSCRIBERS' ? recipients : undefined,\n                subject: data.subject,\n                isDigest,\n                subscriptionTypes: (data.recipientType === 'SUBSCRIBERS' || data.recipientType === 'BOTH')\n                    ? selectedSubscriptionTypes\n                    : undefined,\n                ...(isDigest ? {} : {changelogEntryId: data.changelogEntryId})\n            };\n\n            const response = await fetch(`/api/projects/${projectId}/integrations/email/send`, {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify(payload)\n            });\n\n            if (!response.ok) {\n                const error = await response.json();\n                throw new Error(error.message || 'Failed to send email');\n            }\n\n            return response.json();\n        },\n        onSuccess: (data) => {\n            toast({\n                title: 'Email Sent',\n                description: `Successfully sent to ${data.recipientCount || 0} recipients.`,\n            });\n            // Reset form\n            setManualRecipients([]);\n            setSelectedSubscriptionTypes(['ALL_UPDATES', 'MAJOR_ONLY', 'DIGEST_ONLY']);\n            sendEmailForm.reset({\n                recipientType: 'SUBSCRIBERS',\n                manualRecipients: [],\n                subject: project?.name ? `${project.name} - Changelog Update` : 'Changelog Update',\n                changelogEntryId: undefined,\n                isDigest: false,\n                subscriptionTypes: ['ALL_UPDATES', 'MAJOR_ONLY', 'DIGEST_ONLY']\n            });\n        },\n        onError: (error) => {\n            toast({\n                title: 'Email Failed',\n                description: error.message,\n                variant: 'destructive',\n            });\n        }\n    });\n\n    // Add/remove recipients\n    const addRecipient = () => {\n        if (!newRecipient) return;\n\n        try {\n            // Validate email\n            const email = z.string().email().parse(newRecipient);\n\n            // Check for duplicates\n            if (!manualRecipients.includes(email)) {\n                setManualRecipients([...manualRecipients, email]);\n                sendEmailForm.setValue('manualRecipients', [...manualRecipients, email]);\n            }\n\n            setNewRecipient('');\n        } catch {\n            // Invalid email format\n            toast({\n                title: 'Invalid Email',\n                description: 'Please enter a valid email address',\n                variant: 'destructive'\n            });\n        }\n    };\n\n    const removeRecipient = (index: number) => {\n        const newRecipients = [...manualRecipients];\n        newRecipients.splice(index, 1);\n        setManualRecipients(newRecipients);\n        sendEmailForm.setValue('manualRecipients', newRecipients);\n    };\n\n    const onSaveSubmit = (values: FormValues) => {\n        saveConfig.mutate(values);\n    };\n\n    const onTestSubmit = (testValues: TestEmailFormValues) => {\n        // Combine form values with test email\n        const currentFormValues = form.getValues();\n\n        testConnection.mutate({\n            ...currentFormValues,\n            ...testValues,\n        });\n    };\n\n    const onSendEmailSubmit = (values: SendEmailFormValues) => {\n        // Make sure we have selected recipients or subscriber types\n        if (values.recipientType === 'MANUAL' && manualRecipients.length === 0) {\n            toast({\n                title: 'No Recipients',\n                description: 'Please add at least one recipient email address',\n                variant: 'destructive'\n            });\n            return;\n        }\n\n        if (values.recipientType === 'SUBSCRIBERS' && selectedSubscriptionTypes.length === 0) {\n            toast({\n                title: 'No Subscribers Selected',\n                description: 'Please select at least one subscription type',\n                variant: 'destructive'\n            });\n            return;\n        }\n\n        if (!values.changelogEntryId) {\n            toast({\n                title: 'No Content Selected',\n                description: 'Please select a changelog entry or digest to send',\n                variant: 'destructive'\n            });\n            return;\n        }\n\n        // Send the email\n        sendEmailMutation.mutate({\n            ...values,\n            manualRecipients: manualRecipients,\n            subscriptionTypes: selectedSubscriptionTypes\n        });\n    };\n\n    // Toggle subscription type selection\n    const toggleSubscriptionType = (type: 'ALL_UPDATES' | 'MAJOR_ONLY' | 'DIGEST_ONLY') => {\n        if (selectedSubscriptionTypes.includes(type)) {\n            // Remove if already selected\n            setSelectedSubscriptionTypes(selectedSubscriptionTypes.filter(t => t !== type));\n        } else {\n            // Add if not selected\n            setSelectedSubscriptionTypes([...selectedSubscriptionTypes, type]);\n        }\n    };\n\n    // Handle recipient type change\n    const handleRecipientTypeChange = (value: 'MANUAL' | 'SUBSCRIBERS' | 'BOTH') => {\n        sendEmailForm.setValue('recipientType', value);\n        setSendingToSubscribers(value === 'SUBSCRIBERS' || value === 'BOTH');\n    };\n\n    if (isLoading) {\n        return (\n            <div className=\"flex items-center justify-center h-96\">\n                <Loader2Icon className=\"h-8 w-8 animate-spin text-primary\"/>\n            </div>\n        );\n    }\n\n    return (\n        <div className=\"container max-w-4xl mx-auto py-6\">\n            <div className=\"flex items-center mb-6\">\n                <Button\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    className=\"gap-1\"\n                    onClick={() => router.push(`/dashboard/projects/${projectId}/settings`)}\n                >\n                    <ArrowLeftIcon className=\"h-4 w-4\"/>\n                    Back to Settings\n                </Button>\n                <h1 className=\"text-2xl font-bold ml-4\">Email Integration</h1>\n\n                <div className=\"ml-auto\">\n                    <Button\n                        variant=\"outline\"\n                        size=\"sm\"\n                        className=\"gap-1\"\n                        onClick={() => router.push(`/dashboard/projects/${projectId}/integrations/email/subscribers`)}\n                    >\n                        <UsersIcon className=\"h-4 w-4\"/>\n                        Manage Subscribers\n                    </Button>\n                </div>\n            </div>\n\n            <motion.div\n                initial={{opacity: 0, y: 20}}\n                animate={{opacity: 1, y: 0}}\n                transition={{duration: 0.3}}\n            >\n                <Tabs value={activeTab} onValueChange={setActiveTab} className=\"w-full\">\n                    <TabsList className=\"mb-6\">\n                        <TabsTrigger value=\"settings\" className=\"flex items-center gap-2\">\n                            <MailIcon className=\"h-4 w-4\"/>\n                            SMTP Settings\n                        </TabsTrigger>\n                        <TabsTrigger value=\"send\" className=\"flex items-center gap-2\">\n                            <SendIcon className=\"h-4 w-4\"/>\n                            Send Emails\n                        </TabsTrigger>\n                    </TabsList>\n\n                    <TabsContent value=\"settings\">\n                        <Card>\n                            <CardHeader>\n                                <div className=\"flex items-center\">\n                                    <MailIcon className=\"h-5 w-5 mr-2 text-primary\"/>\n                                    <CardTitle>Email SMTP Configuration</CardTitle>\n                                </div>\n                                <CardDescription>\n                                    Configure SMTP settings to send changelog updates via email\n                                </CardDescription>\n                            </CardHeader>\n\n                            <CardContent>\n                                <Form {...form}>\n                                    <form onSubmit={form.handleSubmit(onSaveSubmit)} className=\"space-y-6\">\n                                        {/* Enable/Disable Switch */}\n                                        <FormField\n                                            control={form.control}\n                                            name=\"enabled\"\n                                            render={({field}) => (\n                                                <FormItem\n                                                    className=\"flex flex-row items-center justify-between rounded-lg border p-4\">\n                                                    <div className=\"space-y-0.5\">\n                                                        <FormLabel className=\"text-base\">\n                                                            Enable Email Notifications\n                                                        </FormLabel>\n                                                        <FormDescription>\n                                                            Send notifications when new changelog entries are published\n                                                        </FormDescription>\n                                                    </div>\n                                                    <FormControl>\n                                                        <Switch\n                                                            checked={field.value}\n                                                            onCheckedChange={field.onChange}\n                                                        />\n                                                    </FormControl>\n                                                </FormItem>\n                                            )}\n                                        />\n\n                                        {/* SMTP Server Group */}\n                                        <div className=\"border rounded-lg p-4\">\n                                            <h3 className=\"text-lg font-medium mb-4\">SMTP Configuration</h3>\n                                            <div className=\"grid gap-4 sm:grid-cols-2\">\n                                                <FormField\n                                                    control={form.control}\n                                                    name=\"smtpHost\"\n                                                    render={({field}) => (\n                                                        <FormItem>\n                                                            <FormLabel>SMTP Host</FormLabel>\n                                                            <FormControl>\n                                                                <Input placeholder=\"smtp.example.com\" {...field} />\n                                                            </FormControl>\n                                                            <FormMessage/>\n                                                        </FormItem>\n                                                    )}\n                                                />\n\n                                                <FormField\n                                                    control={form.control}\n                                                    name=\"smtpPort\"\n                                                    render={({field}) => (\n                                                        <FormItem>\n                                                            <FormLabel>SMTP Port</FormLabel>\n                                                            <FormControl>\n                                                                <Input type=\"number\" {...field} />\n                                                            </FormControl>\n                                                            <FormMessage/>\n                                                        </FormItem>\n                                                    )}\n                                                />\n\n                                                <FormField\n                                                    control={form.control}\n                                                    name=\"smtpUser\"\n                                                    render={({field}) => (\n                                                        <FormItem>\n                                                            <FormLabel>SMTP Username</FormLabel>\n                                                            <FormControl>\n                                                                <Input placeholder=\"username\" {...field}\n                                                                       value={field.value ?? \"\"}/>\n                                                            </FormControl>\n                                                            <FormMessage/>\n                                                        </FormItem>\n                                                    )}\n                                                />\n\n                                                <FormField\n                                                    control={form.control}\n                                                    name=\"smtpPassword\"\n                                                    render={({field}) => (\n                                                        <FormItem>\n                                                            <FormLabel>SMTP Password</FormLabel>\n                                                            <FormControl>\n                                                                <div className=\"relative\">\n                                                                    <Input\n                                                                        type={showPassword ? \"text\" : \"password\"}\n                                                                        placeholder=\"••••••••\"\n                                                                        {...field}\n                                                                        value={field.value ?? \"\"}\n                                                                    />\n                                                                    <Button\n                                                                        type=\"button\"\n                                                                        variant=\"ghost\"\n                                                                        size=\"sm\"\n                                                                        className=\"absolute right-0 top-0 h-full px-3\"\n                                                                        onClick={() => setShowPassword(!showPassword)}\n                                                                    >\n                                                                        {showPassword ? (\n                                                                            <EyeOffIcon className=\"h-4 w-4\"/>\n                                                                        ) : (\n                                                                            <EyeIcon className=\"h-4 w-4\"/>\n                                                                        )}\n                                                                    </Button>\n                                                                </div>\n                                                            </FormControl>\n                                                            <FormDescription>\n                                                                {emailConfig?.smtpPassword ?\n                                                                    \"Password saved. Leave blank to keep the same password.\" :\n                                                                    \"Enter your SMTP password.\"}\n                                                            </FormDescription>\n                                                            <FormMessage/>\n                                                        </FormItem>\n                                                    )}\n                                                />\n\n                                                <FormField\n                                                    control={form.control}\n                                                    name=\"smtpSecure\"\n                                                    render={({field}) => (\n                                                        <FormItem\n                                                            className=\"flex flex-row items-center justify-between rounded-lg border p-4 col-span-2\">\n                                                            <div className=\"space-y-0.5\">\n                                                                <FormLabel>Use Secure Connection (TLS)</FormLabel>\n                                                                <FormDescription>\n                                                                    Enable TLS encryption for SMTP connection\n                                                                </FormDescription>\n                                                            </div>\n                                                            <FormControl>\n                                                                <Switch\n                                                                    checked={field.value}\n                                                                    onCheckedChange={field.onChange}\n                                                                />\n                                                            </FormControl>\n                                                        </FormItem>\n                                                    )}\n                                                />\n                                            </div>\n                                        </div>\n\n                                        {/* Email Content Group */}\n                                        <div className=\"border rounded-lg p-4\">\n                                            <h3 className=\"text-lg font-medium mb-4\">Email Content</h3>\n                                            <div className=\"grid gap-4 sm:grid-cols-2\">\n                                                <FormField\n                                                    control={form.control}\n                                                    name=\"fromEmail\"\n                                                    render={({field}) => (\n                                                        <FormItem>\n                                                            <FormLabel>From Email</FormLabel>\n                                                            <FormControl>\n                                                                <Input\n                                                                    placeholder=\"changelog@yourcompany.com\" {...field} />\n                                                            </FormControl>\n                                                            <FormMessage/>\n                                                        </FormItem>\n                                                    )}\n                                                />\n\n                                                <FormField\n                                                    control={form.control}\n                                                    name=\"fromName\"\n                                                    render={({field}) => (\n                                                        <FormItem>\n                                                            <FormLabel>From Name</FormLabel>\n                                                            <FormControl>\n                                                                <Input placeholder=\"Your Company Changelog\" {...field}\n                                                                       value={field.value ?? ''}/>\n                                                            </FormControl>\n                                                            <FormMessage/>\n                                                        </FormItem>\n                                                    )}\n                                                />\n\n                                                <FormField\n                                                    control={form.control}\n                                                    name=\"replyToEmail\"\n                                                    render={({field}) => (\n                                                        <FormItem>\n                                                            <FormLabel>Reply-To Email (Optional)</FormLabel>\n                                                            <FormControl>\n                                                                <Input placeholder=\"support@yourcompany.com\" {...field}\n                                                                       value={field.value ?? ''}/>\n                                                            </FormControl>\n                                                            <FormMessage/>\n                                                        </FormItem>\n                                                    )}\n                                                />\n\n                                                <FormField\n                                                    control={form.control}\n                                                    name=\"defaultSubject\"\n                                                    render={({field}) => (\n                                                        <FormItem>\n                                                            <FormLabel>Default Subject</FormLabel>\n                                                            <FormControl>\n                                                                <Input placeholder=\"New Changelog Update\" {...field}\n                                                                       value={field.value || ''}/>\n                                                            </FormControl>\n                                                            <FormMessage/>\n                                                        </FormItem>\n                                                    )}\n                                                />\n                                            </div>\n                                        </div>\n\n                                        <div className=\"flex items-center justify-between pt-4\">\n                                            <Button\n                                                type=\"button\"\n                                                variant=\"outline\"\n                                                onClick={() => setIsTestDialogOpen(true)}\n                                                disabled={saveConfig.isPending || !form.formState.isValid}\n                                            >\n                                                <SendIcon className=\"mr-2 h-4 w-4\"/>\n                                                Test Connection\n                                            </Button>\n\n                                            <Button\n                                                type=\"submit\"\n                                                disabled={saveConfig.isPending || !form.formState.isDirty}\n                                            >\n                                                {saveConfig.isPending ? (\n                                                    <>\n                                                        <Loader2Icon className=\"mr-2 h-4 w-4 animate-spin\"/>\n                                                        Saving...\n                                                    </>\n                                                ) : (\n                                                    <>\n                                                        <CheckIcon className=\"mr-2 h-4 w-4\"/>\n                                                        Save Settings\n                                                    </>\n                                                )}\n                                            </Button>\n                                        </div>\n                                    </form>\n                                </Form>\n\n                                {/* Testing status display */}\n                                {emailConfig?.lastTestedAt && (\n                                    <div className=\"mt-8 pt-4 border-t\">\n                                        <h3 className=\"text-sm font-medium mb-2\">Testing Status</h3>\n                                        <div className={`p-4 rounded-md ${\n                                            emailConfig.testStatus?.startsWith('failed')\n                                                ? 'bg-destructive/10 text-destructive'\n                                                : 'bg-primary/10 text-primary'\n                                        }`}>\n                                            <div className=\"flex items-center\">\n                                                {emailConfig.testStatus?.startsWith('failed')\n                                                    ? <AlertCircleIcon className=\"h-5 w-5 mr-2\"/>\n                                                    : <CheckIcon className=\"h-5 w-5 mr-2\"/>\n                                                }\n                                                <div>\n                                                    <p className=\"font-medium\">\n                                                        {emailConfig.testStatus?.startsWith('failed')\n                                                            ? 'Test Failed'\n                                                            : 'Test Successful'\n                                                        }\n                                                    </p>\n                                                    <p className=\"text-sm\">\n                                                        {emailConfig.testStatus?.startsWith('failed') ? emailConfig.testStatus.replace('failed: ', '')\n                                                            : 'Connection verified and test email sent successfully.'\n                                                        }\n                                                    </p>\n                                                    <p className=\"text-xs mt-1\">\n                                                        Last\n                                                        tested: {new Date(emailConfig.lastTestedAt).toLocaleString('en-US', { timeZone: timezone })}\n                                                    </p>\n                                                </div>\n                                            </div>\n                                        </div>\n                                    </div>\n                                )}\n                            </CardContent>\n                        </Card>\n                    </TabsContent>\n\n                    <TabsContent value=\"send\">\n                        <Card>\n                            <CardHeader>\n                                <div className=\"flex items-center\">\n                                    <SendIcon className=\"h-5 w-5 mr-2 text-primary\"/>\n                                    <CardTitle>Send Changelog Email</CardTitle>\n                                </div>\n                                <CardDescription>\n                                    Send changelog updates to specific recipients or subscribers\n                                </CardDescription>\n                            </CardHeader>\n                            <CardContent>\n                                {!emailConfig?.enabled ? (\n                                    <Alert className=\"mb-6\">\n                                        <AlertDescription>\n                                            Email notifications are not enabled. Please enable them in the SMTP Settings\n                                            tab.\n                                        </AlertDescription>\n                                    </Alert>\n                                ) : (\n                                    <Form {...sendEmailForm}>\n                                        <form onSubmit={sendEmailForm.handleSubmit(onSendEmailSubmit)}\n                                              className=\"space-y-6\">\n                                            <FormField\n                                                control={sendEmailForm.control}\n                                                name=\"subject\"\n                                                render={({field}) => (\n                                                    <FormItem>\n                                                        <FormLabel>Email Subject</FormLabel>\n                                                        <FormControl>\n                                                            <Input placeholder=\"New Changelog Update\" {...field} />\n                                                        </FormControl>\n                                                        <FormMessage/>\n                                                    </FormItem>\n                                                )}\n                                            />\n\n                                            <FormField\n                                                control={sendEmailForm.control}\n                                                name=\"changelogEntryId\"\n                                                render={({field}) => (\n                                                    <FormItem>\n                                                        <FormLabel>Content to Send</FormLabel>\n                                                        <Select\n                                                            onValueChange={field.onChange}\n                                                            defaultValue={field.value}\n                                                        >\n                                                            <FormControl>\n                                                                <SelectTrigger>\n                                                                    <SelectValue\n                                                                        placeholder=\"Select an entry or send a digest\"/>\n                                                                </SelectTrigger>\n                                                            </FormControl>\n                                                            <SelectContent>\n                                                                <SelectItem value=\"digest\">\n                                                                    <div className=\"flex items-center\">\n                                                                        <div\n                                                                            className=\"mr-2 h-2 w-2 rounded-full bg-primary\"></div>\n                                                                        Send digest of recent entries\n                                                                    </div>\n                                                                </SelectItem>\n                                                                {recentEntries.map(entry => (\n                                                                    <SelectItem key={entry.id} value={entry.id}>\n                                                                        {entry.title} {entry.version ? `(${entry.version})` : ''}\n                                                                    </SelectItem>\n                                                                ))}\n                                                            </SelectContent>\n                                                        </Select>\n                                                        <FormDescription>\n                                                            Choose a specific changelog entry or send a digest of recent\n                                                            entries\n                                                        </FormDescription>\n                                                        <FormMessage/>\n                                                    </FormItem>\n                                                )}\n                                            />\n\n                                            <div className=\"border rounded-md p-4\">\n                                                <FormField\n                                                    control={sendEmailForm.control}\n                                                    name=\"recipientType\"\n                                                    render={({field}) => (\n                                                        <FormItem className=\"space-y-3\">\n                                                            <FormLabel>Send To</FormLabel>\n                                                            <FormControl>\n                                                                <RadioGroup\n                                                                    onValueChange={(value) =>\n                                                                        handleRecipientTypeChange(value as 'MANUAL' | 'SUBSCRIBERS' | 'BOTH')\n                                                                    }\n                                                                    defaultValue={field.value}\n                                                                    className=\"flex flex-col space-y-1\"\n                                                                >\n                                                                    <FormItem\n                                                                        className=\"flex items-center space-x-3 space-y-0\">\n                                                                        <FormControl>\n                                                                            <RadioGroupItem value=\"SUBSCRIBERS\"/>\n                                                                        </FormControl>\n                                                                        <FormLabel\n                                                                            className=\"font-normal cursor-pointer\">\n                                                                            <div className=\"flex items-center\">\n                                                                                <UsersIcon\n                                                                                    className=\"h-4 w-4 mr-2 text-muted-foreground\"/>\n                                                                                Subscribers\n                                                                                {isLoadingSubscribers ? (\n                                                                                    <Loader2Icon\n                                                                                        className=\"ml-2 h-3 w-3 animate-spin\"/>\n                                                                                ) : (\n                                                                                    <Badge variant=\"outline\"\n                                                                                           className=\"ml-2\">\n                                                                                        {subscriberCount}\n                                                                                    </Badge>\n                                                                                )}\n                                                                            </div>\n                                                                        </FormLabel>\n                                                                    </FormItem>\n                                                                    <FormItem\n                                                                        className=\"flex items-center space-x-3 space-y-0\">\n                                                                        <FormControl>\n                                                                            <RadioGroupItem value=\"MANUAL\"/>\n                                                                        </FormControl>\n                                                                        <FormLabel\n                                                                            className=\"font-normal cursor-pointer\">\n                                                                            <div className=\"flex items-center\">\n                                                                                <MailIcon\n                                                                                    className=\"h-4 w-4 mr-2 text-muted-foreground\"/>\n                                                                                Manual Recipients\n                                                                                {manualRecipients.length > 0 && (\n                                                                                    <Badge variant=\"outline\"\n                                                                                           className=\"ml-2\">\n                                                                                        {manualRecipients.length}\n                                                                                    </Badge>\n                                                                                )}\n                                                                            </div>\n                                                                        </FormLabel>\n                                                                    </FormItem>\n                                                                    <FormItem\n                                                                        className=\"flex items-center space-x-3 space-y-0\">\n                                                                        <FormControl>\n                                                                            <RadioGroupItem value=\"BOTH\"/>\n                                                                        </FormControl>\n                                                                        <FormLabel\n                                                                            className=\"font-normal cursor-pointer\">\n                                                                            <div className=\"flex items-center\">\n                                                                                <UserPlusIcon\n                                                                                    className=\"h-4 w-4 mr-2 text-muted-foreground\"/>\n                                                                                Both (Subscribers and Manual Recipients)\n                                                                            </div>\n                                                                        </FormLabel>\n                                                                    </FormItem>\n                                                                </RadioGroup>\n                                                            </FormControl>\n                                                            <FormMessage/>\n                                                        </FormItem>\n                                                    )}\n                                                />\n                                            </div>\n\n                                            {/* Subscription Types - Only show if sending to subscribers */}\n                                            {sendingToSubscribers && (\n                                                <div className=\"border rounded-md p-4\">\n                                                    <h3 className=\"text-sm font-medium mb-2\">Subscription Types</h3>\n                                                    <div className=\"flex flex-wrap gap-2 mt-2\">\n                                                        <Badge\n                                                            variant={selectedSubscriptionTypes.includes('ALL_UPDATES') ? \"default\" : \"outline\"}\n                                                            className=\"cursor-pointer hover:bg-primary/90 transition-colors\"\n                                                            onClick={() => toggleSubscriptionType('ALL_UPDATES')}\n                                                        >\n                                                            {selectedSubscriptionTypes.includes('ALL_UPDATES') && (\n                                                                <CheckCircleIcon className=\"mr-1 h-3 w-3\"/>\n                                                            )}\n                                                            All Updates\n                                                        </Badge>\n                                                        <Badge\n                                                            variant={selectedSubscriptionTypes.includes('MAJOR_ONLY') ? \"default\" : \"outline\"}\n                                                            className=\"cursor-pointer hover:bg-primary/90 transition-colors\"\n                                                            onClick={() => toggleSubscriptionType('MAJOR_ONLY')}\n                                                        >\n                                                            {selectedSubscriptionTypes.includes('MAJOR_ONLY') && (\n                                                                <CheckCircleIcon className=\"mr-1 h-3 w-3\"/>\n                                                            )}\n                                                            Major Updates Only\n                                                        </Badge>\n                                                        <Badge\n                                                            variant={selectedSubscriptionTypes.includes('DIGEST_ONLY') ? \"default\" : \"outline\"}\n                                                            className=\"cursor-pointer hover:bg-primary/90 transition-colors\"\n                                                            onClick={() => toggleSubscriptionType('DIGEST_ONLY')}\n                                                        >\n                                                            {selectedSubscriptionTypes.includes('DIGEST_ONLY') && (\n                                                                <CheckCircleIcon className=\"mr-1 h-3 w-3\"/>\n                                                            )}\n                                                            Digest Only\n                                                        </Badge>\n                                                    </div>\n                                                    <p className=\"text-xs text-muted-foreground mt-2\">\n                                                        Select which subscription types to include in this email.\n                                                    </p>\n                                                </div>\n                                            )}\n\n                                            {/* Manual recipients - Only show if manual recipients are enabled */}\n                                            {(sendEmailForm.watch('recipientType') === 'MANUAL' || sendEmailForm.watch('recipientType') === 'BOTH') && (\n                                                <FormField\n                                                    control={sendEmailForm.control}\n                                                    name=\"manualRecipients\"\n                                                    render={() => (\n                                                        <FormItem>\n                                                            <FormLabel>Manual Recipients</FormLabel>\n                                                            <div className=\"space-y-3\">\n                                                                <div className=\"flex gap-2\">\n                                                                    <Input\n                                                                        placeholder=\"example@email.com\"\n                                                                        value={newRecipient}\n                                                                        onChange={(e) => setNewRecipient(e.target.value)}\n                                                                        onKeyDown={(e) => {\n                                                                            if (e.key === 'Enter') {\n                                                                                e.preventDefault();\n                                                                                addRecipient();\n                                                                            }\n                                                                        }}\n                                                                        className=\"flex-1\"\n                                                                    />\n                                                                    <Button\n                                                                        type=\"button\"\n                                                                        variant=\"outline\"\n                                                                        size=\"sm\"\n                                                                        onClick={addRecipient}\n                                                                        disabled={!newRecipient}\n                                                                        className=\"whitespace-nowrap\"\n                                                                    >\n                                                                        <PlusIcon className=\"h-4 w-4 mr-1\"/>\n                                                                        Add\n                                                                    </Button>\n                                                                </div>\n\n                                                                {manualRecipients.length > 0 && (\n                                                                    <div className=\"flex flex-wrap gap-2 p-3 border rounded-md bg-muted/30\">\n                                                                        {manualRecipients.map((email, index) => (\n                                                                            <Badge\n                                                                                key={index}\n                                                                                variant=\"secondary\"\n                                                                                className=\"flex items-center gap-2 px-3 py-2\"\n                                                                            >\n                                                                                <MailIcon className=\"h-3 w-3\"/>\n                                                                                <span>{email}</span>\n                                                                                <button\n                                                                                    type=\"button\"\n                                                                                    onClick={() => removeRecipient(index)}\n                                                                                    className=\"ml-1 hover:text-destructive transition-colors\"\n                                                                                >\n                                                                                    <XIcon className=\"h-4 w-4\"/>\n                                                                                </button>\n                                                                            </Badge>\n                                                                        ))}\n                                                                    </div>\n                                                                )}\n                                                            </div>\n\n                                                            {sendEmailForm.formState.errors.manualRecipients && (\n                                                                <p className=\"text-sm font-medium text-destructive\">\n                                                                    {sendEmailForm.formState.errors.manualRecipients.message}\n                                                                </p>\n                                                            )}\n                                                            <FormDescription>\n                                                                Enter an email and click Add, or press Enter to add a recipient\n                                                            </FormDescription>\n                                                        </FormItem>\n                                                    )}\n                                                />\n                                            )}\n\n                                            {/* Send button and summary */}\n                                            <div className=\"border rounded-md p-4 bg-muted/20\">\n                                                <div className=\"flex items-center justify-between\">\n                                                    <div>\n                                                        <h3 className=\"text-sm font-medium\">Recipient Summary</h3>\n                                                        <p className=\"text-sm text-muted-foreground mt-1\">\n                                                            {sendEmailForm.watch('recipientType') === 'SUBSCRIBERS' && (\n                                                                <>Sending to {subscriberCount} subscribers</>\n                                                            )}\n                                                            {sendEmailForm.watch('recipientType') === 'MANUAL' && (\n                                                                <>Sending to {manualRecipients.length} manual\n                                                                    recipients</>\n                                                            )}\n                                                            {sendEmailForm.watch('recipientType') === 'BOTH' && (\n                                                                <>Sending to {manualRecipients.length} manual recipients\n                                                                    and approximately {subscriberCount} subscribers</>\n                                                            )}\n                                                        </p>\n                                                    </div>\n                                                    <Button\n                                                        type=\"submit\"\n                                                        disabled={\n                                                            sendEmailMutation.isPending ||\n                                                            (sendEmailForm.watch('recipientType') === 'MANUAL' && manualRecipients.length === 0) ||\n                                                            (sendEmailForm.watch('recipientType') === 'SUBSCRIBERS' && selectedSubscriptionTypes.length === 0) ||\n                                                            !sendEmailForm.watch('changelogEntryId')\n                                                        }\n                                                    >\n                                                        {sendEmailMutation.isPending ? (\n                                                            <>\n                                                                <Loader2Icon className=\"mr-2 h-4 w-4 animate-spin\"/>\n                                                                Sending...\n                                                            </>\n                                                        ) : (\n                                                            <>\n                                                                <SendIcon className=\"mr-2 h-4 w-4\"/>\n                                                                Send Email\n                                                            </>\n                                                        )}\n                                                    </Button>\n                                                </div>\n                                            </div>\n                                        </form>\n                                    </Form>\n                                )}\n                            </CardContent>\n                        </Card>\n\n                        {/* Email Preview Box */}\n                        <Card className=\"mt-4\">\n                            <CardHeader>\n                                <CardTitle className=\"text-sm\">Email Preview</CardTitle>\n                            </CardHeader>\n                            <CardContent>\n                                <div className=\"bg-card border rounded-md p-4 mb-4 text-sm\">\n                                    <div style={{maxWidth: '100%'}}>\n                                        <h2 style={{\n                                            fontSize: '18px',\n                                            fontWeight: 'bold',\n                                            marginBottom: '10px',\n                                            textAlign: 'center'\n                                        }}>\n                                            {project?.name || 'Project Name'} Changelog\n                                        </h2>\n                                        <p style={{\n                                            color: '#666',\n                                            fontSize: '14px',\n                                            marginBottom: '10px',\n                                            textAlign: 'center'\n                                        }}>\n                                            {sendEmailForm.watch('changelogEntryId') === 'digest' ? 'Recent updates to our product' : 'New update to our product'}\n                                        </p>\n                                        <hr style={{margin: '10px 0'}}/>\n\n                                        <div style={{padding: '10px 0'}}>\n                                            <div style={{fontSize: '16px', fontWeight: 'bold', margin: '8px 0'}}>\n                                                {recentEntries.length > 0 && sendEmailForm.watch('changelogEntryId') !== 'digest'\n                                                    ? recentEntries.find(e => e.id === sendEmailForm.watch('changelogEntryId'))?.title || 'Example Changelog Entry'\n                                                    : 'Recent Updates'}\n                                                {recentEntries.length > 0 && sendEmailForm.watch('changelogEntryId') !== 'digest' && (\n                                                    <span style={{\n                                                        color: '#666',\n                                                        fontSize: '12px',\n                                                        fontWeight: 'normal',\n                                                        marginLeft: '8px'\n                                                    }}>\n                                                        {recentEntries.find(e => e.id === sendEmailForm.watch('changelogEntryId'))?.version || 'v1.0.0'}\n                                                    </span>\n                                                )}\n                                            </div>\n\n                                            <div style={{marginBottom: '8px'}}>\n                                                <span style={{\n                                                    backgroundColor: '#f1f5f9',\n                                                    borderRadius: '4px',\n                                                    color: '#475569',\n                                                    display: 'inline-block',\n                                                    fontSize: '10px',\n                                                    margin: '0 4px 4px 0',\n                                                    padding: '2px 6px'\n                                                }}>\n                                                    {sendEmailForm.watch('changelogEntryId') === 'digest' ? 'Multiple Updates' : 'Feature'}\n                                                </span>\n                                            </div>\n\n                                            {sendEmailForm.watch('changelogEntryId') === 'digest'\n                                                ? <div style={{\n                                                    color: '#333',\n                                                    fontSize: '12px',\n                                                    lineHeight: '1.4',\n                                                    margin: '8px 0'\n                                                }}>\n                                                    &apos;This digest contains multiple recent updates to our\n                                                    product...&apos;\n                                                </div>\n                                                : <div style={{\n                                                    color: '#333',\n                                                    fontSize: '12px',\n                                                    lineHeight: '1.4',\n                                                    margin: '8px 0'\n                                                }}>\n                                                    <RenderMarkdown>\n                                                        {(() => {\n                                                            const entry = recentEntries.find(e => e.id === sendEmailForm.watch('changelogEntryId'));\n                                                            return entry?.content\n                                                                ? entry.content.substring(0, 100) + '...'\n                                                                : 'This is a simplified preview of how your email will look.';\n                                                        })()}\n                                                    </RenderMarkdown>\n                                                </div>}\n                                        </div>\n                                    </div>\n                                </div>\n\n                                <div className=\"flex items-center justify-between text-xs text-muted-foreground\">\n                                    <p>\n                                        This is a simplified preview. Actual emails will include styling and formatting\n                                        based on the entry content.\n                                    </p>\n                                    <Button\n                                        variant=\"ghost\"\n                                        size=\"sm\"\n                                        className=\"hover:text-foreground transition-colors\"\n                                        onClick={() => router.push(`/dashboard/projects/${projectId}/integrations/email/subscribers`)}\n                                    >\n                                        <UsersIcon className=\"h-3 w-3 mr-1\"/>\n                                        Manage Subscribers\n                                    </Button>\n                                </div>\n                            </CardContent>\n                        </Card>\n                    </TabsContent>\n                </Tabs>\n\n                {/* Test email dialog */}\n                <AlertDialog open={isTestDialogOpen} onOpenChange={setIsTestDialogOpen}>\n                    <AlertDialogContent>\n                        <AlertDialogHeader>\n                            <AlertDialogTitle>Test Email Connection</AlertDialogTitle>\n                            <AlertDialogDescription>\n                                Send a test email to verify your SMTP configuration.\n                                This will validate your settings and confirm that emails can be delivered.\n                            </AlertDialogDescription>\n                        </AlertDialogHeader>\n\n                        <Form {...testEmailForm}>\n                            <form onSubmit={testEmailForm.handleSubmit(onTestSubmit)} className=\"py-4\">\n                                <FormField\n                                    control={testEmailForm.control}\n                                    name=\"testEmail\"\n                                    render={({field}) => (\n                                        <FormItem>\n                                            <FormLabel>Recipient Email</FormLabel>\n                                            <FormControl>\n                                                <Input\n                                                    placeholder=\"you@example.com\"\n                                                    {...field}\n                                                />\n                                            </FormControl>\n                                            <FormDescription>\n                                                Enter your email address to receive the test message\n                                            </FormDescription>\n                                            <FormMessage/>\n                                        </FormItem>\n                                    )}\n                                />\n                            </form>\n                        </Form>\n\n                        <AlertDialogFooter>\n                            <AlertDialogCancel>Cancel</AlertDialogCancel>\n                            <AlertDialogAction\n                                disabled={testConnection.isPending || !testEmailForm.formState.isValid}\n                                onClick={testEmailForm.handleSubmit(onTestSubmit)}\n                            >\n                                {testConnection.isPending ? (\n                                    <>\n                                        <Loader2Icon className=\"mr-2 h-4 w-4 animate-spin\"/>\n                                        Testing...\n                                    </>\n                                ) : (\n                                    <>\n                                        <SendIcon className=\"mr-2 h-4 w-4\"/>\n                                        Send Test Email\n                                    </>\n                                )}\n                            </AlertDialogAction>\n                        </AlertDialogFooter>\n                    </AlertDialogContent>\n                </AlertDialog>\n            </motion.div>\n        </div>\n    );\n}"
  },
  {
    "path": "app/dashboard/projects/[projectId]/integrations/email/subscribers/page.tsx",
    "content": "'use client';\n\nimport { useState, useEffect } from 'react';\nimport { useTimezone } from '@/hooks/use-timezone';\nimport { useParams, useRouter } from 'next/navigation';\nimport { useQuery, useMutation } from '@tanstack/react-query';\nimport { motion } from 'framer-motion';\nimport { useForm } from 'react-hook-form';\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport { z } from 'zod';\n\n// UI Components\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n    CardFooter,\n} from '@/components/ui/card';\nimport {\n    Form,\n    FormControl,\n    FormDescription,\n    FormField,\n    FormItem,\n    FormLabel,\n    FormMessage,\n} from '@/components/ui/form';\nimport { Input } from '@/components/ui/input';\nimport { Button } from '@/components/ui/button';\nimport { useToast } from '@/hooks/use-toast';\nimport {\n    Table,\n    TableBody,\n    TableCell,\n    TableHead,\n    TableHeader,\n    TableRow,\n} from '@/components/ui/table';\nimport {\n    Select,\n    SelectContent,\n    SelectItem,\n    SelectTrigger,\n    SelectValue,\n} from '@/components/ui/select';\nimport {\n    AlertDialog,\n    AlertDialogAction,\n    AlertDialogCancel,\n    AlertDialogContent,\n    AlertDialogDescription,\n    AlertDialogFooter,\n    AlertDialogHeader,\n    AlertDialogTitle,\n} from '@/components/ui/alert-dialog';\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogFooter,\n    DialogHeader,\n    DialogTitle,\n} from '@/components/ui/dialog';\nimport {\n    ArrowLeftIcon,\n    PlusIcon,\n    TrashIcon,\n    UserIcon,\n    UsersIcon,\n    MailIcon,\n    BellIcon,\n    BellOffIcon,\n    CalendarIcon,\n    SearchIcon,\n    EditIcon,\n    ChevronLeftIcon,\n    ChevronRightIcon,\n    CheckIcon,\n} from 'lucide-react';\nimport { Badge } from '@/components/ui/badge';\n\n// Form schemas\nconst subscriberSchema = z.object({\n    email: z.string().email('Invalid email address'),\n    name: z.string().optional(),\n    subscriptionType: z.enum(['ALL_UPDATES', 'MAJOR_ONLY', 'DIGEST_ONLY']).default('ALL_UPDATES'),\n});\n\ntype SubscriberFormValues = z.infer<typeof subscriberSchema>;\n\n// Update subscriber schema\nconst updateSubscriberSchema = z.object({\n    name: z.string().optional(),\n    subscriptionType: z.enum(['ALL_UPDATES', 'MAJOR_ONLY', 'DIGEST_ONLY']),\n});\n\ntype UpdateSubscriberFormValues = z.infer<typeof updateSubscriberSchema>;\n\ntype Subscriber = {\n    id: string;\n    email: string;\n    name?: string;\n    subscriptionType: 'ALL_UPDATES' | 'MAJOR_ONLY' | 'DIGEST_ONLY';\n    createdAt: string;\n    lastEmailSentAt?: string;\n};\n\nexport default function SubscribersPage() {\n    const params = useParams();\n    const router = useRouter();\n    const timezone = useTimezone();\n    const { toast } = useToast();\n    const projectId = params.projectId as string;\n\n    // States\n    const [selectedSubscriber, setSelectedSubscriber] = useState<Subscriber | null>(null);\n    const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);\n    const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);\n    const [searchQuery, setSearchQuery] = useState('');\n    const [currentPage, setCurrentPage] = useState(1);\n    const [pageSize] = useState(10);\n\n    // Add form\n    const form = useForm<SubscriberFormValues>({\n        resolver: zodResolver(subscriberSchema),\n        defaultValues: {\n            email: '',\n            name: '',\n            subscriptionType: 'ALL_UPDATES',\n        },\n    });\n\n    // Edit form\n    const editForm = useForm<UpdateSubscriberFormValues>({\n        resolver: zodResolver(updateSubscriberSchema),\n        defaultValues: {\n            name: '',\n            subscriptionType: 'ALL_UPDATES',\n        },\n    });\n\n    // Set edit form values when a subscriber is selected\n    useEffect(() => {\n        if (selectedSubscriber && isEditDialogOpen) {\n            editForm.reset({\n                name: selectedSubscriber.name || '',\n                subscriptionType: selectedSubscriber.subscriptionType,\n            });\n        }\n    }, [selectedSubscriber, isEditDialogOpen, editForm]);\n\n    // Fetch subscribers with pagination\n    const { data, isLoading, refetch } = useQuery({\n        queryKey: ['subscribers', projectId, currentPage, pageSize, searchQuery],\n        queryFn: async () => {\n            const searchParam = searchQuery ? `&search=${encodeURIComponent(searchQuery)}` : '';\n            const response = await fetch(`/api/subscribers?projectId=${projectId}&page=${currentPage}&limit=${pageSize}${searchParam}`);\n            if (!response.ok) throw new Error('Failed to fetch subscribers');\n            return response.json();\n        },\n    });\n\n    // Add subscriber mutation\n    const addSubscriber = useMutation({\n        mutationFn: async (data: SubscriberFormValues) => {\n            const response = await fetch('/api/subscribers', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({\n                    ...data,\n                    projectId,\n                }),\n            });\n\n            if (!response.ok) {\n                const error = await response.json();\n                throw new Error(error.error || 'Failed to add subscriber');\n            }\n\n            return response.json();\n        },\n        onSuccess: () => {\n            toast({\n                title: 'Subscriber Added',\n                description: 'The subscriber has been added successfully.',\n            });\n            refetch();\n            form.reset();\n        },\n        onError: (error) => {\n            toast({\n                title: 'Error',\n                description: error.message,\n                variant: 'destructive',\n            });\n        },\n    });\n\n    // Update subscriber mutation\n    const updateSubscriber = useMutation({\n        mutationFn: async ({ subscriberId, data }: { subscriberId: string, data: UpdateSubscriberFormValues }) => {\n            const response = await fetch(`/api/subscribers/${subscriberId}?projectId=${projectId}`, {\n                method: 'PATCH',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify(data),\n            });\n\n            if (!response.ok) {\n                const error = await response.json();\n                throw new Error(error.error || 'Failed to update subscriber');\n            }\n\n            return response.json();\n        },\n        onSuccess: () => {\n            toast({\n                title: 'Subscriber Updated',\n                description: 'The subscriber has been updated successfully.',\n            });\n            setIsEditDialogOpen(false);\n            refetch();\n        },\n        onError: (error) => {\n            toast({\n                title: 'Error',\n                description: error.message,\n                variant: 'destructive',\n            });\n        },\n    });\n\n    // Delete subscriber mutation\n    const deleteSubscriber = useMutation({\n        mutationFn: async (subscriberId: string) => {\n            const response = await fetch(`/api/subscribers/${subscriberId}?projectId=${projectId}`, {\n                method: 'DELETE',\n            });\n\n            if (!response.ok) {\n                const error = await response.json();\n                throw new Error(error.error || 'Failed to delete subscriber');\n            }\n\n            return response.json();\n        },\n        onSuccess: () => {\n            toast({\n                title: 'Subscriber Removed',\n                description: 'The subscriber has been removed successfully.',\n            });\n            setIsDeleteDialogOpen(false);\n            setSelectedSubscriber(null);\n            refetch();\n        },\n        onError: (error) => {\n            toast({\n                title: 'Error',\n                description: error.message,\n                variant: 'destructive',\n            });\n        },\n    });\n\n    const onSubmit = (values: SubscriberFormValues) => {\n        addSubscriber.mutate(values);\n    };\n\n    const onUpdateSubmit = (values: UpdateSubscriberFormValues) => {\n        if (selectedSubscriber) {\n            updateSubscriber.mutate({\n                subscriberId: selectedSubscriber.id,\n                data: values\n            });\n        }\n    };\n\n    const handleEditClick = (subscriber: Subscriber) => {\n        setSelectedSubscriber(subscriber);\n        setIsEditDialogOpen(true);\n    };\n\n    const handleDeleteClick = (subscriber: Subscriber) => {\n        setSelectedSubscriber(subscriber);\n        setIsDeleteDialogOpen(true);\n    };\n\n    const handleConfirmDelete = () => {\n        if (selectedSubscriber) {\n            deleteSubscriber.mutate(selectedSubscriber.id);\n        }\n    };\n\n    const handleSearch = (e: React.FormEvent) => {\n        e.preventDefault();\n        setCurrentPage(1); // Reset to first page when searching\n        refetch();\n    };\n\n    const clearSearch = () => {\n        setSearchQuery('');\n        setCurrentPage(1);\n        refetch();\n    };\n\n    const formatDate = (dateString: string) => {\n        return new Date(dateString).toLocaleDateString('en-US', {\n            year: 'numeric',\n            month: 'short',\n            day: 'numeric',\n            timeZone: timezone,\n        });\n    };\n\n    const getSubscriptionTypeLabel = (type: string) => {\n        switch (type) {\n            case 'ALL_UPDATES':\n                return 'All Updates';\n            case 'MAJOR_ONLY':\n                return 'Major Updates Only';\n            case 'DIGEST_ONLY':\n                return 'Digest Only';\n            default:\n                return type;\n        }\n    };\n\n    // Calculate pagination\n    const totalItems = data?.totalCount || 0;\n    const totalPages = Math.ceil(totalItems / pageSize);\n\n    return (\n        <div className=\"container max-w-5xl mx-auto py-6\">\n            <div className=\"flex items-center mb-6\">\n                <Button\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    className=\"gap-1\"\n                    onClick={() => router.push(`/dashboard/projects/${projectId}/integrations/email`)}\n                >\n                    <ArrowLeftIcon className=\"h-4 w-4\" />\n                    Back to Email Settings\n                </Button>\n                <h1 className=\"text-2xl font-bold ml-4\">Subscriber Management</h1>\n            </div>\n\n            <motion.div\n                initial={{ opacity: 0, y: 20 }}\n                animate={{ opacity: 1, y: 0 }}\n                transition={{ duration: 0.3 }}\n            >\n                <Card className=\"mb-8\">\n                    <CardHeader>\n                        <div className=\"flex items-center\">\n                            <UsersIcon className=\"h-5 w-5 mr-2 text-primary\" />\n                            <CardTitle>Add Subscriber</CardTitle>\n                        </div>\n                        <CardDescription>\n                            Add a new subscriber to receive changelog notifications\n                        </CardDescription>\n                    </CardHeader>\n\n                    <CardContent>\n                        <Form {...form}>\n                            <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-4\">\n                                <div className=\"grid gap-4 sm:grid-cols-2\">\n                                    <FormField\n                                        control={form.control}\n                                        name=\"email\"\n                                        render={({ field }) => (\n                                            <FormItem>\n                                                <FormLabel>Email Address</FormLabel>\n                                                <FormControl>\n                                                    <Input placeholder=\"user@example.com\" {...field} />\n                                                </FormControl>\n                                                <FormMessage />\n                                            </FormItem>\n                                        )}\n                                    />\n\n                                    <FormField\n                                        control={form.control}\n                                        name=\"name\"\n                                        render={({ field }) => (\n                                            <FormItem>\n                                                <FormLabel>Name (Optional)</FormLabel>\n                                                <FormControl>\n                                                    <Input placeholder=\"John Doe\" {...field} />\n                                                </FormControl>\n                                                <FormMessage />\n                                            </FormItem>\n                                        )}\n                                    />\n                                </div>\n\n                                <FormField\n                                    control={form.control}\n                                    name=\"subscriptionType\"\n                                    render={({ field }) => (\n                                        <FormItem>\n                                            <FormLabel>Subscription Type</FormLabel>\n                                            <Select\n                                                onValueChange={field.onChange}\n                                                defaultValue={field.value}\n                                            >\n                                                <FormControl>\n                                                    <SelectTrigger>\n                                                        <SelectValue placeholder=\"Select subscription type\" />\n                                                    </SelectTrigger>\n                                                </FormControl>\n                                                <SelectContent>\n                                                    <SelectItem value=\"ALL_UPDATES\">All Updates</SelectItem>\n                                                    <SelectItem value=\"MAJOR_ONLY\">Major Updates Only</SelectItem>\n                                                    <SelectItem value=\"DIGEST_ONLY\">Digest Only</SelectItem>\n                                                </SelectContent>\n                                            </Select>\n                                            <FormDescription>\n                                                Determines which types of updates the subscriber will receive\n                                            </FormDescription>\n                                            <FormMessage />\n                                        </FormItem>\n                                    )}\n                                />\n\n                                <div className=\"flex justify-end\">\n                                    <Button\n                                        type=\"submit\"\n                                        disabled={addSubscriber.isPending}\n                                        className=\"w-full sm:w-auto\"\n                                    >\n                                        {addSubscriber.isPending ? (\n                                            <>\n                                                <div className=\"mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent\" />\n                                                Adding...\n                                            </>\n                                        ) : (\n                                            <>\n                                                <PlusIcon className=\"mr-2 h-4 w-4\" />\n                                                Add Subscriber\n                                            </>\n                                        )}\n                                    </Button>\n                                </div>\n                            </form>\n                        </Form>\n                    </CardContent>\n                </Card>\n\n                <Card>\n                    <CardHeader>\n                        <div className=\"flex items-center justify-between\">\n                            <div className=\"flex items-center\">\n                                <MailIcon className=\"h-5 w-5 mr-2 text-primary\" />\n                                <CardTitle>Subscribers List</CardTitle>\n                            </div>\n\n                            {/* Search form */}\n                            <form onSubmit={handleSearch} className=\"relative max-w-sm\">\n                                <SearchIcon className=\"absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground\" />\n                                <Input\n                                    type=\"search\"\n                                    placeholder=\"Search by email or name...\"\n                                    className=\"pl-8 w-[250px]\"\n                                    value={searchQuery}\n                                    onChange={(e) => setSearchQuery(e.target.value)}\n                                />\n                                {/*{searchQuery && (*/}\n                                {/*    <Button*/}\n                                {/*        type=\"button\"*/}\n                                {/*        variant=\"ghost\"*/}\n                                {/*        size=\"sm\"*/}\n                                {/*        className=\"absolute right-0 top-0 h-9 w-9 p-0\"*/}\n                                {/*        onClick={clearSearch}*/}\n                                {/*    >*/}\n                                {/*        <XIcon className=\"h-4 w-4\" />*/}\n                                {/*    </Button>*/}\n                                {/*)}*/}\n                            </form>\n                        </div>\n                        <CardDescription>\n                            Manage existing subscribers and their preferences\n                        </CardDescription>\n                    </CardHeader>\n\n                    <CardContent>\n                        {isLoading ? (\n                            <div className=\"flex justify-center py-8\">\n                                <div className=\"h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent\"></div>\n                            </div>\n                        ) : data?.subscribers?.length === 0 ? (\n                            <div className=\"text-center py-12 border rounded-md\">\n                                <UsersIcon className=\"h-12 w-12 mx-auto text-muted-foreground\" />\n                                <h3 className=\"mt-4 text-lg font-medium\">No subscribers found</h3>\n                                <p className=\"mt-2 text-sm text-muted-foreground\">\n                                    {searchQuery ? 'Try a different search term or ' : 'Add subscribers above to start sending them changelog updates.'}\n                                    {searchQuery && (\n                                        <Button variant=\"link\" className=\"p-0 h-auto\" onClick={clearSearch}>\n                                            clear your search\n                                        </Button>\n                                    )}\n                                </p>\n                            </div>\n                        ) : (\n                            <div className=\"rounded-md border\">\n                                <Table>\n                                    <TableHeader>\n                                        <TableRow>\n                                            <TableHead>Email</TableHead>\n                                            <TableHead>Name</TableHead>\n                                            <TableHead>Subscription Type</TableHead>\n                                            <TableHead>Subscribed On</TableHead>\n                                            <TableHead>Last Email</TableHead>\n                                            <TableHead className=\"text-right\">Actions</TableHead>\n                                        </TableRow>\n                                    </TableHeader>\n                                    <TableBody>\n                                        {data?.subscribers?.map((subscriber: Subscriber) => (\n                                            <TableRow key={subscriber.id}>\n                                                <TableCell className=\"font-medium\">\n                                                    <div className=\"flex items-center\">\n                                                        <MailIcon className=\"h-4 w-4 mr-2 text-muted-foreground\" />\n                                                        {subscriber.email}\n                                                    </div>\n                                                </TableCell>\n                                                <TableCell>\n                                                    <div className=\"flex items-center\">\n                                                        <UserIcon className=\"h-4 w-4 mr-2 text-muted-foreground\" />\n                                                        {subscriber.name || '-'}\n                                                    </div>\n                                                </TableCell>\n                                                <TableCell>\n                                                    <Badge variant={\n                                                        subscriber.subscriptionType === 'ALL_UPDATES' ? 'default' :\n                                                            subscriber.subscriptionType === 'MAJOR_ONLY' ? 'warning' : 'secondary'\n                                                    }>\n                                                        {subscriber.subscriptionType === 'ALL_UPDATES' && <BellIcon className=\"h-3 w-3 mr-1\" />}\n                                                        {subscriber.subscriptionType === 'MAJOR_ONLY' && <BellOffIcon className=\"h-3 w-3 mr-1\" />}\n                                                        {subscriber.subscriptionType === 'DIGEST_ONLY' && <CalendarIcon className=\"h-3 w-3 mr-1\" />}\n                                                        {getSubscriptionTypeLabel(subscriber.subscriptionType)}\n                                                    </Badge>\n                                                </TableCell>\n                                                <TableCell>{formatDate(subscriber.createdAt)}</TableCell>\n                                                <TableCell>\n                                                    {subscriber.lastEmailSentAt ? formatDate(subscriber.lastEmailSentAt) : 'Never'}\n                                                </TableCell>\n                                                <TableCell className=\"text-right\">\n                                                    <div className=\"flex justify-end gap-2\">\n                                                        <Button\n                                                            variant=\"outline\"\n                                                            size=\"sm\"\n                                                            onClick={() => handleEditClick(subscriber)}\n                                                            title=\"Edit subscriber\"\n                                                        >\n                                                            <EditIcon className=\"h-4 w-4 text-primary\" />\n                                                        </Button>\n                                                        <Button\n                                                            variant=\"outline\"\n                                                            size=\"sm\"\n                                                            onClick={() => handleDeleteClick(subscriber)}\n                                                            title=\"Remove subscriber\"\n                                                        >\n                                                            <TrashIcon className=\"h-4 w-4 text-destructive\" />\n                                                        </Button>\n                                                    </div>\n                                                </TableCell>\n                                            </TableRow>\n                                        ))}\n                                    </TableBody>\n                                </Table>\n                            </div>\n                        )}\n                    </CardContent>\n\n                    {/* Pagination */}\n                    {!isLoading && data?.subscribers?.length > 0 && (\n                        <CardFooter className=\"flex items-center justify-between px-6 pt-0\">\n                            <div className=\"text-sm text-muted-foreground\">\n                                Showing <span className=\"font-medium\">{(currentPage - 1) * pageSize + 1}</span> to{\" \"}\n                                <span className=\"font-medium\">{Math.min(currentPage * pageSize, totalItems)}</span> of{\" \"}\n                                <span className=\"font-medium\">{totalItems}</span> subscribers\n                            </div>\n                            <div className=\"flex items-center gap-2\">\n                                <Button\n                                    variant=\"outline\"\n                                    size=\"sm\"\n                                    onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}\n                                    disabled={currentPage === 1}\n                                >\n                                    <ChevronLeftIcon className=\"h-4 w-4\" />\n                                </Button>\n                                <div className=\"text-sm\">\n                                    Page <span className=\"font-medium\">{currentPage}</span> of{\" \"}\n                                    <span className=\"font-medium\">{totalPages || 1}</span>\n                                </div>\n                                <Button\n                                    variant=\"outline\"\n                                    size=\"sm\"\n                                    onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}\n                                    disabled={currentPage >= totalPages}\n                                >\n                                    <ChevronRightIcon className=\"h-4 w-4\" />\n                                </Button>\n                            </div>\n                        </CardFooter>\n                    )}\n                </Card>\n            </motion.div>\n\n            {/* Delete confirmation dialog */}\n            <AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>\n                <AlertDialogContent>\n                    <AlertDialogHeader>\n                        <AlertDialogTitle>Confirm Removal</AlertDialogTitle>\n                        <AlertDialogDescription>\n                            Are you sure you want to remove <span className=\"font-medium\">{selectedSubscriber?.email}</span> from subscribers? This action cannot be undone.\n                        </AlertDialogDescription>\n                    </AlertDialogHeader>\n                    <AlertDialogFooter>\n                        <AlertDialogCancel>Cancel</AlertDialogCancel>\n                        <AlertDialogAction onClick={handleConfirmDelete}>\n                            {deleteSubscriber.isPending ? (\n                                <>\n                                    <div className=\"mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent\" />\n                                    Removing...\n                                </>\n                            ) : (\n                                'Remove Subscriber'\n                            )}\n                        </AlertDialogAction>\n                    </AlertDialogFooter>\n                </AlertDialogContent>\n            </AlertDialog>\n\n            {/* Edit subscriber dialog */}\n            <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>\n                <DialogContent>\n                    <DialogHeader>\n                        <DialogTitle>Edit Subscriber</DialogTitle>\n                        <DialogDescription>\n                            Update information for {selectedSubscriber?.email}\n                        </DialogDescription>\n                    </DialogHeader>\n\n                    <Form {...editForm}>\n                        <form onSubmit={editForm.handleSubmit(onUpdateSubmit)} className=\"space-y-4\">\n                            <FormField\n                                control={editForm.control}\n                                name=\"name\"\n                                render={({ field }) => (\n                                    <FormItem>\n                                        <FormLabel>Name</FormLabel>\n                                        <FormControl>\n                                            <Input placeholder=\"John Doe\" {...field} />\n                                        </FormControl>\n                                        <FormMessage />\n                                    </FormItem>\n                                )}\n                            />\n\n                            <FormField\n                                control={editForm.control}\n                                name=\"subscriptionType\"\n                                render={({ field }) => (\n                                    <FormItem>\n                                        <FormLabel>Subscription Type</FormLabel>\n                                        <Select\n                                            onValueChange={field.onChange}\n                                            defaultValue={field.value}\n                                        >\n                                            <FormControl>\n                                                <SelectTrigger>\n                                                    <SelectValue placeholder=\"Select subscription type\" />\n                                                </SelectTrigger>\n                                            </FormControl>\n                                            <SelectContent>\n                                                <SelectItem value=\"ALL_UPDATES\">\n                                                    <div className=\"flex items-center\">\n                                                        <BellIcon className=\"mr-2 h-4 w-4\" />\n                                                        All Updates\n                                                    </div>\n                                                </SelectItem>\n                                                <SelectItem value=\"MAJOR_ONLY\">\n                                                    <div className=\"flex items-center\">\n                                                        <BellOffIcon className=\"mr-2 h-4 w-4\" />\n                                                        Major Updates Only\n                                                    </div>\n                                                </SelectItem>\n                                                <SelectItem value=\"DIGEST_ONLY\">\n                                                    <div className=\"flex items-center\">\n                                                        <CalendarIcon className=\"mr-2 h-4 w-4\" />\n                                                        Digest Only\n                                                    </div>\n                                                </SelectItem>\n                                            </SelectContent>\n                                        </Select>\n                                        <FormDescription>\n                                            Determines which types of updates the subscriber will receive\n                                        </FormDescription>\n                                        <FormMessage />\n                                    </FormItem>\n                                )}\n                            />\n\n                            <DialogFooter>\n                                <Button\n                                    type=\"button\"\n                                    variant=\"outline\"\n                                    onClick={() => setIsEditDialogOpen(false)}\n                                >\n                                    Cancel\n                                </Button>\n                                <Button\n                                    type=\"submit\"\n                                    disabled={updateSubscriber.isPending}\n                                >\n                                    {updateSubscriber.isPending ? (\n                                        <>\n                                            <div className=\"mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent\" />\n                                            Updating...\n                                        </>\n                                    ) : (\n                                        <>\n                                            <CheckIcon className=\"mr-2 h-4 w-4\" />\n                                            Save Changes\n                                        </>\n                                    )}\n                                </Button>\n                            </DialogFooter>\n                        </form>\n                    </Form>\n                </DialogContent>\n            </Dialog>\n        </div>\n    );\n}"
  },
  {
    "path": "app/dashboard/projects/[projectId]/integrations/github/page.tsx",
    "content": "'use client';\n\nimport React, { useState, useEffect } from 'react';\nimport { useParams, useRouter } from 'next/navigation';\nimport { useTimezone } from '@/hooks/use-timezone';\nimport {\n    ArrowLeft,\n    Settings,\n    Activity,\n    BookOpen,\n    Shield,\n    ExternalLink,\n    Clock,\n    GitBranch,\n    Users,\n    Star,\n    CheckCircle2,\n    Loader2\n} from 'lucide-react';\nimport {SiGithub} from '@icons-pack/react-simple-icons';\n\nimport { Button } from '@/components/ui/button';\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';\nimport { Badge } from '@/components/ui/badge';\nimport { Separator } from '@/components/ui/separator';\nimport { Alert, AlertDescription } from '@/components/ui/alert';\nimport {\n    Breadcrumb,\n    BreadcrumbItem,\n    BreadcrumbLink,\n    BreadcrumbList,\n    BreadcrumbPage,\n    BreadcrumbSeparator,\n} from '@/components/ui/breadcrumb';\n\n// Import our integration components\nimport GitHubIntegrationSettings from '@/components/github/GitHubIntegrationSettings';\nimport GitHubGenerateDialog from '@/components/github/GitHubGenerateDialog';\n\ninterface Project {\n    id: string;\n    name: string;\n    isPublic: boolean;\n    createdAt: string;\n}\n\ninterface GitHubIntegration {\n    enabled: boolean;\n    repositoryUrl: string;\n    defaultBranch: string;\n    lastSyncAt: string | null;\n    lastCommitSha: string | null;\n    hasAccessToken: boolean;\n    includeBreakingChanges: boolean;\n    includeFixes: boolean;\n    includeFeatures: boolean;\n    includeChores: boolean;\n    customCommitTypes: string[];\n}\n\ninterface RepositoryStats {\n    name: string;\n    fullName: string;\n    description: string;\n    private: boolean;\n    defaultBranch: string;\n    language: string;\n    stargazersCount: number;\n    forksCount: number;\n    openIssuesCount: number;\n    pushedAt: string;\n}\n\nexport default function GitHubIntegrationPage() {\n    const params = useParams();\n    const router = useRouter();\n    const projectId = params.projectId as string;\n    const timezone = useTimezone();\n\n    // State\n    const [project, setProject] = useState<Project | null>(null);\n    const [integration, setIntegration] = useState<GitHubIntegration | null>(null);\n    const [repoStats, setRepoStats] = useState<RepositoryStats | null>(null);\n    const [isLoadingProject, setIsLoadingProject] = useState(true);\n    const [isLoadingIntegration, setIsLoadingIntegration] = useState(true);\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    const [isLoadingStats, setIsLoadingStats] = useState(false);\n    const [error, setError] = useState<string | null>(null);\n\n    // Load project data\n    useEffect(() => {\n        loadProject();\n        loadIntegration();\n    }, [projectId]);\n\n    // Load repository stats when integration is available\n    useEffect(() => {\n        if (integration?.enabled && integration.hasAccessToken) {\n            loadRepositoryStats();\n        }\n    }, [integration]);\n\n    const loadProject = async () => {\n        try {\n            setIsLoadingProject(true);\n            const response = await fetch(`/api/projects/${projectId}`);\n            if (!response.ok) throw new Error('Failed to load project');\n\n            const data = await response.json();\n            setProject(data);\n        } catch (err) {\n            setError(err instanceof Error ? err.message : 'Failed to load project');\n        } finally {\n            setIsLoadingProject(false);\n        }\n    };\n\n    const loadIntegration = async () => {\n        try {\n            setIsLoadingIntegration(true);\n            const response = await fetch(`/api/projects/${projectId}/integrations/github`);\n            if (!response.ok) throw new Error('Failed to load integration');\n\n            const data = await response.json();\n            setIntegration(data);\n        } catch (err) {\n            console.error('Failed to load integration:', err);\n            // Don't set error for integration - it might not exist yet\n        } finally {\n            setIsLoadingIntegration(false);\n        }\n    };\n\n    const loadRepositoryStats = async () => {\n        if (!integration?.repositoryUrl || !integration.hasAccessToken) return;\n\n        try {\n            setIsLoadingStats(true);\n            const response = await fetch(`/api/projects/${projectId}/integrations/github/test`, {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({\n                    repositoryUrl: integration.repositoryUrl,\n                    accessToken: 'existing' // Signal to use existing token\n                })\n            });\n\n            if (response.ok) {\n                const data = await response.json();\n                if (data.success && data.repository) {\n                    setRepoStats(data.repository);\n                }\n            }\n        } catch (err) {\n            console.error('Failed to load repository stats:', err);\n        } finally {\n            setIsLoadingStats(false);\n        }\n    };\n\n    const handleChangelogGenerated = (content: string, version?: string) => {\n        // Redirect to changelog creation with pre-filled content\n        const searchParams = new URLSearchParams({\n            content,\n            ...(version && { version })\n        });\n        router.push(`/dashboard/projects/${projectId}/changelog/new?${searchParams}`);\n    };\n\n    const formatDate = (dateString: string) => {\n        return new Date(dateString).toLocaleString('en-US', { timeZone: timezone });\n    };\n\n    const getRepositoryOwnerAndName = (url: string) => {\n        try {\n            const match = url.match(/github\\.com\\/([^\\/]+)\\/([^\\/]+)/);\n            return match ? { owner: match[1], name: match[2] } : null;\n        } catch {\n            return null;\n        }\n    };\n\n    if (isLoadingProject) {\n        return (\n            <div className=\"flex items-center justify-center min-h-96\">\n                <Loader2 className=\"h-8 w-8 animate-spin\" />\n            </div>\n        );\n    }\n\n    if (error || !project) {\n        return (\n            <div className=\"max-w-4xl mx-auto p-6\">\n                <Alert variant=\"destructive\">\n                    <AlertDescription>\n                        {error || 'Project not found'}\n                    </AlertDescription>\n                </Alert>\n            </div>\n        );\n    }\n\n    const repoInfo = integration?.repositoryUrl ? getRepositoryOwnerAndName(integration.repositoryUrl) : null;\n    const isConfigured = integration?.enabled && integration.hasAccessToken;\n\n    return (\n        <div className=\"max-w-6xl mx-auto p-6 space-y-6\">\n            {/* Breadcrumb Navigation */}\n            <Breadcrumb>\n                <BreadcrumbList>\n                    <BreadcrumbItem>\n                        <BreadcrumbLink href=\"/dashboard\">Dashboard</BreadcrumbLink>\n                    </BreadcrumbItem>\n                    <BreadcrumbSeparator />\n                    <BreadcrumbItem>\n                        <BreadcrumbLink href=\"/dashboard/projects\">Projects</BreadcrumbLink>\n                    </BreadcrumbItem>\n                    <BreadcrumbSeparator />\n                    <BreadcrumbItem>\n                        <BreadcrumbLink href={`/dashboard/projects/${projectId}`}>\n                            {project.name}\n                        </BreadcrumbLink>\n                    </BreadcrumbItem>\n                    <BreadcrumbSeparator />\n                    <BreadcrumbItem>\n                        <BreadcrumbLink href={`/dashboard/projects/${projectId}/settings`}>\n                            Settings\n                        </BreadcrumbLink>\n                    </BreadcrumbItem>\n                    <BreadcrumbSeparator />\n                    <BreadcrumbItem>\n                        <BreadcrumbPage>GitHub</BreadcrumbPage>\n                    </BreadcrumbItem>\n                </BreadcrumbList>\n            </Breadcrumb>\n\n            {/* Header */}\n            <div className=\"flex items-center justify-between\">\n                <div className=\"flex items-center gap-4\">\n                    <Button\n                        variant=\"ghost\"\n                        size=\"icon\"\n                        onClick={() => router.back()}\n                    >\n                        <ArrowLeft className=\"h-4 w-4\" />\n                    </Button>\n                    <div className=\"flex items-center gap-3\">\n                        <div className=\"p-2 bg-primary/10 rounded-lg\">\n                            <SiGithub className=\"h-6 w-6 text-primary\" />\n                        </div>\n                        <div>\n                            <h1 className=\"text-2xl font-bold\">GitHub Integration</h1>\n                            <p className=\"text-muted-foreground\">\n                                Project: {project.name}\n                            </p>\n                        </div>\n                    </div>\n                </div>\n\n                <div className=\"flex items-center gap-3\">\n                    {isConfigured && (\n                        <GitHubGenerateDialog\n                            projectId={projectId}\n                            onGenerated={handleChangelogGenerated}\n                        />\n                    )}\n                    <Badge variant={isConfigured ? \"default\" : \"secondary\"}>\n                        {isConfigured ? \"Configured\" : \"Not Configured\"}\n                    </Badge>\n                </div>\n            </div>\n\n            {/* Status Overview */}\n            {isConfigured ? (\n                <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n                    <Card>\n                        <CardContent className=\"p-4\">\n                            <div className=\"flex items-center gap-3\">\n                                <div className=\"p-2 bg-green-100 dark:bg-green-900/30 rounded-full\">\n                                    <CheckCircle2 className=\"h-4 w-4 text-green-600 dark:text-green-400\" />\n                                </div>\n                                <div>\n                                    <p className=\"text-sm font-medium\">Connected</p>\n                                    <p className=\"text-xs text-muted-foreground\">\n                                        {repoInfo ? `${repoInfo.owner}/${repoInfo.name}` : 'Repository linked'}\n                                    </p>\n                                </div>\n                            </div>\n                        </CardContent>\n                    </Card>\n\n                    <Card>\n                        <CardContent className=\"p-4\">\n                            <div className=\"flex items-center gap-3\">\n                                <div className=\"p-2 bg-blue-100 dark:bg-blue-900/30 rounded-full\">\n                                    <Clock className=\"h-4 w-4 text-blue-600 dark:text-blue-400\" />\n                                </div>\n                                <div>\n                                    <p className=\"text-sm font-medium\">Last Sync</p>\n                                    <p className=\"text-xs text-muted-foreground\">\n                                        {integration.lastSyncAt\n                                            ? formatDate(integration.lastSyncAt)\n                                            : 'Never'\n                                        }\n                                    </p>\n                                </div>\n                            </div>\n                        </CardContent>\n                    </Card>\n\n                    <Card>\n                        <CardContent className=\"p-4\">\n                            <div className=\"flex items-center gap-3\">\n                                <div className=\"p-2 bg-purple-100 dark:bg-purple-900/30 rounded-full\">\n                                    <GitBranch className=\"h-4 w-4 text-purple-600 dark:text-purple-400\" />\n                                </div>\n                                <div>\n                                    <p className=\"text-sm font-medium\">Default Branch</p>\n                                    <p className=\"text-xs text-muted-foreground\">\n                                        {integration.defaultBranch}\n                                    </p>\n                                </div>\n                            </div>\n                        </CardContent>\n                    </Card>\n                </div>\n            ) : (\n                <Alert variant=\"warning\">\n                    <AlertDescription>\n                        GitHub integration is not configured. Set up your repository connection below to start generating changelog content from commits.\n                    </AlertDescription>\n                </Alert>\n            )}\n\n            {/* Repository Stats */}\n            {isConfigured && repoStats && (\n                <Card>\n                    <CardHeader>\n                        <CardTitle className=\"flex items-center gap-2\">\n                            <SiGithub className=\"h-5 w-5\" />\n                            Repository Information\n                            <Button variant=\"ghost\" size=\"icon\" className=\"ml-auto\">\n                                <ExternalLink\n                                    className=\"h-4 w-4\"\n                                    onClick={() => window.open(integration.repositoryUrl, '_blank')}\n                                />\n                            </Button>\n                        </CardTitle>\n                    </CardHeader>\n                    <CardContent>\n                        <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4\">\n                            <div className=\"space-y-1\">\n                                <p className=\"text-sm font-medium\">Repository</p>\n                                <p className=\"text-2xl font-bold\">{repoStats.fullName}</p>\n                                {repoStats.description && (\n                                    <p className=\"text-sm text-muted-foreground\">{repoStats.description}</p>\n                                )}\n                            </div>\n\n                            <div className=\"space-y-1\">\n                                <p className=\"text-sm font-medium flex items-center gap-1\">\n                                    <Star className=\"h-3 w-3\" />\n                                    Stars\n                                </p>\n                                <p className=\"text-2xl font-bold\">{repoStats.stargazersCount.toLocaleString()}</p>\n                            </div>\n\n                            <div className=\"space-y-1\">\n                                <p className=\"text-sm font-medium flex items-center gap-1\">\n                                    <Users className=\"h-3 w-3\" />\n                                    Forks\n                                </p>\n                                <p className=\"text-2xl font-bold\">{repoStats.forksCount.toLocaleString()}</p>\n                            </div>\n\n                            <div className=\"space-y-1\">\n                                <p className=\"text-sm font-medium\">Language</p>\n                                <p className=\"text-2xl font-bold\">{repoStats.language || 'N/A'}</p>\n                            </div>\n                        </div>\n\n                        <Separator className=\"my-4\" />\n\n                        <div className=\"flex items-center justify-between\">\n                            <div className=\"flex items-center gap-4\">\n                                <Badge variant={repoStats.private ? \"secondary\" : \"outline\"}>\n                                    {repoStats.private ? \"Private\" : \"Public\"}\n                                </Badge>\n                                {repoStats.openIssuesCount > 0 && (\n                                    <span className=\"text-sm text-muted-foreground\">\n                                        {repoStats.openIssuesCount} open issues\n                                    </span>\n                                )}\n                            </div>\n                            <span className=\"text-sm text-muted-foreground\">\n                                Last push: {formatDate(repoStats.pushedAt)}\n                            </span>\n                        </div>\n                    </CardContent>\n                </Card>\n            )}\n\n            {/* Main Content Tabs */}\n            <Tabs defaultValue=\"settings\" className=\"w-full\">\n                <TabsList className=\"grid w-full grid-cols-3\">\n                    <TabsTrigger value=\"settings\" className=\"gap-2\">\n                        <Settings className=\"h-4 w-4\" />\n                        Configuration\n                    </TabsTrigger>\n                    <TabsTrigger value=\"activity\" className=\"gap-2\">\n                        <Activity className=\"h-4 w-4\" />\n                        Activity\n                    </TabsTrigger>\n                    <TabsTrigger value=\"security\" className=\"gap-2\">\n                        <Shield className=\"h-4 w-4\" />\n                        Security\n                    </TabsTrigger>\n                </TabsList>\n\n                <TabsContent value=\"settings\" className=\"space-y-6\">\n                    <Card>\n                        <CardHeader>\n                            <CardTitle>Repository Configuration</CardTitle>\n                            <CardDescription>\n                                Configure your GitHub repository connection and content generation preferences\n                            </CardDescription>\n                        </CardHeader>\n                        <CardContent>\n                            {isLoadingIntegration ? (\n                                <div className=\"flex items-center justify-center py-8\">\n                                    <Loader2 className=\"h-6 w-6 animate-spin\" />\n                                </div>\n                            ) : (\n                                <GitHubIntegrationSettings\n                                    projectId={projectId}\n                                    projectName={project.name}\n                                />\n                            )}\n                        </CardContent>\n                    </Card>\n                </TabsContent>\n\n                <TabsContent value=\"activity\" className=\"space-y-6\">\n                    <Card>\n                        <CardHeader>\n                            <CardTitle>Recent Activity</CardTitle>\n                            <CardDescription>\n                                View recent changelog generations and sync history\n                            </CardDescription>\n                        </CardHeader>\n                        <CardContent>\n                            {integration?.lastSyncAt ? (\n                                <div className=\"space-y-4\">\n                                    <div className=\"flex items-center justify-between p-4 border rounded-lg\">\n                                        <div>\n                                            <p className=\"font-medium\">Last Sync</p>\n                                            <p className=\"text-sm text-muted-foreground\">\n                                                Synced repository data from {integration.defaultBranch} branch\n                                            </p>\n                                        </div>\n                                        <div className=\"text-right\">\n                                            <p className=\"text-sm font-medium\">{formatDate(integration.lastSyncAt)}</p>\n                                            {integration.lastCommitSha && (\n                                                <code className=\"text-xs bg-muted px-2 py-1 rounded\">\n                                                    {integration.lastCommitSha.substring(0, 7)}\n                                                </code>\n                                            )}\n                                        </div>\n                                    </div>\n                                </div>\n                            ) : (\n                                <div className=\"text-center py-8\">\n                                    <Activity className=\"h-12 w-12 mx-auto text-muted-foreground mb-4\" />\n                                    <h3 className=\"font-medium mb-2\">No Activity Yet</h3>\n                                    <p className=\"text-sm text-muted-foreground\">\n                                        Generate your first changelog to see activity history here.\n                                    </p>\n                                </div>\n                            )}\n                        </CardContent>\n                    </Card>\n                </TabsContent>\n\n                <TabsContent value=\"security\" className=\"space-y-6\">\n                    <Card>\n                        <CardHeader>\n                            <CardTitle>Security Information</CardTitle>\n                            <CardDescription>\n                                Security features and access token management\n                            </CardDescription>\n                        </CardHeader>\n                        <CardContent className=\"space-y-4\">\n                            <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n                                <div>\n                                    <h4 className=\"font-medium mb-3\">Security Features</h4>\n                                    <ul className=\"space-y-2 text-sm\">\n                                        <li className=\"flex items-center gap-2\">\n                                            <CheckCircle2 className=\"h-4 w-4 text-green-500\" />\n                                            Access tokens encrypted at rest\n                                        </li>\n                                        <li className=\"flex items-center gap-2\">\n                                            <CheckCircle2 className=\"h-4 w-4 text-green-500\" />\n                                            No webhooks or external callbacks\n                                        </li>\n                                        <li className=\"flex items-center gap-2\">\n                                            <CheckCircle2 className=\"h-4 w-4 text-green-500\" />\n                                            Minimal GitHub permissions required\n                                        </li>\n                                        <li className=\"flex items-center gap-2\">\n                                            <CheckCircle2 className=\"h-4 w-4 text-green-500\" />\n                                            Project-level token isolation\n                                        </li>\n                                        <li className=\"flex items-center gap-2\">\n                                            <CheckCircle2 className=\"h-4 w-4 text-green-500\" />\n                                            Comprehensive audit logging\n                                        </li>\n                                    </ul>\n                                </div>\n\n                                <div>\n                                    <h4 className=\"font-medium mb-3\">Required Permissions</h4>\n                                    <ul className=\"space-y-2 text-sm\">\n                                        <li className=\"flex items-center gap-2\">\n                                            <BookOpen className=\"h-4 w-4 text-blue-500\" />\n                                            <span className=\"font-mono\">repo</span> (for private repos)\n                                        </li>\n                                        <li className=\"flex items-center gap-2\">\n                                            <BookOpen className=\"h-4 w-4 text-blue-500\" />\n                                            <span className=\"font-mono\">public_repo</span> (for public repos)\n                                        </li>\n                                    </ul>\n                                    <p className=\"text-xs text-muted-foreground mt-3\">\n                                        These permissions allow reading repository information, commits, tags, and releases.\n                                    </p>\n                                </div>\n                            </div>\n\n                            <Separator />\n\n                            <div>\n                                <h4 className=\"font-medium mb-2\">Access Token Status</h4>\n                                <div className=\"flex items-center justify-between p-3 border rounded-lg\">\n                                    <div>\n                                        <p className=\"text-sm font-medium\">\n                                            {integration?.hasAccessToken ? 'Token Configured' : 'No Token'}\n                                        </p>\n                                        <p className=\"text-xs text-muted-foreground\">\n                                            {integration?.hasAccessToken\n                                                ? 'A valid access token is stored and encrypted'\n                                                : 'Configure an access token to enable the integration'\n                                            }\n                                        </p>\n                                    </div>\n                                    <Badge variant={integration?.hasAccessToken ? \"default\" : \"secondary\"}>\n                                        {integration?.hasAccessToken ? \"Active\" : \"Inactive\"}\n                                    </Badge>\n                                </div>\n                            </div>\n                        </CardContent>\n                    </Card>\n                </TabsContent>\n            </Tabs>\n        </div>\n    );\n}"
  },
  {
    "path": "app/dashboard/projects/[projectId]/integrations/slack/page.tsx",
    "content": "'use client';\n\nimport {useState, useEffect} from 'react';\nimport {useParams, useRouter} from 'next/navigation';\nimport {useQuery, useMutation, useQueryClient} from '@tanstack/react-query';\nimport {motion} from 'framer-motion';\nimport {z} from 'zod';\nimport {useForm} from 'react-hook-form';\nimport {zodResolver} from '@hookform/resolvers/zod';\n\n// UI Components\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card';\nimport {\n    Form,\n    FormControl,\n    FormDescription,\n    FormField,\n    FormItem,\n    FormLabel,\n    FormMessage,\n} from '@/components/ui/form';\nimport {Input} from '@/components/ui/input';\nimport {Button} from '@/components/ui/button';\nimport {Switch} from '@/components/ui/switch';\nimport {\n    Select,\n    SelectContent,\n    SelectItem,\n    SelectTrigger,\n    SelectValue,\n} from '@/components/ui/select';\nimport {\n    AlertDialog,\n    AlertDialogAction,\n    AlertDialogCancel,\n    AlertDialogContent,\n    AlertDialogDescription,\n    AlertDialogFooter,\n    AlertDialogHeader,\n    AlertDialogTitle,\n} from '@/components/ui/alert-dialog';\nimport {useToast} from '@/hooks/use-toast';\nimport {Alert, AlertDescription, AlertTitle} from '@/components/ui/alert';\nimport {\n    CheckIcon,\n    Loader2Icon,\n    AlertCircleIcon,\n    ArrowLeftIcon,\n    SendIcon,\n    TrashIcon,\n    LinkIcon,\n} from 'lucide-react';\nimport {SlackLogo} from '@/lib/services/slack/logo';\n\n// Define types\ninterface SlackIntegration {\n    id?: string;\n    projectId?: string;\n    accessToken?: string;\n    teamId: string;\n    teamName?: string;\n    botUserId: string;\n    botUsername?: string;\n    channelId: string;\n    channelName?: string;\n    autoSend: boolean;\n    enabled: boolean;\n    lastSyncAt?: Date | null;\n    lastErrorMessage?: string | null;\n    postCount: number;\n    createdAt?: Date;\n    updatedAt?: Date;\n}\n\ninterface Project {\n    id: string;\n    name: string;\n}\n\n// Form schema\nconst formSchema = z.object({\n    channelId: z.string().min(1, 'Channel ID is required'),\n    channelName: z.string().optional(),\n    autoSend: z.boolean().default(true),\n    enabled: z.boolean().default(true),\n});\n\ntype FormValues = z.infer<typeof formSchema>;\n\nexport default function SlackIntegrationPage() {\n    const params = useParams();\n    const router = useRouter();\n    const {toast} = useToast();\n    const queryClient = useQueryClient();\n    const projectId = params.projectId as string;\n\n    const [showDisconnectDialog, setShowDisconnectDialog] = useState(false);\n    const [isAuthenticating, setIsAuthenticating] = useState(false);\n\n    // Fetch system config to check if Slack is enabled\n    const {data: systemConfig} = useQuery({\n        queryKey: ['slack-system-config'],\n        queryFn: async () => {\n            const response = await fetch('/api/admin/system/slack');\n            if (!response.ok) return null;\n            return response.json();\n        },\n    });\n\n    // Fetch Slack integration config\n    const {data: integration, isLoading} = useQuery({\n        queryKey: ['slack-integration', projectId],\n        queryFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}/integrations/slack`);\n            if (!response.ok) {\n                if (response.status === 404) return null;\n                throw new Error('Failed to fetch Slack integration');\n            }\n            return response.json() as Promise<SlackIntegration>;\n        },\n    });\n\n    // Fetch project\n    const {data: project} = useQuery({\n        queryKey: ['project', projectId],\n        queryFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}`);\n            if (!response.ok) throw new Error('Failed to fetch project');\n            return response.json() as Promise<Project>;\n        },\n    });\n\n    // Fetch available Slack channels\n    const {data: channelsData, isLoading: isLoadingChannels} = useQuery({\n        queryKey: ['slack-channels', projectId],\n        queryFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}/integrations/slack/channels`);\n            if (!response.ok) {\n                if (response.status === 400) return null;\n                throw new Error('Failed to fetch channels');\n            }\n            return response.json();\n        },\n        enabled: !!integration, // Only fetch if integration is connected\n    });\n\n    const form = useForm<FormValues>({\n        resolver: zodResolver(formSchema),\n        defaultValues: {\n            channelId: integration?.channelId || '',\n            channelName: integration?.channelName || '',\n            autoSend: integration?.autoSend ?? true,\n            enabled: integration?.enabled ?? true,\n        },\n    });\n\n    // Update when integration loads\n    useEffect(() => {\n        if (integration) {\n            form.reset({\n                channelId: integration.channelId,\n                channelName: integration.channelName || '',\n                autoSend: integration.autoSend,\n                enabled: integration.enabled,\n            });\n        }\n    }, [integration, form]);\n\n    // Update settings mutation\n    const updateMutation = useMutation({\n        mutationFn: async (values: FormValues) => {\n            const response = await fetch(`/api/projects/${projectId}/integrations/slack`, {\n                method: 'PUT',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify(values),\n            });\n            if (!response.ok) throw new Error('Failed to update Slack integration');\n            return response.json();\n        },\n        onSuccess: () => {\n            queryClient.invalidateQueries({queryKey: ['slack-integration', projectId]});\n            toast({\n                title: 'Success',\n                description: 'Slack integration settings updated',\n            });\n        },\n        onError: (error) => {\n            toast({\n                title: 'Error',\n                description: error instanceof Error ? error.message : 'Failed to update settings',\n                variant: 'destructive',\n            });\n        },\n    });\n\n    // Disconnect mutation\n    const disconnectMutation = useMutation({\n        mutationFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}/integrations/slack`, {\n                method: 'DELETE',\n            });\n            if (!response.ok) throw new Error('Failed to disconnect Slack');\n            return response.json();\n        },\n        onSuccess: () => {\n            queryClient.invalidateQueries({queryKey: ['slack-integration', projectId]});\n            toast({\n                title: 'Success',\n                description: 'Slack integration disconnected',\n            });\n            setShowDisconnectDialog(false);\n        },\n        onError: (error) => {\n            toast({\n                title: 'Error',\n                description: error instanceof Error ? error.message : 'Failed to disconnect',\n                variant: 'destructive',\n            });\n        },\n    });\n\n    // Handle OAuth authentication\n    const handleConnectSlack = () => {\n        if (!systemConfig?.slackOAuthClientId) {\n            toast({\n                title: 'Error',\n                description: 'Slack OAuth is not configured. Please contact your administrator.',\n                variant: 'destructive',\n            });\n            return;\n        }\n\n        setIsAuthenticating(true);\n        const clientId = systemConfig.slackOAuthClientId;\n        const redirectUri = `${window.location.origin}/api/integrations/slack/callback`;\n        const scope = 'chat:write,channels:join,channels:read,groups:read,im:read,mpim:read,users:read';\n        const state = btoa(JSON.stringify({projectId}));\n\n        const authUrl = `https://slack.com/oauth/v2/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(state)}`;\n\n        window.location.href = authUrl;\n    };\n\n    // Handle form submission\n    const onSubmit = async (values: FormValues) => {\n        updateMutation.mutate(values);\n    };\n\n    if (isLoading) {\n        return (\n            <div className=\"space-y-4\">\n                <div className=\"h-32 bg-muted animate-pulse rounded-lg\"/>\n            </div>\n        );\n    }\n\n    // Check if Slack is not configured at system level\n    const isSlackConfigured = systemConfig?.slackOAuthEnabled;\n\n    return (\n        <div className=\"space-y-6\">\n            {/* Header */}\n            <div className=\"flex items-center justify-between\">\n                <div>\n                    <h1 className=\"text-3xl font-bold flex items-center gap-2\">\n                        <SlackLogo className=\"w-12 h-12\"/>\n                        Slack Integration\n                    </h1>\n                    <p className=\"text-muted-foreground mt-2\">\n                        Post changelog updates directly to your Slack workspace\n                    </p>\n                </div>\n                <Button\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    onClick={() => router.back()}\n                    className=\"gap-2\"\n                >\n                    <ArrowLeftIcon className=\"w-4 h-4\"/>\n                    Back\n                </Button>\n            </div>\n\n            {!isSlackConfigured ? (\n                // Not configured at system level\n                <motion.div\n                    initial={{opacity: 0, y: 20}}\n                    animate={{opacity: 1, y: 0}}\n                    transition={{duration: 0.3}}\n                >\n                    <Alert variant=\"warning\" borderStyle=\"accent\">\n                        <AlertTitle>Slack Not Configured</AlertTitle>\n                        <AlertDescription>\n                            The Slack integration has not been set up in system integrations.\n                            Please contact your administrator to configure Slack OAuth credentials.\n                        </AlertDescription>\n                    </Alert>\n                </motion.div>\n            ) : !integration ? (\n                // Not connected\n                <motion.div\n                    initial={{opacity: 0, y: 20}}\n                    animate={{opacity: 1, y: 0}}\n                    transition={{duration: 0.3}}\n                >\n                    <Card>\n                        <CardHeader>\n                            <CardTitle>Connect Slack Workspace</CardTitle>\n                            <CardDescription>\n                                Authorize Changerawr to post updates to your Slack workspace\n                            </CardDescription>\n                        </CardHeader>\n                        <CardContent className=\"space-y-4\">\n                            <Alert variant=\"info\" borderStyle=\"accent\">\n                                <AlertTitle>Authorization Required</AlertTitle>\n                                <AlertDescription>\n                                    You'll be redirected to Slack to authorize this integration. We'll request\n                                    permissions to post messages and read channels.\n                                </AlertDescription>\n                            </Alert>\n\n                            <Button\n                                onClick={handleConnectSlack}\n                                disabled={isAuthenticating}\n                                className=\"w-full gap-2 bg-[#36C5F0] hover:bg-[#1E90FF] text-white\"\n                            >\n                                {isAuthenticating ? (\n                                    <Loader2Icon className=\"w-4 h-4 animate-spin\"/>\n                                ) : (\n                                    <LinkIcon className=\"w-4 h-4\"/>\n                                )}\n                                {isAuthenticating ? 'Connecting...' : 'Connect Slack Workspace'}\n                            </Button>\n                        </CardContent>\n                    </Card>\n                </motion.div>\n            ) : (\n                // Connected\n                <motion.div\n                    initial={{opacity: 0, y: 20}}\n                    animate={{opacity: 1, y: 0}}\n                    transition={{duration: 0.3}}\n                    className=\"space-y-6\"\n                >\n                    {/* Connection Status */}\n                    <Card className=\"border-green-200 dark:border-green-900 bg-green-50 dark:bg-green-950\">\n                        <CardHeader>\n                            <CardTitle className=\"flex items-center gap-2 text-green-900 dark:text-green-100\">\n                                <CheckIcon className=\"w-5 h-5\"/>\n                                Connected to {integration.teamName}\n                            </CardTitle>\n                        </CardHeader>\n                        <CardContent className=\"space-y-4\">\n                            <div className=\"grid grid-cols-2 gap-4\">\n                                <div className=\"space-y-2\">\n                                    <p className=\"text-sm text-muted-foreground\">Workspace</p>\n                                    <p className=\"font-medium\">{integration.teamName || integration.teamId}</p>\n                                </div>\n                                <div className=\"space-y-2\">\n                                    <p className=\"text-sm text-muted-foreground\">Bot Username</p>\n                                    <p className=\"font-medium\">{integration.botUsername || integration.botUserId}</p>\n                                </div>\n                                <div className=\"space-y-2\">\n                                    <p className=\"text-sm text-muted-foreground\">Default Channel</p>\n                                    <p className=\"font-medium\">#{integration.channelName || integration.channelId}</p>\n                                </div>\n                                <div className=\"space-y-2\">\n                                    <p className=\"text-sm text-muted-foreground\">Messages Posted</p>\n                                    <p className=\"font-medium\">{integration.postCount}</p>\n                                </div>\n                            </div>\n\n                            {integration.lastErrorMessage && (\n                                <Alert variant=\"destructive\" borderStyle=\"accent\">\n                                    <AlertTitle>Last Error</AlertTitle>\n                                    <AlertDescription>\n                                        {integration.lastErrorMessage}\n                                    </AlertDescription>\n                                </Alert>\n                            )}\n                        </CardContent>\n                    </Card>\n\n                    {/* Settings Form */}\n                    <Card>\n                        <CardHeader>\n                            <CardTitle>Settings</CardTitle>\n                            <CardDescription>Configure how changelog updates are posted to Slack</CardDescription>\n                        </CardHeader>\n                        <CardContent>\n                            <Form {...form}>\n                                <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-6\">\n                                    {/* Channel Configuration */}\n                                    <div className=\"space-y-4\">\n                                        <h3 className=\"font-semibold\">Channel Configuration</h3>\n\n                                        {isLoadingChannels ? (\n                                            <div className=\"space-y-2\">\n                                                <p className=\"text-sm font-medium\">Channel</p>\n                                                <div className=\"h-10 bg-muted animate-pulse rounded-md\"/>\n                                            </div>\n                                        ) : (\n                                            <FormField\n                                                control={form.control}\n                                                name=\"channelId\"\n                                                render={({field}) => {\n                                                    const selectedChannel = channelsData?.channels?.find(\n                                                        (ch: any) => ch.id === field.value\n                                                    );\n                                                    const displayText = selectedChannel\n                                                        ? `${selectedChannel.isPrivate ? '🔒' : '#'} ${selectedChannel.name}`\n                                                        : \"Select a channel\";\n\n                                                    return (\n                                                        <FormItem>\n                                                            <FormLabel>Channel</FormLabel>\n                                                            <FormControl>\n                                                                <Select value={field.value || ''} onValueChange={field.onChange}>\n                                                                    <SelectTrigger>\n                                                                        <SelectValue placeholder={displayText}>\n                                                                            {displayText}\n                                                                        </SelectValue>\n                                                                    </SelectTrigger>\n                                                                    <SelectContent>\n                                                                        {channelsData?.channels?.map((channel: any) => (\n                                                                            <SelectItem key={channel.id} value={channel.id}>\n                                                                                {channel.isPrivate ? '🔒' : '#'} {channel.name}\n                                                                            </SelectItem>\n                                                                        ))}\n                                                                    </SelectContent>\n                                                                </Select>\n                                                            </FormControl>\n                                                            <FormDescription>\n                                                                Select the Slack channel where changelog updates will be posted\n                                                            </FormDescription>\n                                                            <FormMessage/>\n                                                        </FormItem>\n                                                    );\n                                                }}\n                                            />\n                                        )}\n\n                                        <FormField\n                                            control={form.control}\n                                            name=\"channelName\"\n                                            render={({field}) => (\n                                                <FormItem>\n                                                    <FormLabel>Channel Name (Optional)</FormLabel>\n                                                    <FormControl>\n                                                        <Input\n                                                            placeholder=\"changelog-updates\"\n                                                            {...field}\n                                                        />\n                                                    </FormControl>\n                                                    <FormDescription>\n                                                        Display name for the channel (for reference)\n                                                    </FormDescription>\n                                                    <FormMessage/>\n                                                </FormItem>\n                                            )}\n                                        />\n                                    </div>\n\n                                    {/* Posting Settings */}\n                                    <div className=\"space-y-4\">\n                                        <h3 className=\"font-semibold\">Posting Preferences</h3>\n\n                                        <FormField\n                                            control={form.control}\n                                            name=\"autoSend\"\n                                            render={({field}) => (\n                                                <FormItem className=\"flex items-center justify-between rounded-lg border p-4\">\n                                                    <div className=\"space-y-0.5\">\n                                                        <FormLabel>Auto-Post Updates</FormLabel>\n                                                        <FormDescription>\n                                                            Automatically post to Slack when a changelog entry is published\n                                                        </FormDescription>\n                                                    </div>\n                                                    <FormControl>\n                                                        <Switch\n                                                            checked={field.value}\n                                                            onCheckedChange={field.onChange}\n                                                        />\n                                                    </FormControl>\n                                                </FormItem>\n                                            )}\n                                        />\n\n                                        <FormField\n                                            control={form.control}\n                                            name=\"enabled\"\n                                            render={({field}) => (\n                                                <FormItem className=\"flex items-center justify-between rounded-lg border p-4\">\n                                                    <div className=\"space-y-0.5\">\n                                                        <FormLabel>Enable Integration</FormLabel>\n                                                        <FormDescription>\n                                                            Disable to pause all Slack posting\n                                                        </FormDescription>\n                                                    </div>\n                                                    <FormControl>\n                                                        <Switch\n                                                            checked={field.value}\n                                                            onCheckedChange={field.onChange}\n                                                        />\n                                                    </FormControl>\n                                                </FormItem>\n                                            )}\n                                        />\n                                    </div>\n\n                                    {/* Action Buttons */}\n                                    <div className=\"flex gap-2\">\n                                        <Button\n                                            type=\"submit\"\n                                            disabled={updateMutation.isPending}\n                                            className=\"gap-2\"\n                                        >\n                                            {updateMutation.isPending ? (\n                                                <Loader2Icon className=\"w-4 h-4 animate-spin\"/>\n                                            ) : (\n                                                <CheckIcon className=\"w-4 h-4\"/>\n                                            )}\n                                            Save Settings\n                                        </Button>\n\n                                        <Button\n                                            type=\"button\"\n                                            variant=\"destructive\"\n                                            onClick={() => setShowDisconnectDialog(true)}\n                                            className=\"gap-2\"\n                                        >\n                                            <TrashIcon className=\"w-4 h-4\"/>\n                                            Disconnect\n                                        </Button>\n                                    </div>\n                                </form>\n                            </Form>\n                        </CardContent>\n                    </Card>\n\n                    {/* Disconnect Dialog */}\n                    <AlertDialog open={showDisconnectDialog} onOpenChange={setShowDisconnectDialog}>\n                        <AlertDialogContent>\n                            <AlertDialogHeader>\n                                <AlertDialogTitle>Disconnect Slack?</AlertDialogTitle>\n                                <AlertDialogDescription>\n                                    This will remove the Slack integration from {project?.name}. You can\n                                    reconnect anytime.\n                                </AlertDialogDescription>\n                            </AlertDialogHeader>\n                            <AlertDialogFooter>\n                                <AlertDialogCancel>Cancel</AlertDialogCancel>\n                                <AlertDialogAction\n                                    onClick={() => disconnectMutation.mutate()}\n                                    disabled={disconnectMutation.isPending}\n                                    className=\"bg-destructive\"\n                                >\n                                    {disconnectMutation.isPending ? (\n                                        <Loader2Icon className=\"w-4 h-4 animate-spin\"/>\n                                    ) : (\n                                        'Disconnect'\n                                    )}\n                                </AlertDialogAction>\n                            </AlertDialogFooter>\n                        </AlertDialogContent>\n                    </AlertDialog>\n                </motion.div>\n            )}\n        </div>\n    );\n}"
  },
  {
    "path": "app/dashboard/projects/[projectId]/integrations/widget/[widgetId]/page.tsx",
    "content": "import {db} from '@/lib/db';\nimport {notFound} from 'next/navigation';\nimport WidgetEditor from './widget-editor';\n\ninterface WidgetEditPageProps {\n    params: Promise<{ projectId: string; widgetId: string }>;\n}\n\nexport default async function WidgetEditPage({params}: WidgetEditPageProps) {\n    const {projectId, widgetId} = await params;\n\n    const widget = await db.widget.findUnique({\n        where: {id: widgetId},\n        include: {project: {select: {name: true, isPublic: true}}},\n    });\n\n    if (!widget || widget.projectId !== projectId) {\n        notFound();\n    }\n\n    // ✅ rename Project → project to match WidgetEditor’s prop type\n    const normalizedWidget = {\n        ...widget,\n        project: widget.project,\n    };\n\n    return <WidgetEditor widget={normalizedWidget} projectId={projectId}/>;\n}\n"
  },
  {
    "path": "app/dashboard/projects/[projectId]/integrations/widget/[widgetId]/widget-editor.tsx",
    "content": "'use client';\n\nimport {useState, useEffect, useRef} from 'react';\nimport {Widget, Project} from '@prisma/client';\nimport {Button} from '@/components/ui/button';\nimport {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@/components/ui/card';\nimport {Input} from '@/components/ui/input';\nimport {Label} from '@/components/ui/label';\nimport {Textarea} from '@/components/ui/textarea';\nimport {Switch} from '@/components/ui/switch';\nimport {ArrowLeft, Copy, Check, RefreshCw} from 'lucide-react';\nimport {useRouter} from 'next/navigation';\nimport {toast} from '@/hooks/use-toast';\nimport {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs';\nimport {Light as SyntaxHighlighter} from 'react-syntax-highlighter';\nimport {atomOneDark, atomOneLight} from 'react-syntax-highlighter/dist/esm/styles/hljs';\nimport {useTheme} from 'next-themes';\nimport javascript from 'react-syntax-highlighter/dist/esm/languages/hljs/javascript';\nimport xml from 'react-syntax-highlighter/dist/esm/languages/hljs/xml';\nimport typescript from 'react-syntax-highlighter/dist/esm/languages/hljs/typescript';\nimport go from 'react-syntax-highlighter/dist/esm/languages/hljs/go';\n\nimport 'dotenv/config';\n\n// Register languages\nSyntaxHighlighter.registerLanguage('javascript', javascript);\nSyntaxHighlighter.registerLanguage('xml', xml);\nSyntaxHighlighter.registerLanguage('typescript', typescript);\nSyntaxHighlighter.registerLanguage('go', go);\n\n// Custom language definitions\nSyntaxHighlighter.registerLanguage('vue', () => ({\n    contains: [\n        {\n            className: 'tag',\n            begin: '<template',\n            end: '>',\n            starts: {\n                end: '</template>',\n                returnEnd: true,\n                subLanguage: 'xml'\n            }\n        },\n        {\n            className: 'tag',\n            begin: '<script',\n            end: '>',\n            starts: {\n                end: '</script>',\n                returnEnd: true,\n                subLanguage: 'javascript'\n            }\n        }\n    ]\n}));\n\nSyntaxHighlighter.registerLanguage('svelte', () => ({\n    contains: [\n        {\n            className: 'tag',\n            begin: '<script',\n            end: '>',\n            starts: {\n                end: '</script>',\n                returnEnd: true,\n                subLanguage: 'javascript'\n            }\n        },\n        {\n            className: 'template',\n            begin: '{',\n            end: '}',\n            subLanguage: 'javascript'\n        },\n        {\n            className: 'tag',\n            begin: '<[A-Za-z]',\n            end: '>',\n            contains: [\n                {\n                    className: 'attr',\n                    begin: ' [A-Za-z]+=',\n                    end: /(?=\\s|$)/,\n                    contains: [\n                        {\n                            className: 'string',\n                            begin: '\"',\n                            end: '\"'\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}));\n\ninterface WidgetEditorProps {\n    widget: Widget & { project: { name: string; isPublic: boolean } };\n    projectId: string;\n}\n\nexport default function WidgetEditor({widget: initialWidget, projectId}: WidgetEditorProps) {\n    const router = useRouter();\n    const {theme} = useTheme();\n    const [widget, setWidget] = useState(initialWidget);\n    const [saving, setSaving] = useState(false);\n    const [copied, setCopied] = useState(false);\n    const [previewKey, setPreviewKey] = useState(0);\n    const iframeRef = useRef<HTMLIFrameElement>(null);\n\n    const getEmbedCode = (language: string) => {\n        const scriptUrl = `${window.location.origin}/api/integrations/widget/${projectId}/${widget.id}`;\n\n        switch (language) {\n            case 'HTML':\n                return `<script src=\"${scriptUrl}\" async></script>`;\n\n            case 'React':\n                return `import { useEffect } from 'react';\n\nexport default function Changelog() {\n  useEffect(() => {\n    const script = document.createElement('script');\n    script.src = '${scriptUrl}';\n    script.async = true;\n    document.body.appendChild(script);\n\n    return () => {\n      document.body.removeChild(script);\n    };\n  }, []);\n\n  return <div id=\"changerawr-changelog\" />;\n}`;\n\n            case 'Vue':\n                return `<template>\n  <div id=\"changerawr-changelog\"></div>\n</template>\n\n<script>\nexport default {\n  mounted() {\n    const script = document.createElement('script');\n    script.src = '${scriptUrl}';\n    script.async = true;\n    document.body.appendChild(script);\n  }\n}\n</script>`;\n\n            case 'Svelte':\n                return `<script>\n  import { onMount } from 'svelte';\n\n  onMount(() => {\n    const script = document.createElement('script');\n    script.src = '${scriptUrl}';\n    script.async = true;\n    document.body.appendChild(script);\n\n    return () => {\n      document.body.removeChild(script);\n    };\n  });\n</script>\n\n<div id=\"changerawr-changelog\"></div>`;\n\n            case 'Angular':\n                return `import { Component, OnInit } from '@angular/core';\n\n@Component({\n  selector: 'app-changelog',\n  template: '<div id=\"changerawr-changelog\"></div>'\n})\nexport class ChangelogComponent implements OnInit {\n  ngOnInit() {\n    const script = document.createElement('script');\n    script.src = '${scriptUrl}';\n    script.async = true;\n    document.body.appendChild(script);\n  }\n}`;\n\n            case 'Go':\n                return `package main\n\nimport \"html/template\"\n\nfunc ChangelogWidget() template.HTML {\n    return template.HTML(\\`<script src=\"${scriptUrl}\" async></script>\\`)\n}`;\n\n            default:\n                return '';\n        }\n    };\n\n    const getLanguageForHighlighter = (lang: string) => {\n        switch (lang) {\n            case 'HTML':\n                return 'xml';\n            case 'React':\n                return 'javascript';\n            case 'Vue':\n                return 'vue';\n            case 'Svelte':\n                return 'svelte';\n            case 'Angular':\n                return 'typescript';\n            case 'Go':\n                return 'go';\n            default:\n                return 'javascript';\n        }\n    };\n\n    const handleSave = async () => {\n        setSaving(true);\n        try {\n            const res = await fetch(`/api/integrations/widget/${projectId}/${widget.id}`, {\n                method: 'PUT',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({\n                    name: widget.name,\n                    customCSS: widget.customCSS,\n                    isActive: widget.isActive,\n                }),\n            });\n\n            if (!res.ok) throw new Error('Failed to save');\n\n            toast({title: 'Success', description: 'Widget saved successfully'});\n            router.refresh();\n        } catch (error) {\n            toast({title: 'Error', description: 'Failed to save widget', variant: 'destructive'});\n        } finally {\n            setSaving(false);\n        }\n    };\n\n    const copyEmbedCode = () => {\n        const code = `<script src=\"${window.location.origin}/api/integrations/widget/${projectId}/${widget.id}\" async></script>`;\n        navigator.clipboard.writeText(code);\n        setCopied(true);\n        toast({title: 'Success', description: 'Embed code copied!'});\n        setTimeout(() => setCopied(false), 2000);\n    };\n\n    const refreshPreview = () => {\n        setPreviewKey(prev => prev + 1);\n    };\n\n    const getPreviewHTML = () => {\n        const scriptUrl = `${window.location.origin}/api/integrations/widget/${projectId}/${widget.id}`;\n\n        // Different preview layouts based on variant\n        const variantSpecificContent = {\n            classic: `\n                <div style=\"max-width: 600px; margin: 0 auto;\">\n                    <h2 style=\"font-family: system-ui; margin-bottom: 1rem;\">Widget Preview</h2>\n                    <script src=\"${scriptUrl}\" async></script>\n                </div>\n            `,\n            floating: `\n                <div style=\"height: 400px; position: relative; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\">\n                    <div style=\"padding: 2rem; color: white; font-family: system-ui;\">\n                        <h2>Your Website Content</h2>\n                        <p>The floating widget appears in the corner →</p>\n                    </div>\n                    <script src=\"${scriptUrl}\" async></script>\n                </div>\n            `,\n            modal: `\n                <div style=\"padding: 2rem; font-family: system-ui; text-align: center;\">\n                    <h2>Modal Widget Preview</h2>\n                    <p style=\"margin: 1rem 0; color: #666;\">Click the button below to open the modal</p>\n                    <button id=\"modal-trigger\" style=\"padding: 0.75rem 1.5rem; background: #0066ff; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; font-weight: 600;\">\n                        Open Changelog\n                    </button>\n                    <script src=\"${scriptUrl}\" data-trigger=\"modal-trigger\" async></script>\n                </div>\n            `,\n            announcement: `\n                <div style=\"padding-top: 4rem; font-family: system-ui;\">\n                    <div style=\"max-width: 800px; margin: 0 auto; padding: 2rem;\">\n                        <h2>Announcement Bar Preview</h2>\n                        <p style=\"color: #666;\">The announcement bar appears at the top of the page ↑</p>\n                    </div>\n                    <script src=\"${scriptUrl}\" async></script>\n                </div>\n            `\n        };\n\n        const content = variantSpecificContent[widget.variant as keyof typeof variantSpecificContent] || variantSpecificContent.classic;\n\n        return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Widget Preview</title>\n    <style>\n        * { margin: 0; padding: 0; box-sizing: border-box; }\n        body {\n            font-family: system-ui, -apple-system, sans-serif;\n            background: ${theme === 'dark' ? '#0f172a' : '#f8fafc'};\n            color: ${theme === 'dark' ? '#e2e8f0' : '#1e293b'};\n            min-height: 100vh;\n        }\n    </style>\n</head>\n<body>\n    ${content}\n</body>\n</html>`;\n    };\n\n    useEffect(() => {\n        if (iframeRef.current) {\n            const doc = iframeRef.current.contentDocument;\n            if (doc) {\n                doc.open();\n                doc.write(getPreviewHTML());\n                doc.close();\n            }\n        }\n    }, [previewKey, widget.id, widget.variant, theme]);\n\n    return (\n        <div className=\"space-y-6\">\n            <div className=\"flex items-center gap-4\">\n                <Button\n                    variant=\"outline\"\n                    size=\"sm\"\n                    onClick={() => router.push(`/dashboard/projects/${projectId}/integrations/widget`)}\n                >\n                    <ArrowLeft className=\"w-4 h-4 mr-2\"/>\n                    Back\n                </Button>\n                <div>\n                    <h1 className=\"text-3xl font-bold\">Edit Widget</h1>\n                    <p className=\"text-muted-foreground\">{widget.variant} variant</p>\n                </div>\n            </div>\n\n            <div className=\"grid gap-6 lg:grid-cols-2\">\n                <div className=\"space-y-6\">\n                    <Card>\n                        <CardHeader>\n                            <CardTitle>Widget Settings</CardTitle>\n                            <CardDescription>Configure your widget</CardDescription>\n                        </CardHeader>\n                        <CardContent className=\"space-y-4\">\n                            <div className=\"space-y-2\">\n                                <Label htmlFor=\"name\">Widget Name</Label>\n                                <Input\n                                    id=\"name\"\n                                    value={widget.name}\n                                    onChange={(e) => setWidget({...widget, name: e.target.value})}\n                                />\n                            </div>\n\n                            <div className=\"space-y-2\">\n                                <Label htmlFor=\"variant\">Variant</Label>\n                                <Input id=\"variant\" value={widget.variant} disabled/>\n                                <p className=\"text-xs text-muted-foreground\">\n                                    Variant cannot be changed after creation\n                                </p>\n                            </div>\n\n                            <div className=\"flex items-center justify-between\">\n                                <div className=\"space-y-0.5\">\n                                    <Label>Active</Label>\n                                    <p className=\"text-xs text-muted-foreground\">\n                                        Inactive widgets won't load on your site\n                                    </p>\n                                </div>\n                                <Switch\n                                    checked={widget.isActive}\n                                    onCheckedChange={(checked) =>\n                                        setWidget({...widget, isActive: checked})\n                                    }\n                                />\n                            </div>\n\n                            <div className=\"space-y-2\">\n                                <Label htmlFor=\"customCSS\">Custom CSS</Label>\n                                <Textarea\n                                    id=\"customCSS\"\n                                    value={widget.customCSS || ''}\n                                    onChange={(e) =>\n                                        setWidget({...widget, customCSS: e.target.value || null})\n                                    }\n                                    placeholder=\".changerawr-widget { border-radius: 12px; }\"\n                                    className=\"font-mono text-sm\"\n                                    rows={8}\n                                />\n                                <p className=\"text-xs text-muted-foreground\">\n                                    Add custom CSS to style your widget. Uses CSS custom properties.\n                                </p>\n                            </div>\n\n                            <Button onClick={handleSave} disabled={saving} className=\"w-full\">\n                                {saving ? 'Saving...' : 'Save Changes'}\n                            </Button>\n                        </CardContent>\n                    </Card>\n                </div>\n\n                <div className=\"space-y-6\">\n                    <Card>\n                        <CardHeader>\n                            <CardTitle>Embed Code</CardTitle>\n                            <CardDescription>Add this code to your website</CardDescription>\n                        </CardHeader>\n                        <CardContent>\n                            <Tabs defaultValue=\"HTML\" className=\"w-full\">\n                                <TabsList className=\"grid w-full grid-cols-6\">\n                                    <TabsTrigger value=\"HTML\">HTML</TabsTrigger>\n                                    <TabsTrigger value=\"React\">React</TabsTrigger>\n                                    <TabsTrigger value=\"Vue\">Vue</TabsTrigger>\n                                    <TabsTrigger value=\"Svelte\">Svelte</TabsTrigger>\n                                    <TabsTrigger value=\"Angular\">Angular</TabsTrigger>\n                                    <TabsTrigger value=\"Go\">Go</TabsTrigger>\n                                </TabsList>\n                                {['HTML', 'React', 'Vue', 'Svelte', 'Angular', 'Go'].map((lang) => (\n                                    <TabsContent key={lang} value={lang} className=\"space-y-2\">\n                                        <div className=\"relative\">\n                                            <SyntaxHighlighter\n                                                language={getLanguageForHighlighter(lang)}\n                                                style={theme === 'dark' ? atomOneDark : atomOneLight}\n                                                customStyle={{\n                                                    borderRadius: '0.5rem',\n                                                    fontSize: '0.875rem',\n                                                    margin: 0,\n                                                }}\n                                            >\n                                                {getEmbedCode(lang)}\n                                            </SyntaxHighlighter>\n                                            <Button\n                                                variant=\"outline\"\n                                                size=\"sm\"\n                                                className=\"absolute top-2 right-2\"\n                                                onClick={() => {\n                                                    navigator.clipboard.writeText(getEmbedCode(lang));\n                                                    setCopied(true);\n                                                    toast({title: 'Success', description: 'Code copied!'});\n                                                    setTimeout(() => setCopied(false), 2000);\n                                                }}\n                                            >\n                                                {copied ? <Check className=\"w-4 h-4\"/> : <Copy className=\"w-4 h-4\"/>}\n                                            </Button>\n                                        </div>\n                                    </TabsContent>\n                                ))}\n                            </Tabs>\n\n                            {widget.variant === 'classic' && (\n                                <div className=\"space-y-2 pt-4 border-t mt-4\">\n                                    <p className=\"text-sm font-medium\">Classic Widget Options</p>\n                                    <p className=\"text-xs text-muted-foreground\">\n                                        Add data attributes to customize:\n                                    </p>\n                                    <code className=\"block px-3 py-2 bg-muted rounded text-xs font-mono\">\n                                        data-theme=\"dark\"<br/>\n                                        data-popup=\"true\"<br/>\n                                        data-position=\"bottom-right\"<br/>\n                                        data-max-entries=\"5\"\n                                    </code>\n                                </div>\n                            )}\n                        </CardContent>\n                    </Card>\n\n                    <Card>\n                        <CardHeader>\n                            <div className=\"flex items-center justify-between\">\n                                <div>\n                                    <CardTitle>Preview</CardTitle>\n                                    <CardDescription>See how your widget looks</CardDescription>\n                                </div>\n                                <Button\n                                    variant=\"outline\"\n                                    size=\"sm\"\n                                    onClick={refreshPreview}\n                                >\n                                    <RefreshCw className=\"w-4 h-4 mr-2\"/>\n                                    Refresh\n                                </Button>\n                            </div>\n                        </CardHeader>\n                        <CardContent>\n                            <div className=\"border rounded-lg overflow-hidden bg-background\">\n                                <iframe\n                                    ref={iframeRef}\n                                    key={previewKey}\n                                    title=\"Widget Preview\"\n                                    className=\"w-full h-[500px] border-0\"\n                                    sandbox=\"allow-scripts allow-same-origin\"\n                                />\n                            </div>\n                            <div className=\"mt-4 flex items-start gap-2 text-sm text-muted-foreground\">\n                                <span className=\"text-yellow-600 dark:text-yellow-400\">ℹ️</span>\n                                <p>\n                                    This is a live preview of your widget. Changes to custom CSS require saving first,\n                                    then refresh the preview.\n                                    {!widget.project.isPublic && ' Your project is private, so the widget may not load properly.'}\n                                </p>\n                            </div>\n                        </CardContent>\n                    </Card>\n                </div>\n            </div>\n        </div>\n    );\n}\n"
  },
  {
    "path": "app/dashboard/projects/[projectId]/integrations/widget/page.tsx",
    "content": "import {db} from '@/lib/db';\nimport WidgetList from './widget-list';\n\ninterface WidgetPageProps {\n    params: Promise<{ projectId: string }>;\n}\n\nexport default async function WidgetPage({params}: WidgetPageProps) {\n    const {projectId} = await params;\n\n    // Fetch widgets and project\n    const [widgets, project] = await Promise.all([\n        db.widget.findMany({\n            where: {projectId},\n            orderBy: {createdAt: 'desc'}\n        }),\n        db.project.findUnique({\n            where: {id: projectId},\n            select: {id: true, name: true, isPublic: true}\n        })\n    ]);\n\n    if (!project) {\n        return <div>Project not found</div>;\n    }\n\n    return <WidgetList projectId={projectId} initialWidgets={widgets} project={project}/>;\n}\n"
  },
  {
    "path": "app/dashboard/projects/[projectId]/integrations/widget/widget-config.tsx",
    "content": "'use client'\n\nimport { useEffect, useMemo, useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport { useQuery } from '@tanstack/react-query'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'\nimport { Switch } from '@/components/ui/switch'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'\nimport { Label } from '@/components/ui/label'\nimport { Input } from '@/components/ui/input'\nimport { Button } from '@/components/ui/button'\nimport { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'\nimport { ArrowLeft, Check, Code2, Copy, Eye, Globe, Palette, Settings, Settings2 } from 'lucide-react'\nimport WidgetPreview from '@/components/changelog/WidgetPreview'\nimport { Separator } from '@/components/ui/separator'\nimport { ScrollArea } from \"@/components/ui/scroll-area\"\nimport {Light as SyntaxHighlighter} from 'react-syntax-highlighter'\nimport {atomOneDark, atomOneLight} from 'react-syntax-highlighter/dist/esm/styles/hljs'\nimport { useTheme } from 'next-themes'\nimport javascript from 'react-syntax-highlighter/dist/esm/languages/hljs/javascript'\nimport xml from 'react-syntax-highlighter/dist/esm/languages/hljs/xml'\nimport typescript from 'react-syntax-highlighter/dist/esm/languages/hljs/typescript'\nimport go from 'react-syntax-highlighter/dist/esm/languages/hljs/go'\n\n// Register languages\nSyntaxHighlighter.registerLanguage('javascript', javascript)\nSyntaxHighlighter.registerLanguage('xml', xml)\nSyntaxHighlighter.registerLanguage('typescript', typescript)\nSyntaxHighlighter.registerLanguage('go', go)\n\n// Custom language definitions\nSyntaxHighlighter.registerLanguage('vue', () => ({\n    contains: [\n        {\n            className: 'tag',\n            begin: '<template',\n            end: '>',\n            starts: {\n                end: '</template>',\n                returnEnd: true,\n                subLanguage: 'xml'\n            }\n        },\n        {\n            className: 'tag',\n            begin: '<script',\n            end: '>',\n            starts: {\n                end: '</script>',\n                returnEnd: true,\n                subLanguage: 'javascript'\n            }\n        }\n    ]\n}))\n\nSyntaxHighlighter.registerLanguage('svelte', () => ({\n    contains: [\n        {\n            className: 'tag',\n            begin: '<script',\n            end: '>',\n            starts: {\n                end: '</script>',\n                returnEnd: true,\n                subLanguage: 'javascript'\n            }\n        },\n        {\n            className: 'template',\n            begin: '{',\n            end: '}',\n            subLanguage: 'javascript'\n        },\n        {\n            className: 'tag',\n            begin: '<[A-Za-z]',\n            end: '>',\n            contains: [\n                {\n                    className: 'attr',\n                    begin: ' [A-Za-z]+=',\n                    end: /(?=\\s|$)/,\n                    contains: [\n                        {\n                            className: 'string',\n                            begin: '\"',\n                            end: '\"'\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n}))\n\n// Configuration constants\nconst MIN_ENTRIES = 1;\nconst MAX_ENTRIES = 10;\nconst DEFAULT_ENTRIES = 3;\n\n// Type definitions\nexport interface WidgetConfig {\n    theme: 'light' | 'dark'\n    isPopup: boolean\n    position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'\n    maxEntries: number\n    maxHeight: string\n    trigger: string\n}\n\n// Code example interface\ninterface CodeExample {\n    language: string\n    template: (config: WidgetConfig & { projectId: string }) => string\n}\n\n// Code examples collection\nconst CODE_EXAMPLES: CodeExample[] = [\n    {\n        language: 'HTML',\n        template: (config) => {\n            const attributes = [\n                config.isPopup ? 'data-popup=\"true\"' : '',\n                `data-theme=\"${config.theme}\"`,\n                config.isPopup ? `data-position=\"${config.position}\"` : '',\n                config.maxEntries !== DEFAULT_ENTRIES ? `data-max-entries=\"${config.maxEntries}\"` : '',\n                config.maxHeight !== '400px' ? `data-max-height=\"${config.maxHeight}\"` : '',\n                config.trigger ? `data-trigger=\"${config.trigger}\"` : '',\n            ].filter(Boolean).join('\\n    ')\n\n            return `${config.isPopup && config.theme === 'dark' ? '<div style=\"--theme: dark;\">\\n' : ''}${config.trigger ? `<button id=\"${config.trigger}\">View Updates</button>\\n` : ''}<script \n    src=\"${window.location.origin}/api/integrations/widget/${config.projectId}\"\n    ${attributes}\n    async\n></script>${config.isPopup && config.theme === 'dark' ? '\\n</div>' : ''}`\n        }\n    },\n    {\n        language: 'React',\n        template: (config) => {\n            const attributes = [\n                config.isPopup ? 'data-popup=\"true\"' : '',\n                `data-theme=\"${config.theme}\"`,\n                config.isPopup ? `data-position=\"${config.position}\"` : '',\n                config.maxEntries !== DEFAULT_ENTRIES ? `data-max-entries=\"${config.maxEntries}\"` : '',\n                config.maxHeight !== '400px' ? `data-max-height=\"${config.maxHeight}\"` : '',\n                config.trigger ? `data-trigger=\"${config.trigger}\"` : '',\n            ].filter(Boolean)\n\n            return `import React, { useEffect } from 'react';\n\nexport default function ChangelogWidget() {\n    useEffect(() => {\n        const script = document.createElement('script');\n        script.src = '${window.location.origin}/api/integrations/widget/${config.projectId}';\n        script.async = true;\n        ${attributes.map(attr => {\n                const [key, value] = attr.split('=');\n                return `        script.setAttribute('${key}', ${value});`\n            }).join('\\n')}\n        document.body.appendChild(script);\n\n        return () => {\n            document.body.removeChild(script);\n        };\n    }, []);\n\n    ${config.trigger ? `return <button id=\"${config.trigger}\">View Updates</button>;` : 'return null;'}\n}`\n        }\n    },\n    {\n        language: 'Vue',\n        template: (config) => {\n            const attributes = [\n                config.isPopup ? 'data-popup=\"true\"' : '',\n                `data-theme=\"${config.theme}\"`,\n                config.isPopup ? `data-position=\"${config.position}\"` : '',\n                config.maxEntries !== DEFAULT_ENTRIES ? `data-max-entries=\"${config.maxEntries}\"` : '',\n                config.maxHeight !== '400px' ? `data-max-height=\"${config.maxHeight}\"` : '',\n                config.trigger ? `data-trigger=\"${config.trigger}\"` : '',\n            ].filter(Boolean)\n\n            return `<template>\n    <div>\n        ${config.trigger ? `<button id=\"${config.trigger}\">View Updates</button>` : ''}\n    </div>\n</template>\n\n<script setup>\nimport { onMounted, onUnmounted } from 'vue'\n\nconst createChangelog = () => {\n    const script = document.createElement('script')\n    script.src = '${window.location.origin}/api/integrations/widget/${config.projectId}'\n    script.async = true\n    ${attributes.map(attr => {\n                const [key, value] = attr.split('=');\n                return `    script.setAttribute('${key}', ${value});`\n            }).join('\\n')}\n    document.body.appendChild(script)\n    return script\n}\n\nconst script = onMounted(createChangelog)\nonUnmounted(() => {\n    if (script.value) {\n        document.body.removeChild(script.value)\n    }\n})\n</script>`\n        }\n    },\n    {\n        language: 'Go',\n        template: (config) => `package main\n\nimport (\n    \"fmt\"\n    \"html/template\"\n    \"net/http\"\n)\n\nfunc renderChangelog(w http.ResponseWriter, r *http.Request) {\n    tmpl := template.Must(template.New(\"changelog\").Parse(\\`\n        {{if .Popup}}<div style=\"--theme: {{.Theme}};\">{{end}}\n        {{if .Trigger}}<button id=\"{{.Trigger}}\">View Updates</button>{{end}}\n        <script \n            src=\"{{.WidgetSrc}}\"\n            ${config.isPopup ? 'data-popup=\"true\"' : ''}\n            data-theme=\"{{.Theme}}\"\n            {{if .Popup}}data-position=\"{{.Position}}\"{{end}}\n            data-max-entries=\"{{.MaxEntries}}\"\n            data-max-height=\"{{.MaxHeight}}\"\n            {{if .Trigger}}data-trigger=\"{{.Trigger}}\"{{end}}\n            async\n        ></script>\n        {{if .Popup}}</div>{{end}}\n    \\`))\n\n    data := struct {\n        WidgetSrc   string\n        Popup       bool\n        Theme       string\n        Position    string\n        MaxEntries  int\n        MaxHeight   string\n        Trigger     string\n    }{\n        WidgetSrc:   \"${window.location.origin}/api/integrations/widget/${config.projectId}\",\n        Popup:       ${config.isPopup},\n        Theme:       \"${config.theme}\",\n        Position:    \"${config.position}\",\n        MaxEntries:  ${config.maxEntries},\n        MaxHeight:   \"${config.maxHeight}\",\n        Trigger:     \"${config.trigger}\",\n    }\n\n    err := tmpl.Execute(w, data)\n    if err != nil {\n        http.Error(w, err.Error(), http.StatusInternalServerError)\n    }\n}`\n    },\n    {\n        language: 'Svelte',\n        template: (config) => {\n            const attributes = [\n                config.isPopup ? 'data-popup=\"true\"' : '',\n                `data-theme=\"${config.theme}\"`,\n                config.isPopup ? `data-position=\"${config.position}\"` : '',\n                config.maxEntries !== DEFAULT_ENTRIES ? `data-max-entries=\"${config.maxEntries}\"` : '',\n                config.maxHeight !== '400px' ? `data-max-height=\"${config.maxHeight}\"` : '',\n                config.trigger ? `data-trigger=\"${config.trigger}\"` : '',\n            ].filter(Boolean)\n\n            return `<script>\n    import { onMount, onDestroy } from 'svelte';\n\n    let script;\n\n    onMount(() => {\n        script = document.createElement('script');\n        script.src = '${window.location.origin}/api/integrations/widget/${config.projectId}';\n        script.async = true;\n        ${attributes.map(attr => {\n                const [key, value] = attr.split('=');\n                return `        script.setAttribute('${key}', ${value});`\n            }).join('\\n')}\n        document.body.appendChild(script);\n    });\n\n    onDestroy(() => {\n        if (script) {\n            document.body.removeChild(script);\n        }\n    });\n</script>\n\n${config.trigger ? `<button id=\"${config.trigger}\">View Updates</button>` : ''}`\n        }\n    },\n    {\n        language: 'Angular',\n        template: (config) => {\n            const attributes = [\n                config.isPopup ? 'data-popup=\"true\"' : '',\n                `data-theme=\"${config.theme}\"`,\n                config.isPopup ? `data-position=\"${config.position}\"` : '',\n                config.maxEntries !== DEFAULT_ENTRIES ? `data-max-entries=\"${config.maxEntries}\"` : '',\n                config.maxHeight !== '400px' ? `data-max-height=\"${config.maxHeight}\"` : '',\n                config.trigger ? `data-trigger=\"${config.trigger}\"` : '',\n            ].filter(Boolean)\n\n            return `import { Component, OnInit, OnDestroy } from '@angular/core';\n\n@Component({\n    selector: 'app-changelog-widget',\n    template: \\`\n        ${config.trigger ? `<button id=\"${config.trigger}\">View Updates</button>` : ''}\n    \\`\n})\nexport class ChangelogWidgetComponent implements OnInit, OnDestroy {\n    private script: HTMLScriptElement;\n\n    ngOnInit() {\n        this.script = document.createElement('script');\n        this.script.src = '${window.location.origin}/api/integrations/widget/${config.projectId}';\n        this.script.async = true;\n        ${attributes.map(attr => {\n                const [key, value] = attr.split('=');\n                return `        this.script.setAttribute('${key}', ${value});`\n            }).join('\\n')}\n        document.body.appendChild(this.script);\n    }\n\n    ngOnDestroy() {\n        if (this.script) {\n            document.body.removeChild(this.script);\n        }\n    }\n}`\n        }\n    }\n]\n\ninterface CodeInstallationProps {\n    config: WidgetConfig;\n    projectId: string;\n    codeExamples: CodeExample[];\n}\n\nfunction CodeInstallation({ config, projectId, codeExamples }: CodeInstallationProps) {\n    const [currentLanguage, setCurrentLanguage] = useState('HTML')\n    const [copied, setCopied] = useState(false)\n    const { theme } = useTheme()\n\n    const generateEmbedCode = useMemo(() => {\n        const currentExample = codeExamples.find(ex => ex.language === currentLanguage)\n        return currentExample ? currentExample.template({...config, projectId}) : ''\n    }, [config, currentLanguage, projectId, codeExamples])\n\n    const handleCopy = async () => {\n        await navigator.clipboard.writeText(generateEmbedCode)\n        setCopied(true)\n        setTimeout(() => setCopied(false), 2000)\n    }\n\n    type LanguageMap = {\n        [key: string]: string;\n        HTML: string;\n        React: string;\n        Vue: string;\n        Go: string;\n        Svelte: string;\n        Angular: string;\n    };\n\n    const languageMap: LanguageMap = {\n        'HTML': 'xml',\n        'React': 'javascript',\n        'Vue': 'vue',\n        'Go': 'go',\n        'Svelte': 'svelte',\n        'Angular': 'typescript'\n    }\n\n    return (\n        <Card>\n            <CardHeader>\n                <CardTitle className=\"flex items-center gap-2\">\n                    <Globe className=\"h-5 w-5\" />\n                    Installation Code\n                </CardTitle>\n                <CardDescription>\n                    Copy and paste this code into your website\n                </CardDescription>\n            </CardHeader>\n            <CardContent className=\"p-0\">\n                <div className=\"relative\">\n                    <div className=\"flex items-center justify-between p-4 border-b\">\n                        <div className=\"flex gap-2\">\n                            {codeExamples.map((example) => (\n                                <Button\n                                    key={example.language}\n                                    variant={currentLanguage === example.language ? 'default' : 'ghost'}\n                                    size=\"sm\"\n                                    onClick={() => setCurrentLanguage(example.language)}\n                                >\n                                    {example.language}\n                                </Button>\n                            ))}\n                        </div>\n                        <Button\n                            size=\"sm\"\n                            variant=\"outline\"\n                            onClick={handleCopy}\n                            className=\"flex items-center gap-2\"\n                        >\n                            {copied ? (\n                                <>\n                                    <Check className=\"h-4 w-4\" />\n                                    Copied!\n                                </>\n                            ) : (\n                                <>\n                                    <Copy className=\"h-4 w-4\" />\n                                    Copy\n                                </>\n                            )}\n                        </Button>\n                    </div>\n                    <ScrollArea className=\"h-96\">\n                        <div className=\"p-4\">\n                            <SyntaxHighlighter\n                                language={languageMap[currentLanguage]}\n                                style={theme === 'dark' ? atomOneDark : atomOneLight}\n                                customStyle={{\n                                    margin: 0,\n                                    padding: 0,\n                                    background: 'transparent',\n                                }}\n                                codeTagProps={{\n                                    className: 'text-sm font-mono'\n                                }}\n                            >\n                                {generateEmbedCode}\n                            </SyntaxHighlighter>\n                        </div>\n                    </ScrollArea>\n                </div>\n            </CardContent>\n        </Card>\n    )\n}\n\nexport default function WidgetConfigContent({projectId}: { projectId: string }) {\n    const router = useRouter()\n    const [mounted, setMounted] = useState(false)\n    const [activeTab, setActiveTab] = useState('design')\n    const [config, setConfig] = useState<WidgetConfig>({\n        theme: 'light',\n        isPopup: false,\n        position: 'bottom-right',\n        maxEntries: DEFAULT_ENTRIES,\n        maxHeight: '400px',\n        trigger: ''\n    })\n\n    const {data: project, isLoading, isError} = useQuery({\n        queryKey: ['project-settings', projectId],\n        queryFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}/settings`)\n            if (!response.ok) throw new Error('Failed to fetch settings')\n            return response.json()\n        }\n    })\n\n    useEffect(() => {\n        const detectInitialTheme = () => {\n            const htmlElement = document.documentElement;\n            const isDarkMode =\n                htmlElement.classList.contains('dark') ||\n                htmlElement.style.colorScheme === 'dark';\n\n            setConfig(prev => ({\n                ...prev,\n                theme: isDarkMode ? 'dark' : 'light'\n            }));\n        };\n\n        detectInitialTheme();\n        setMounted(true);\n\n        const observer = new MutationObserver((mutations) => {\n            for (const mutation of mutations) {\n                if (mutation.type === 'attributes' &&\n                    (mutation.attributeName === 'class' || mutation.attributeName === 'style')) {\n                    detectInitialTheme();\n                    break;\n                }\n            }\n        });\n\n        observer.observe(document.documentElement, {\n            attributes: true,\n            attributeFilter: ['class', 'style']\n        });\n\n        return () => observer.disconnect();\n    }, [])\n\n    if (!mounted || isLoading) {\n        return (\n            <div className=\"flex items-center justify-center h-screen\">\n                <Settings2 className=\"h-8 w-8 animate-spin text-primary\" />\n            </div>\n        )\n    }\n\n    if (isError || !project?.isPublic) {\n        return (\n            <div className=\"container max-w-6xl py-8\">\n                <Alert variant=\"destructive\">\n                    <AlertTitle>{isError ? 'Error Loading Project' : 'Project is not public'}</AlertTitle>\n                    <AlertDescription>\n                        {isError\n                            ? 'Unable to load project settings. Please try again later.'\n                            : 'The widget is only available for public projects. Please make your project public in settings to use the widget.'}\n                    </AlertDescription>\n                </Alert>\n            </div>\n        )\n    }\n\n    return (\n        <div className=\"min-h-screen bg-background\">\n            <div className=\"container max-w-6xl py-8\">\n                <div className=\"flex items-center gap-4 mb-8\">\n                    <Button\n                        variant=\"ghost\"\n                        size=\"icon\"\n                        onClick={() => router.back()}\n                        className=\"hover:bg-muted\"\n                    >\n                        <ArrowLeft className=\"h-4 w-4\" />\n                    </Button>\n                    <div>\n                        <h1 className=\"text-3xl font-bold\">Widget Configuration</h1>\n                        <p className=\"text-muted-foreground mt-1\">Customize your changelog widget</p>\n                    </div>\n                </div>\n\n                <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\n                    <div className=\"lg:col-span-2 space-y-6\">\n                        <Tabs value={activeTab} onValueChange={setActiveTab} className=\"w-full\">\n                            <TabsList className=\"grid w-full grid-cols-2 mb-6\">\n                                <TabsTrigger value=\"design\" className=\"flex items-center gap-2\">\n                                    <Palette className=\"h-4 w-4\" />\n                                    Design\n                                </TabsTrigger>\n                                <TabsTrigger value=\"code\" className=\"flex items-center gap-2\">\n                                    <Code2 className=\"h-4 w-4\" />\n                                    Installation\n                                </TabsTrigger>\n                            </TabsList>\n\n                            <TabsContent value=\"design\" className=\"space-y-6\">\n                                <Card>\n                                    <CardHeader>\n                                        <CardTitle className=\"flex items-center gap-2\">\n                                            <Settings className=\"h-5 w-5\" />\n                                            Widget Settings\n                                        </CardTitle>\n                                    </CardHeader>\n                                    <CardContent className=\"space-y-6\">\n                                        <div className=\"flex items-center justify-between\">\n                                            <div className=\"space-y-1\">\n                                                <Label htmlFor=\"popup-mode\" className=\"text-base\">Popup Mode</Label>\n                                                <p className=\"text-sm text-muted-foreground\">Show widget in a popup overlay</p>\n                                            </div>\n                                            <Switch\n                                                id=\"popup-mode\"\n                                                checked={config.isPopup}\n                                                onCheckedChange={(checked) => setConfig(prev => ({\n                                                    ...prev,\n                                                    isPopup: checked,\n                                                    trigger: checked ? (prev.trigger || 'changelog-trigger') : ''\n                                                }))}\n                                            />\n                                        </div>\n\n                                        <Separator />\n\n                                        {config.isPopup && (\n                                            <>\n                                                <div className=\"space-y-2\">\n                                                    <Label htmlFor=\"position\">Position</Label>\n                                                    <Select\n                                                        value={config.position}\n                                                        onValueChange={(value) => setConfig(prev => ({\n                                                            ...prev,\n                                                            position: value as \"bottom-right\" | \"bottom-left\" | \"top-right\" | \"top-left\"\n                                                        }))}\n                                                    >\n                                                        <SelectTrigger>\n                                                            <SelectValue placeholder=\"Select position\" />\n                                                        </SelectTrigger>\n                                                        <SelectContent>\n                                                            <SelectItem value=\"bottom-right\">Bottom Right</SelectItem>\n                                                            <SelectItem value=\"bottom-left\">Bottom Left</SelectItem>\n                                                            <SelectItem value=\"top-right\">Top Right</SelectItem>\n                                                            <SelectItem value=\"top-left\">Top Left</SelectItem>\n                                                        </SelectContent>\n                                                    </Select>\n                                                </div>\n\n                                                <div className=\"space-y-2\">\n                                                    <Label htmlFor=\"trigger\">Trigger Button ID</Label>\n                                                    <Input\n                                                        id=\"trigger\"\n                                                        value={config.trigger}\n                                                        onChange={(e) => setConfig(prev => ({...prev, trigger: e.target.value}))}\n                                                        placeholder=\"changelog-trigger\"\n                                                    />\n                                                </div>\n\n                                                <Separator />\n                                            </>\n                                        )}\n\n                                        <div className=\"space-y-2\">\n                                            <Label htmlFor=\"max-entries\">Maximum Entries</Label>\n                                            <Input\n                                                id=\"max-entries\"\n                                                type=\"number\"\n                                                value={config.maxEntries}\n                                                onChange={(e) => {\n                                                    const value = parseInt(e.target.value);\n                                                    if (value >= MIN_ENTRIES && value <= MAX_ENTRIES) {\n                                                        setConfig(prev => ({...prev, maxEntries: value}));\n                                                    }\n                                                }}\n                                                min={MIN_ENTRIES}\n                                                max={MAX_ENTRIES}\n                                            />\n                                            <p className=\"text-sm text-muted-foreground\">Choose between {MIN_ENTRIES} and {MAX_ENTRIES} entries</p>\n                                        </div>\n\n                                        <div className=\"space-y-2\">\n                                            <Label htmlFor=\"max-height\">Maximum Height</Label>\n                                            <Input\n                                                id=\"max-height\"\n                                                value={config.maxHeight}\n                                                onChange={(e) => setConfig(prev => ({...prev, maxHeight: e.target.value}))}\n                                                placeholder=\"400px\"\n                                            />\n                                        </div>\n                                    </CardContent>\n                                </Card>\n                            </TabsContent>\n\n                            <TabsContent value=\"code\">\n                                <CodeInstallation config={config} projectId={projectId} codeExamples={CODE_EXAMPLES} />\n                            </TabsContent>\n                        </Tabs>\n                    </div>\n\n                    <div className=\"lg:col-span-1\">\n                        <Card className=\"sticky top-8\">\n                            <CardHeader>\n                                <CardTitle className=\"flex items-center gap-2\">\n                                    <Eye className=\"h-5 w-5\" />\n                                    Live Preview\n                                </CardTitle>\n                                <CardDescription>\n                                    See how your widget looks\n                                </CardDescription>\n                            </CardHeader>\n                            <CardContent>\n                                <Tabs\n                                    value={config.theme}\n                                    onValueChange={(value) => setConfig(prev => ({\n                                        ...prev,\n                                        theme: value as 'light' | 'dark'\n                                    }))}\n                                >\n                                    <TabsList className=\"w-full mb-4\">\n                                        <TabsTrigger value=\"light\" className=\"flex-1\">Light</TabsTrigger>\n                                        <TabsTrigger value=\"dark\" className=\"flex-1\">Dark</TabsTrigger>\n                                    </TabsList>\n                                    <TabsContent value=\"light\" className=\"mt-0\">\n                                        <WidgetPreview config={config} />\n                                    </TabsContent>\n                                    <TabsContent value=\"dark\" className=\"mt-0\">\n                                        <WidgetPreview config={config} />\n                                    </TabsContent>\n                                </Tabs>\n                            </CardContent>\n                        </Card>\n                    </div>\n                </div>\n            </div>\n        </div>\n    )\n}"
  },
  {
    "path": "app/dashboard/projects/[projectId]/integrations/widget/widget-list.tsx",
    "content": "'use client';\n\nimport { useState } from 'react';\nimport { Widget } from '@prisma/client';\nimport { Button } from '@/components/ui/button';\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Plus, Code2, Pencil, Trash2, Copy, Check } from 'lucide-react';\nimport 'dotenv/config';\nimport { Badge } from '@/components/ui/badge';\nimport { useRouter } from 'next/navigation';\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogHeader,\n    DialogTitle,\n} from '@/components/ui/dialog';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport {\n    Select,\n    SelectContent,\n    SelectItem,\n    SelectTrigger,\n    SelectValue,\n} from '@/components/ui/select';\nimport { toast } from '@/hooks/use-toast';\n\ninterface WidgetListProps {\n    projectId: string;\n    initialWidgets: Widget[];\n    project: { id: string; name: string; isPublic: boolean };\n}\n\nexport default function WidgetList({ projectId, initialWidgets, project }: WidgetListProps) {\n    const router = useRouter();\n    const [widgets, setWidgets] = useState(initialWidgets);\n    const [isCreateOpen, setIsCreateOpen] = useState(false);\n    const [copiedId, setCopiedId] = useState<string | null>(null);\n\n    const handleCreateWidget = async (e: React.FormEvent<HTMLFormElement>) => {\n        e.preventDefault();\n        const formData = new FormData(e.currentTarget);\n\n        try {\n            const res = await fetch(`/api/integrations/widget/${projectId}/${projectId}`, {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({\n                    name: formData.get('name'),\n                    variant: formData.get('variant'),\n                }),\n            });\n\n            if (!res.ok) throw new Error('Failed to create widget');\n\n            const { widget } = await res.json();\n            setWidgets([widget, ...widgets]);\n            setIsCreateOpen(false);\n            toast({ title: 'Success', description: 'Widget created successfully' });\n            router.refresh();\n        } catch (error) {\n            toast({ title: 'Error', description: 'Failed to create widget', variant: 'destructive' });\n        }\n    };\n\n    const handleDeleteWidget = async (widgetId: string) => {\n        if (!confirm('Are you sure you want to delete this widget?')) return;\n\n        try {\n            const res = await fetch(`/api/integrations/widget/${projectId}/${widgetId}`, {\n                method: 'DELETE',\n            });\n\n            if (!res.ok) throw new Error('Failed to delete widget');\n\n            setWidgets(widgets.filter((w) => w.id !== widgetId));\n            toast({ title: 'Success', description: 'Widget deleted' });\n            router.refresh();\n        } catch (error) {\n            toast({ title: 'Error', description: 'Failed to delete widget', variant: 'destructive' });\n        }\n    };\n\n    const copyEmbedCode = (widgetId: string) => {\n        const code = `<script src=\"${window.location.origin}/api/integrations/widget/${projectId}/${widgetId}\" async></script>`;\n        navigator.clipboard.writeText(code);\n        setCopiedId(widgetId);\n        toast({ title: 'Success', description: 'Embed code copied!' });\n        setTimeout(() => setCopiedId(null), 2000);\n    };\n\n    const variantColors = {\n        classic: 'blue',\n        floating: 'green',\n        modal: 'purple',\n        announcement: 'orange',\n    };\n\n    return (\n        <div className=\"space-y-6\">\n            <div className=\"flex items-center justify-between\">\n                <div>\n                    <h1 className=\"text-3xl font-bold\">Widgets</h1>\n                    <p className=\"text-muted-foreground mt-1\">\n                        Manage changelog widgets for {project.name}\n                    </p>\n                </div>\n                <Button onClick={() => setIsCreateOpen(true)}>\n                    <Plus className=\"w-4 h-4 mr-2\" />\n                    Create Widget\n                </Button>\n            </div>\n\n            {!project.isPublic && (\n                <Card className=\"border-yellow-500/50 bg-yellow-500/5\">\n                    <CardContent className=\"pt-6\">\n                        <p className=\"text-sm text-yellow-600 dark:text-yellow-400\">\n                            ⚠️ This project is private. Widgets will not be publicly accessible until you make the project public.\n                        </p>\n                    </CardContent>\n                </Card>\n            )}\n\n            {widgets.length === 0 ? (\n                <Card>\n                    <CardContent className=\"flex flex-col items-center justify-center py-12\">\n                        <Code2 className=\"w-12 h-12 text-muted-foreground mb-4\" />\n                        <h3 className=\"text-lg font-semibold mb-2\">No widgets yet</h3>\n                        <p className=\"text-sm text-muted-foreground mb-4\">\n                            Create your first widget to embed your changelog\n                        </p>\n                        <Button onClick={() => setIsCreateOpen(true)}>\n                            <Plus className=\"w-4 h-4 mr-2\" />\n                            Create Widget\n                        </Button>\n                    </CardContent>\n                </Card>\n            ) : (\n                <div className=\"grid gap-4\">\n                    {widgets.map((widget) => (\n                        <Card key={widget.id}>\n                            <CardHeader>\n                                <div className=\"flex items-start justify-between\">\n                                    <div className=\"space-y-1\">\n                                        <div className=\"flex items-center gap-2\">\n                                            <CardTitle>{widget.name}</CardTitle>\n                                            <Badge\n                                                variant=\"outline\"\n                                                className={`bg-${variantColors[widget.variant as keyof typeof variantColors]}-500/10`}\n                                            >\n                                                {widget.variant}\n                                            </Badge>\n                                            {!widget.isActive && (\n                                                <Badge variant=\"secondary\">Inactive</Badge>\n                                            )}\n                                        </div>\n                                        <CardDescription>\n                                            Created {new Date(widget.createdAt).toLocaleDateString()}\n                                        </CardDescription>\n                                    </div>\n                                    <div className=\"flex gap-2\">\n                                        <Button\n                                            variant=\"outline\"\n                                            size=\"sm\"\n                                            onClick={() => router.push(`/dashboard/projects/${projectId}/integrations/widget/${widget.id}`)}\n                                        >\n                                            <Pencil className=\"w-4 h-4 mr-2\" />\n                                            Edit\n                                        </Button>\n                                        <Button\n                                            variant=\"outline\"\n                                            size=\"sm\"\n                                            onClick={() => handleDeleteWidget(widget.id)}\n                                        >\n                                            <Trash2 className=\"w-4 h-4\" />\n                                        </Button>\n                                    </div>\n                                </div>\n                            </CardHeader>\n                            <CardContent>\n                                <div className=\"space-y-2\">\n                                    <Label className=\"text-xs text-muted-foreground\">Embed Code</Label>\n                                    <div className=\"flex gap-2\">\n                                        <code className=\"flex-1 px-3 py-2 bg-muted rounded text-sm font-mono overflow-x-auto\">\n                                            {`<script src=\"${window.location.origin}/api/integrations/widget/${projectId}/${widget.id}\" async></script>`}\n                                        </code>\n                                        <Button\n                                            variant=\"outline\"\n                                            size=\"sm\"\n                                            onClick={() => copyEmbedCode(widget.id)}\n                                        >\n                                            {copiedId === widget.id ? (\n                                                <Check className=\"w-4 h-4\" />\n                                            ) : (\n                                                <Copy className=\"w-4 h-4\" />\n                                            )}\n                                        </Button>\n                                    </div>\n                                </div>\n                            </CardContent>\n                        </Card>\n                    ))}\n                </div>\n            )}\n\n            <Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>\n                <DialogContent>\n                    <DialogHeader>\n                        <DialogTitle>Create New Widget</DialogTitle>\n                        <DialogDescription>\n                            Choose a widget variant and give it a name\n                        </DialogDescription>\n                    </DialogHeader>\n                    <form onSubmit={handleCreateWidget} className=\"space-y-4\">\n                        <div className=\"space-y-2\">\n                            <Label htmlFor=\"name\">Widget Name</Label>\n                            <Input\n                                id=\"name\"\n                                name=\"name\"\n                                placeholder=\"Homepage Widget\"\n                                required\n                            />\n                        </div>\n                        <div className=\"space-y-2\">\n                            <Label htmlFor=\"variant\">Variant</Label>\n                            <Select name=\"variant\" defaultValue=\"classic\" required>\n                                <SelectTrigger>\n                                    <SelectValue />\n                                </SelectTrigger>\n                                <SelectContent>\n                                    <SelectItem value=\"classic\">Classic - Traditional inline widget</SelectItem>\n                                    <SelectItem value=\"floating\">Floating - Button with badge in corner</SelectItem>\n                                    <SelectItem value=\"modal\">Modal - Full-screen popup</SelectItem>\n                                    <SelectItem value=\"announcement\">Announcement - Top/bottom bar</SelectItem>\n                                </SelectContent>\n                            </Select>\n                        </div>\n                        <div className=\"flex justify-end gap-2\">\n                            <Button type=\"button\" variant=\"outline\" onClick={() => setIsCreateOpen(false)}>\n                                Cancel\n                            </Button>\n                            <Button type=\"submit\">Create Widget</Button>\n                        </div>\n                    </form>\n                </DialogContent>\n            </Dialog>\n        </div>\n    );\n}\n"
  },
  {
    "path": "app/dashboard/projects/[projectId]/layout.tsx",
    "content": "'use client'\n\nimport {useQuery} from '@tanstack/react-query'\nimport {useParams} from 'next/navigation'\nimport {ProjectSidebar} from '@/components/project/ProjectSidebar'\nimport {Skeleton} from '@/components/ui/skeleton'\nimport React from \"react\";\n\nexport default function ProjectLayout({\n                                          children,\n                                      }: {\n    children: React.ReactNode\n}) {\n    const params = useParams()\n    const projectId = params.projectId as string\n\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    const {data: project, isLoading} = useQuery({\n        queryKey: ['project', projectId],\n        queryFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}`)\n            if (!response.ok) throw new Error('Failed to fetch project')\n            return response.json()\n        }\n    })\n\n    return (\n        <div className=\"flex h-full\">\n            {isLoading ? (\n                <div className=\"min-w-[260px] max-w-[280px] h-full border-r\">\n                    <div className=\"p-4 border-b\">\n                        <Skeleton className=\"h-8 w-36\"/>\n                    </div>\n                    <div className=\"p-4 space-y-3\">\n                        {Array.from({length: 4}).map((_, i) => (\n                            <Skeleton key={i} className=\"h-8 w-full\"/>\n                        ))}\n                    </div>\n                </div>\n            ) : (\n                <ProjectSidebar projectId={projectId}/>\n            )}\n            <div className=\"flex-1 overflow-auto\">\n                {children}\n            </div>\n        </div>\n    )\n}"
  },
  {
    "path": "app/dashboard/projects/[projectId]/page.tsx",
    "content": "// app/dashboard/projects/[projectId]/page.tsx\n\n'use client';\n\nimport {use} from 'react';\nimport {useQuery, useQueryClient} from '@tanstack/react-query';\nimport {motion} from 'framer-motion';\nimport Link from 'next/link';\nimport {\n    ArrowRight,\n    BarChart3,\n    Calendar,\n    CheckCircle2,\n    Clock,\n    Code,\n    ExternalLink,\n    FileText,\n    Globe,\n    Mail,\n    Plus,\n    Settings,\n    TrendingUp,\n    Upload,\n    Users,\n} from 'lucide-react';\n\nimport {SiGithub} from '@icons-pack/react-simple-icons';\n\nimport {\n    Avatar,\n    AvatarFallback\n} from '@/components/ui/avatar';\nimport {Badge} from '@/components/ui/badge';\nimport {Button} from '@/components/ui/button';\nimport {\n    Card,\n    CardContent,\n} from '@/components/ui/card';\nimport {Separator} from '@/components/ui/separator';\nimport {useToast} from '@/hooks/use-toast';\n\nimport {EmptyStateWithImport} from '@/components/projects/importing/ImportDataPrompt';\nimport {ImportResult} from '@/lib/types/projects/importing';\n\ninterface Project {\n    id: string;\n    name: string;\n    isPublic: boolean;\n    allowAutoPublish: boolean;\n    requireApproval: boolean;\n    defaultTags: string[];\n    changelog?: {\n        id: string;\n        entries: Array<{\n            id: string;\n            title: string;\n            version?: string;\n            publishedAt?: string;\n            createdAt: string;\n            tags: Array<{ id: string; name: string; color?: string }>;\n        }>;\n    };\n    createdAt: string;\n    updatedAt: string;\n}\n\ninterface User {\n    id: string;\n    role: 'ADMIN' | 'STAFF' | 'VIEWER';\n}\n\ninterface ProjectPageProps {\n    params: Promise<{ projectId: string }>;\n}\n\nconst fadeIn = {\n    initial: {opacity: 0, y: 20},\n    animate: {opacity: 1, y: 0},\n    transition: {duration: 0.5}\n};\n\nconst staggerChildren = {\n    animate: {\n        transition: {\n            staggerChildren: 0.1\n        }\n    }\n};\n\ninterface QuickStatsCardProps {\n    title: string;\n    value: string | number;\n    subtitle?: string;\n    icon: React.ElementType;\n    trend?: {\n        value: number;\n        direction: 'up' | 'down';\n    };\n    color?: string;\n}\n\nfunction QuickStatsCard({\n                            title,\n                            value,\n                            subtitle,\n                            icon: Icon,\n                            trend,\n                            color = \"text-primary\"\n                        }: QuickStatsCardProps) {\n    return (\n        <Card className=\"hover:shadow-md transition-shadow\">\n            <CardContent className=\"p-6\">\n                <div className=\"flex items-center justify-between\">\n                    <div className=\"space-y-2\">\n                        <p className=\"text-sm font-medium text-muted-foreground\">{title}</p>\n                        <div>\n                            <p className=\"text-2xl font-bold\">{value}</p>\n                            {subtitle && (\n                                <p className=\"text-xs text-muted-foreground\">{subtitle}</p>\n                            )}\n                        </div>\n                        {trend && (\n                            <div className=\"flex items-center text-xs\">\n                                <TrendingUp className={`h-3 w-3 mr-1 ${\n                                    trend.direction === 'up' ? 'text-green-600' : 'text-red-600'\n                                }`}/>\n                                <span className={trend.direction === 'up' ? 'text-green-600' : 'text-red-600'}>\n                  {trend.value}% from last month\n                </span>\n                            </div>\n                        )}\n                    </div>\n                    <div className={`p-3 rounded-full bg-muted/50`}>\n                        <Icon className={`h-6 w-6 ${color}`}/>\n                    </div>\n                </div>\n            </CardContent>\n        </Card>\n    );\n}\n\ninterface ActionCardProps {\n    title: string;\n    description: string;\n    icon: React.ElementType;\n    href?: string;\n    onClick?: () => void;\n    enabled?: boolean;\n    color?: string;\n    badge?: string;\n    external?: boolean;\n}\n\nfunction ActionCard({\n                        title,\n                        description,\n                        icon: Icon,\n                        href,\n                        onClick,\n                        enabled = true,\n                        color = \"text-primary\",\n                        badge,\n                        external = false\n                    }: ActionCardProps) {\n    const content = (\n        <Card className={`transition-all duration-200 ${\n            enabled ? 'hover:shadow-lg hover:scale-[1.02] cursor-pointer' : 'opacity-60 cursor-not-allowed'\n        }`}>\n            <CardContent className=\"p-6 h-full flex flex-col\">\n                <div className=\"flex items-start justify-between mb-4\">\n                    <div className={`p-3 rounded-full bg-muted/50`}>\n                        <Icon className={`h-6 w-6 ${color}`}/>\n                    </div>\n                    {badge && (\n                        <Badge variant=\"secondary\" className=\"text-xs\">\n                            {badge}\n                        </Badge>\n                    )}\n                </div>\n\n                <div className=\"flex-1\">\n                    <h3 className=\"font-semibold text-lg mb-2\">{title}</h3>\n                    <p className=\"text-muted-foreground text-sm leading-relaxed\">\n                        {description}\n                    </p>\n                </div>\n\n                <div className=\"flex items-center text-sm mt-4\">\n                    {enabled ? (\n                        <>\n              <span className=\"text-primary font-medium\">\n                {external ? 'Open' : 'Configure'}\n              </span>\n                            <ArrowRight className=\"ml-2 h-4 w-4 text-primary\"/>\n                        </>\n                    ) : (\n                        <Badge variant=\"outline\"\n                               className=\"border-amber-200 bg-amber-100/50 text-amber-700 dark:border-amber-800/40 dark:bg-amber-900/20 dark:text-amber-400\">\n                            Requires Public Project\n                        </Badge>\n                    )}\n                </div>\n            </CardContent>\n        </Card>\n    );\n\n    if (!enabled) {\n        return content;\n    }\n\n    if (onClick) {\n        return <div onClick={onClick}>{content}</div>;\n    }\n\n    if (href) {\n        if (external) {\n            return (\n                <a href={href} target=\"_blank\" rel=\"noopener noreferrer\">\n                    {content}\n                </a>\n            );\n        }\n        return <Link href={href}>{content}</Link>;\n    }\n\n    return content;\n}\n\ninterface RecentEntryCardProps {\n    entry: {\n        id: string;\n        title: string;\n        version?: string;\n        publishedAt?: string;\n        createdAt: string;\n        tags: Array<{ name: string; color?: string }>;\n    };\n    projectId: string;\n}\n\nfunction RecentEntryCard({entry, projectId}: RecentEntryCardProps) {\n    const isPublished = !!entry.publishedAt;\n    const displayDate = entry.publishedAt || entry.createdAt;\n\n    return (\n        <Card className=\"hover:shadow-md transition-shadow\">\n            <CardContent className=\"p-4\">\n                <div className=\"flex items-start justify-between mb-3\">\n                    <div className=\"flex items-center gap-2\">\n                        <div className={`h-2 w-2 rounded-full ${\n                            isPublished ? 'bg-green-500' : 'bg-yellow-500'\n                        }`}/>\n                        <Badge variant={isPublished ? \"default\" : \"secondary\"} className=\"text-xs\">\n                            {isPublished ? 'Published' : 'Draft'}\n                        </Badge>\n                        {entry.version && (\n                            <Badge variant=\"outline\" className=\"text-xs\">\n                                {entry.version}\n                            </Badge>\n                        )}\n                    </div>\n                    <span className=\"text-xs text-muted-foreground\">\n            {new Date(displayDate).toLocaleDateString()}\n          </span>\n                </div>\n\n                <Link\n                    href={`/dashboard/projects/${projectId}/changelog/${entry.id}`}\n                    className=\"block group\"\n                >\n                    <h4 className=\"font-medium group-hover:text-primary transition-colors mb-2 line-clamp-2\">\n                        {entry.title}\n                    </h4>\n                </Link>\n\n                {entry.tags.length > 0 && (\n                    <div className=\"flex flex-wrap gap-1\">\n                        {entry.tags.slice(0, 3).map((tag, i) => (\n                            <Badge key={i} variant=\"outline\" className=\"text-xs\">\n                                {tag.name}\n                            </Badge>\n                        ))}\n                        {entry.tags.length > 3 && (\n                            <Badge variant=\"outline\" className=\"text-xs\">\n                                +{entry.tags.length - 3}\n                            </Badge>\n                        )}\n                    </div>\n                )}\n            </CardContent>\n        </Card>\n    );\n}\n\nexport default function ProjectPage({params}: ProjectPageProps) {\n    const {projectId} = use(params);\n    const queryClient = useQueryClient();\n    const {toast} = useToast();\n\n    // Fetch project data\n    const {data: project, isLoading: isLoadingProject} = useQuery<Project>({\n        queryKey: ['project', projectId],\n        queryFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}`);\n            if (!response.ok) throw new Error('Failed to fetch project');\n            return response.json();\n        }\n    });\n\n    // Fetch user data for permissions\n    const {data: user} = useQuery<User>({\n        queryKey: ['user'],\n        queryFn: async () => {\n            const response = await fetch('/api/auth/me');\n            if (!response.ok) throw new Error('Failed to fetch user');\n            return response.json();\n        }\n    });\n\n    const handleImportComplete = (result: ImportResult) => {\n        toast({\n            title: 'Import completed!',\n            description: `Successfully imported ${result.importedCount} entries.`,\n        });\n\n        // Refresh project data\n        queryClient.invalidateQueries({queryKey: ['project', projectId]});\n    };\n\n    if (isLoadingProject) {\n        return (\n            <div className=\"container max-w-7xl space-y-6 p-4 md:p-8\">\n                <div className=\"space-y-4\">\n                    <div className=\"h-20 bg-muted rounded-xl animate-pulse\"/>\n                    <div className=\"grid gap-4 md:grid-cols-4\">\n                        {Array.from({length: 4}).map((_, i) => (\n                            <div key={i} className=\"h-32 bg-muted rounded-lg animate-pulse\"/>\n                        ))}\n                    </div>\n                    <div className=\"grid gap-6 md:grid-cols-3\">\n                        {Array.from({length: 6}).map((_, i) => (\n                            <div key={i} className=\"h-48 bg-muted rounded-lg animate-pulse\"/>\n                        ))}\n                    </div>\n                </div>\n            </div>\n        );\n    }\n\n    if (!project) {\n        return (\n            <div className=\"container max-w-7xl\">\n                <div className=\"text-center py-12\">\n                    <FileText className=\"h-16 w-16 text-muted-foreground mx-auto mb-4\"/>\n                    <h2 className=\"text-2xl font-semibold mb-2\">Project Not Found</h2>\n                    <p className=\"text-muted-foreground mb-6\">\n                        The project you&apos;re looking for doesn&apos;t exist or you don&apos;t have access.\n                    </p>\n                    <Button asChild>\n                        <Link href=\"/dashboard/projects\">Back to Projects</Link>\n                    </Button>\n                </div>\n            </div>\n        );\n    }\n\n    const isAdmin = user?.role === 'ADMIN';\n    const hasEntries = project.changelog?.entries && project.changelog.entries.length > 0;\n    const recentEntries = project.changelog?.entries.slice(0, 3) || [];\n    const publishedCount = project.changelog?.entries.filter(e => e.publishedAt).length || 0;\n    const draftCount = project.changelog?.entries.filter(e => !e.publishedAt).length || 0;\n\n    // Show import prompt if no entries and user is admin\n    if (!hasEntries && isAdmin) {\n        return (\n            <div className=\"container max-w-7xl space-y-6 p-4 md:p-8\">\n                {/* Project Header */}\n                <motion.div\n                    initial=\"initial\"\n                    animate=\"animate\"\n                    variants={fadeIn}\n                    className=\"flex flex-col md:flex-row justify-between items-start md:items-center gap-4 bg-gradient-to-r from-background to-muted/60 p-6 rounded-xl border\"\n                >\n                    <div className=\"flex gap-4 items-center\">\n                        <Avatar\n                            className=\"h-16 w-16 rounded-xl bg-primary/10 flex items-center justify-center text-primary font-medium text-2xl border-2 border-primary/20\">\n                            <AvatarFallback>\n                                {project.name.substring(0, 1).toUpperCase()}\n                            </AvatarFallback>\n                        </Avatar>\n                        <div>\n                            <h1 className=\"text-3xl font-bold tracking-tight mb-1\">{project.name}</h1>\n                            <div className=\"flex items-center gap-2 text-muted-foreground\">\n                                <Badge variant={project.isPublic ? \"default\" : \"secondary\"}>\n                                    {project.isPublic ? \"Public\" : \"Private\"}\n                                </Badge>\n                                <span className=\"text-sm\">No entries yet</span>\n                            </div>\n                        </div>\n                    </div>\n\n                    <div className=\"flex items-center gap-2\">\n                        {project.isPublic && (\n                            <Button variant=\"outline\" size=\"sm\" asChild>\n                                <Link href={`/changelog/${project.id}`} target=\"_blank\">\n                                    <ExternalLink className=\"h-4 w-4 mr-2\"/>\n                                    View Public Page\n                                </Link>\n                            </Button>\n                        )}\n                        <Button variant=\"secondary\" size=\"sm\" asChild>\n                            <Link href={`/dashboard/projects/${project.id}/settings`}>\n                                <Settings className=\"h-4 w-4 mr-2\"/>\n                                Settings\n                            </Link>\n                        </Button>\n                    </div>\n                </motion.div>\n\n                {/* Import State */}\n                <EmptyStateWithImport\n                    projectId={project.id}\n                    projectName={project.name}\n                    isAdmin={isAdmin}\n                    onImportComplete={handleImportComplete}\n                />\n            </div>\n        );\n    }\n\n    return (\n        <div className=\"container max-w-7xl space-y-6 p-4 md:p-8\">\n            {/* Project Header */}\n            <motion.div\n                initial=\"initial\"\n                animate=\"animate\"\n                variants={fadeIn}\n                className=\"flex flex-col md:flex-row justify-between items-start md:items-center gap-4 bg-gradient-to-r from-background to-muted/60 p-6 rounded-xl border\"\n            >\n                <div className=\"flex gap-4 items-center\">\n                    <Avatar\n                        className=\"h-16 w-16 rounded-xl bg-primary/10 flex items-center justify-center text-primary font-medium text-2xl border-2 border-primary/20\">\n                        <AvatarFallback>\n                            {project.name.substring(0, 1).toUpperCase()}\n                        </AvatarFallback>\n                    </Avatar>\n                    <div>\n                        <h1 className=\"text-3xl font-bold tracking-tight mb-1\">{project.name}</h1>\n                        <div className=\"flex items-center gap-2 text-muted-foreground\">\n                            <Badge variant={project.isPublic ? \"default\" : \"secondary\"}>\n                                {project.isPublic ? \"Public\" : \"Private\"}\n                            </Badge>\n                            {hasEntries && (\n                                <span className=\"text-sm\">\n                  {project.changelog?.entries.length} entries\n                </span>\n                            )}\n                        </div>\n                    </div>\n                </div>\n\n                <div className=\"flex items-center gap-2\">\n                    {project.isPublic && (\n                        <Button variant=\"outline\" size=\"sm\" asChild>\n                            <Link href={`/changelog/${project.id}`} target=\"_blank\">\n                                <ExternalLink className=\"h-4 w-4 mr-2\"/>\n                                View Public Page\n                            </Link>\n                        </Button>\n                    )}\n                    <Button variant=\"secondary\" size=\"sm\" asChild>\n                        <Link href={`/dashboard/projects/${project.id}/settings`}>\n                            <Settings className=\"h-4 w-4 mr-2\"/>\n                            Settings\n                        </Link>\n                    </Button>\n                </div>\n            </motion.div>\n\n            {/* Quick Stats */}\n            <motion.div\n                initial=\"initial\"\n                animate=\"animate\"\n                variants={staggerChildren}\n                className=\"grid gap-4 md:grid-cols-4\"\n            >\n                <motion.div variants={fadeIn}>\n                    <QuickStatsCard\n                        title=\"Total Entries\"\n                        value={project.changelog?.entries.length || 0}\n                        icon={FileText}\n                        color=\"text-blue-600\"\n                    />\n                </motion.div>\n                <motion.div variants={fadeIn}>\n                    <QuickStatsCard\n                        title=\"Published\"\n                        value={publishedCount}\n                        subtitle=\"Live entries\"\n                        icon={CheckCircle2}\n                        color=\"text-green-600\"\n                    />\n                </motion.div>\n                <motion.div variants={fadeIn}>\n                    <QuickStatsCard\n                        title=\"Drafts\"\n                        value={draftCount}\n                        subtitle=\"Pending entries\"\n                        icon={Clock}\n                        color=\"text-yellow-600\"\n                    />\n                </motion.div>\n                <motion.div variants={fadeIn}>\n                    <QuickStatsCard\n                        title=\"Last Updated\"\n                        value={new Date(project.updatedAt).toLocaleDateString()}\n                        icon={Calendar}\n                        color=\"text-purple-600\"\n                    />\n                </motion.div>\n            </motion.div>\n\n            {/* Quick Actions */}\n            <motion.div\n                initial=\"initial\"\n                animate=\"animate\"\n                variants={fadeIn}\n            >\n                <div className=\"flex items-center justify-between mb-6\">\n                    <h2 className=\"text-2xl font-bold\">Quick Actions</h2>\n                    {hasEntries && isAdmin && (\n                        <Button variant=\"outline\" size=\"sm\" asChild>\n                            <Link href={`/dashboard/projects/${project.id}/import`}>\n                                <Upload className=\"h-4 w-4 mr-2\"/>\n                                Import More Data\n                            </Link>\n                        </Button>\n                    )}\n                </div>\n\n                <div className=\"grid gap-6 md:grid-cols-3\">\n                    <ActionCard\n                        title=\"Create Entry\"\n                        description=\"Write a new changelog entry to document your latest updates and features.\"\n                        icon={Plus}\n                        href={`/dashboard/projects/${project.id}/changelog/new`}\n                        color=\"text-green-600\"\n                    />\n\n                    <ActionCard\n                        title=\"View Changelog\"\n                        description=\"Browse all your changelog entries, manage drafts, and see publication status.\"\n                        icon={FileText}\n                        href={`/dashboard/projects/${project.id}/changelog`}\n                        color=\"text-blue-600\"\n                    />\n\n                    <ActionCard\n                        title=\"Analytics\"\n                        description=\"Track engagement metrics and see how your audience interacts with your changelog.\"\n                        icon={BarChart3}\n                        href={`/dashboard/projects/${project.id}/analytics`}\n                        color=\"text-purple-600\"\n                    />\n                </div>\n            </motion.div>\n\n            {/* Integrations & Features */}\n            <motion.div\n                initial=\"initial\"\n                animate=\"animate\"\n                variants={fadeIn}\n            >\n                <div className=\"flex items-center gap-2 mb-6\">\n                    <h2 className=\"text-2xl font-bold\">Integrations & Features</h2>\n                    <Separator className=\"flex-1\"/>\n                </div>\n\n                <div className=\"grid gap-6 md:grid-cols-3\">\n                    <ActionCard\n                        title=\"Widget Embed\"\n                        description=\"Add a changelog widget to your website to keep users informed about updates.\"\n                        icon={Code}\n                        href={`/dashboard/projects/${project.id}/integrations/widget`}\n                        enabled={project.isPublic}\n                        color=\"text-orange-600\"\n                        badge={project.isPublic ? undefined : \"Requires Public\"}\n                    />\n\n                    <ActionCard\n                        title=\"Email Notifications\"\n                        description=\"Send email updates to subscribers when you publish new changelog entries.\"\n                        icon={Mail}\n                        href={`/dashboard/projects/${project.id}/integrations/email`}\n                        color=\"text-blue-600\"\n                    />\n\n                    <ActionCard\n                        title=\"Public Changelog\"\n                        description=\"Share your changelog with the world through a beautiful, standalone public page.\"\n                        icon={Globe}\n                        href={`/changelog/${project.id}`}\n                        enabled={project.isPublic}\n                        color=\"text-green-600\"\n                        external={true}\n                        badge={project.isPublic ? \"Live\" : \"Requires Public\"}\n                    />\n\n                    <ActionCard\n                        title=\"GitHub Integration\"\n                        description=\"Automatically sync commits and releases from your GitHub repository.\"\n                        icon={SiGithub}\n                        href={`/dashboard/projects/${project.id}/integrations/github`}\n                        color=\"text-gray-700\"\n                        badge=\"Beta\"\n                    />\n\n                    <ActionCard\n                        title=\"Team Management\"\n                        description=\"Manage team members, roles, and permissions for collaborative changelog editing.\"\n                        icon={Users}\n                        href={`/dashboard/projects/${project.id}/team`}\n                        color=\"text-indigo-600\"\n                        badge=\"Coming Soon\"\n                        enabled={false}\n                    />\n\n                    <ActionCard\n                        title=\"Custom Domain\"\n                        description=\"Use your own domain for the public changelog page to maintain brand consistency.\"\n                        icon={Globe}\n                        href={`/dashboard/projects/${project.id}/domains`}\n                        enabled={project.isPublic}\n                        color=\"text-teal-600\"\n                        badge={project.isPublic ? undefined : \"Requires Public\"}\n                    />\n                </div>\n            </motion.div>\n\n            {/* Recent Entries */}\n            {hasEntries && (\n                <motion.div\n                    initial=\"initial\"\n                    animate=\"animate\"\n                    variants={fadeIn}\n                >\n                    <div className=\"flex items-center justify-between mb-6\">\n                        <div className=\"flex items-center gap-3\">\n                            <h2 className=\"text-2xl font-bold\">Recent Entries</h2>\n                            <Badge variant=\"outline\" className=\"font-normal\">\n                                {project.changelog?.entries.length || 0} total\n                            </Badge>\n                        </div>\n\n                        <Button asChild>\n                            <Link href={`/dashboard/projects/${project.id}/changelog`}>\n                                View All Entries\n                                <ArrowRight className=\"ml-2 h-4 w-4\"/>\n                            </Link>\n                        </Button>\n                    </div>\n\n                    <div className=\"grid gap-4 md:grid-cols-3\">\n                        {recentEntries.map((entry) => (\n                            <RecentEntryCard\n                                key={entry.id}\n                                entry={entry}\n                                projectId={project.id}\n                            />\n                        ))}\n                    </div>\n\n                    {recentEntries.length === 0 && (\n                        <Card>\n                            <CardContent className=\"p-12 text-center\">\n                                <FileText className=\"h-12 w-12 text-muted-foreground mx-auto mb-4\"/>\n                                <h3 className=\"text-lg font-semibold mb-2\">No entries yet</h3>\n                                <p className=\"text-muted-foreground mb-6\">\n                                    Get started by creating your first changelog entry.\n                                </p>\n                                <Button asChild>\n                                    <Link href={`/dashboard/projects/${project.id}/changelog/new`}>\n                                        <Plus className=\"h-4 w-4 mr-2\"/>\n                                        Create First Entry\n                                    </Link>\n                                </Button>\n                            </CardContent>\n                        </Card>\n                    )}\n                </motion.div>\n            )}\n        </div>\n    );\n}"
  },
  {
    "path": "app/dashboard/projects/[projectId]/settings/page.tsx",
    "content": "'use client'\n\nimport {use, useState} from 'react'\nimport {useRouter} from 'next/navigation'\nimport {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'\nimport {Button} from '@/components/ui/button'\nimport {Input} from '@/components/ui/input'\nimport {Label} from '@/components/ui/label'\nimport {Switch} from '@/components/ui/switch'\nimport {Card, CardContent, CardDescription, CardHeader, CardTitle,} from '@/components/ui/card'\nimport {\n    AlertDialog,\n    AlertDialogAction,\n    AlertDialogCancel,\n    AlertDialogContent,\n    AlertDialogDescription,\n    AlertDialogFooter,\n    AlertDialogHeader,\n    AlertDialogTitle,\n    AlertDialogTrigger,\n} from '@/components/ui/alert-dialog'\nimport {useToast} from '@/hooks/use-toast'\nimport {\n    AlertTriangle,\n    CheckCircle,\n    Clock,\n    Code,\n    ExternalLink,\n    Github,\n    Globe,\n    Loader2,\n    Lock,\n    Mail,\n    Puzzle,\n    Rss,\n    Settings,\n    Shield,\n    Tag,\n    ArrowRight,\n    Slack,\n} from 'lucide-react'\nimport {DestructiveActionRequest} from '@/components/changelog/RequestHandler'\nimport {useAuth} from '@/context/auth'\nimport {Alert, AlertDescription} from '@/components/ui/alert'\nimport TagManagement from \"@/components/project/settings/TagManagement\";\nimport {Badge} from '@/components/ui/badge'\n\ninterface ProjectSettingsPageProps {\n    params: Promise<{ projectId: string }>\n}\n\ninterface ProjectSettings {\n    id: string\n    name: string\n    isPublic: boolean\n    allowAutoPublish: boolean\n    requireApproval: boolean\n    defaultTags: string[]\n    updatedAt: string\n}\n\nexport default function ProjectSettingsPage({params}: ProjectSettingsPageProps) {\n    const {projectId} = use(params)\n    const router = useRouter()\n    const {toast} = useToast()\n    const queryClient = useQueryClient()\n    const [activeTab, setActiveTab] = useState('general')\n    // const [newTag, setNewTag] = useState('')\n    const [isDeleting, setIsDeleting] = useState(false)\n\n    const {user} = useAuth()\n\n    const {data: project, isLoading} = useQuery<ProjectSettings>({\n        queryKey: ['project-settings', projectId],\n        queryFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}/settings`)\n            if (!response.ok) throw new Error('Failed to fetch settings')\n            return response.json()\n        }\n    })\n\n    const updateSettings = useMutation({\n        mutationFn: async (data: Partial<ProjectSettings>) => {\n            const response = await fetch(`/api/projects/${projectId}/settings`, {\n                method: 'PATCH',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify(data)\n            })\n            if (!response.ok) throw new Error('Failed to update settings')\n            return response.json()\n        },\n        onSuccess: (data) => {\n            queryClient.setQueryData(['project-settings', projectId], data)\n            toast({title: 'Success', description: 'Settings updated successfully'})\n        }\n    })\n\n    const deleteProject = useMutation({\n        mutationFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}`, {\n                method: 'DELETE'\n            })\n            if (!response.ok) throw new Error('Failed to delete project')\n        },\n        onSuccess: () => {\n            toast({title: 'Success', description: 'Project deleted successfully'})\n            router.push('/dashboard/projects')\n        },\n        onSettled: () => {\n            setIsDeleting(false)\n        }\n    })\n\n    if (isLoading || !project) {\n        return (\n            <div className=\"flex items-center justify-center h-96\">\n                <div className=\"animate-pulse\">Loading...</div>\n            </div>\n        )\n    }\n\n    const handleUpdate = (field: keyof ProjectSettings, value: unknown) => {\n        updateSettings.mutate({[field]: value})\n    }\n\n    // const handleAddTag = (e?: React.KeyboardEvent<HTMLInputElement>) => {\n    //     if (e && e.key !== 'Enter') return\n    //     if (newTag.trim()) {\n    //         const updatedTags = Array.from(new Set([...project.defaultTags, newTag.trim()]))\n    //         updateSettings.mutate({defaultTags: updatedTags})\n    //         setNewTag('')\n    //     }\n    // }\n    //\n    // const handleTagDeletion = (tag: string) => {\n    //     if (user?.role === 'ADMIN') {\n    //         const updatedTags = project.defaultTags.filter(t => t !== tag)\n    //         handleUpdate('defaultTags', updatedTags)\n    //     }\n    // }\n\n    const tabs = [\n        {id: 'general', label: 'General', icon: Settings},\n        {id: 'access', label: 'Access', icon: Shield},\n        {id: 'integrations', label: 'Integrations', icon: Puzzle},\n        {id: 'tags', label: 'Tags', icon: Tag},\n        {id: 'danger', label: 'Danger', icon: AlertTriangle, className: 'text-destructive'}\n    ]\n\n    // Integration definitions - organized for better scalability\n    const integrations = [\n        {\n            id: 'widget',\n            name: 'Changelog Widget',\n            description: 'Embed a customizable widget into your website',\n            icon: Code,\n            status: 'stable',\n            requiresPublic: true,\n            action: {\n                type: 'navigate',\n                label: 'Configure',\n                path: `/dashboard/projects/${projectId}/integrations/widget`\n            }\n        },\n        {\n            id: 'email',\n            name: 'Email Notifications',\n            description: 'Send updates to subscribers via email',\n            icon: Mail,\n            status: 'stable',\n            requiresPublic: false,\n            action: {\n                type: 'navigate',\n                label: 'Configure',\n                path: `/dashboard/projects/${projectId}/integrations/email`\n            }\n        },\n        {\n            id: 'github',\n            name: 'GitHub Integration',\n            description: 'Use your GitHub data with changelogs',\n            icon: Github,\n            status: 'stable',\n            requiresPublic: false,\n            action: {\n                type: 'navigate',\n                label: 'Configure',\n                path: `/dashboard/projects/${projectId}/integrations/github`\n            }\n        },\n        {\n            id: 'rss',\n            name: 'RSS Feed',\n            description: 'Subscribe to changelog updates',\n            icon: Rss,\n            status: 'stable',\n            requiresPublic: true,\n            action: {\n                type: 'external',\n                label: 'View Feed',\n                url: `/changelog/${projectId}/rss.xml`\n            }\n        },\n        {\n            id: 'domains',\n            name: 'Domains',\n            description: 'Configure a custom domain for your public changelog',\n            icon: Globe,\n            status: 'stable',\n            requiresPublic: true,\n            action: {\n                type: 'navigate',\n                label: 'Configure',\n                path: `/dashboard/projects/${projectId}/domains`\n            }\n        },\n        {\n            id: 'slack',\n            name: 'Slack',\n            description: 'Post changelog updates to your Slack workspace',\n            icon: Slack,\n            status: 'stable',\n            requiresPublic: false,\n            action: {\n                type: 'navigate',\n                label: 'Configure',\n                path: `/dashboard/projects/${projectId}/integrations/slack`\n            }\n        }\n    ]\n\n    const comingSoonIntegrations = ['Discord', 'Teams', 'Zapier', 'Webhook']\n\n    const renderTabContent = () => {\n        switch (activeTab) {\n            case 'general':\n                return (\n                    <Card>\n                        <CardHeader>\n                            <CardTitle>General Settings</CardTitle>\n                            <CardDescription>\n                                Basic project configuration and details\n                            </CardDescription>\n                        </CardHeader>\n                        <CardContent>\n                            <div className=\"space-y-4\">\n                                <div className=\"space-y-2\">\n                                    <Label htmlFor=\"name\">Project Name</Label>\n                                    <Input\n                                        id=\"name\"\n                                        value={project.name}\n                                        onChange={(e) => handleUpdate('name', e.target.value)}\n                                        className=\"max-w-md\"\n                                    />\n                                </div>\n                            </div>\n                        </CardContent>\n                    </Card>\n                )\n\n            case 'access':\n                return (\n                    <Card>\n                        <CardHeader>\n                            <CardTitle>Access Settings</CardTitle>\n                            <CardDescription>\n                                Configure visibility and permissions\n                            </CardDescription>\n                        </CardHeader>\n                        <CardContent>\n                            <div className=\"space-y-6\">\n                                {user?.role !== 'ADMIN' && (\n                                    <Alert variant=\"warning\">\n                                        <AlertDescription>\n                                            Only administrators can modify access settings.\n                                        </AlertDescription>\n                                    </Alert>\n                                )}\n                                <div className=\"flex justify-between items-center\">\n                                    <div className=\"space-y-0.5\">\n                                        <Label>Public Access</Label>\n                                        <p className=\"text-sm text-muted-foreground\">\n                                            Make changelog visible without authentication\n                                        </p>\n                                    </div>\n                                    <Switch\n                                        checked={project.isPublic}\n                                        onCheckedChange={(checked) => handleUpdate('isPublic', checked)}\n                                        disabled={user?.role !== 'ADMIN'}\n                                    />\n                                </div>\n                                <div className=\"flex justify-between items-center\">\n                                    <div className=\"space-y-0.5\">\n                                        <Label>Auto-Publish</Label>\n                                        <p className=\"text-sm text-muted-foreground\">\n                                            Automatically publish new entries\n                                        </p>\n                                    </div>\n                                    <Switch\n                                        checked={project.allowAutoPublish}\n                                        onCheckedChange={(checked) => handleUpdate('allowAutoPublish', checked)}\n                                        disabled={user?.role !== 'ADMIN'}\n                                    />\n                                </div>\n                                <div className=\"flex justify-between items-center\">\n                                    <div className=\"space-y-0.5\">\n                                        <Label>Require Approval</Label>\n                                        <p className=\"text-sm text-muted-foreground\">\n                                            Require admin approval for new entries\n                                        </p>\n                                    </div>\n                                    <Switch\n                                        checked={project.requireApproval}\n                                        onCheckedChange={(checked) => handleUpdate('requireApproval', checked)}\n                                        disabled={user?.role !== 'ADMIN'}\n                                    />\n                                </div>\n                            </div>\n                        </CardContent>\n                    </Card>\n                )\n\n            case 'integrations':\n                return (\n                    <Card className=\"border-border/50 shadow-sm\">\n                        <CardHeader className=\"pb-4\">\n                            <div className=\"flex items-center gap-2\">\n                                <div className=\"p-1.5 rounded-lg bg-primary/10 dark:bg-primary/20\">\n                                    <Puzzle className=\"h-4 w-4 text-primary\" />\n                                </div>\n                                <CardTitle className=\"text-xl\">Integrations</CardTitle>\n                            </div>\n                            <CardDescription className=\"text-muted-foreground\">\n                                Connect your changelog with external services and automation tools\n                            </CardDescription>\n                        </CardHeader>\n                        <CardContent className=\"space-y-6\">\n                            {!project.isPublic && (\n                                <Alert icon={<Lock className=\"h-4 w-4 text-amber-600 dark:text-amber-400\" />} className=\"border-amber-200/50 bg-amber-50/50 dark:border-amber-800/50 dark:bg-amber-950/20\">\n                                    <AlertDescription className=\"text-amber-800 dark:text-amber-200\">\n                                        Some integrations require your project to be public. Enable public access to unlock all features.\n                                    </AlertDescription>\n                                </Alert>\n                            )}\n\n                            {/* Available Integrations Grid */}\n                            <div className=\"grid gap-4 md:grid-cols-2\">\n                                {integrations.map((integration) => {\n                                    const Icon = integration.icon\n                                    const isBlocked = integration.requiresPublic && !project.isPublic\n\n                                    return (\n                                        <Card\n                                            key={integration.id}\n                                            className={`group relative overflow-hidden transition-all duration-200 ${\n                                                isBlocked\n                                                    ? 'border-dashed border-border/60 bg-muted/20 opacity-70'\n                                                    : 'border-border/50 bg-card hover:border-primary/30 hover:shadow-md hover:-translate-y-0.5'\n                                            }`}\n                                        >\n                                            {/* Accent line for active integrations */}\n                                            {!isBlocked && (\n                                                <div className=\"h-0.5 bg-gradient-to-r from-primary/60 via-primary/30 to-transparent\" />\n                                            )}\n\n                                            <CardContent className=\"p-5\">\n                                                <div className=\"space-y-4\">\n                                                    {/* Header with icon and status */}\n                                                    <div className=\"flex items-start justify-between\">\n                                                        <div className=\"flex items-start gap-3\">\n                                                            <div className={`p-2.5 rounded-xl transition-colors ${\n                                                                isBlocked\n                                                                    ? 'bg-muted/60 text-muted-foreground/60'\n                                                                    : 'bg-primary/10 text-primary group-hover:bg-primary/15 dark:bg-primary/20 dark:group-hover:bg-primary/25'\n                                                            }`}>\n                                                                <Icon className=\"h-5 w-5\" />\n                                                            </div>\n                                                            <div className=\"space-y-1\">\n                                                                <div className=\"flex items-center gap-2\">\n                                                                    <h3 className=\"font-semibold text-foreground\">{integration.name}</h3>\n                                                                    {integration.status === 'beta' && (\n                                                                        <Badge variant=\"outline\" className=\"text-xs px-2 py-0.5 bg-orange-50 text-orange-700 border-orange-200 dark:bg-orange-950/30 dark:text-orange-300 dark:border-orange-800\">\n                                                                            <Clock className=\"h-3 w-3 mr-1\" />\n                                                                            Beta\n                                                                        </Badge>\n                                                                    )}\n                                                                    {integration.status === 'stable' && (\n                                                                        <Badge variant=\"outline\" className=\"text-xs px-2 py-0.5 bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-950/30 dark:text-emerald-300 dark:border-emerald-800\">\n                                                                            <CheckCircle className=\"h-3 w-3 mr-1\" />\n                                                                            Stable\n                                                                        </Badge>\n                                                                    )}\n                                                                </div>\n                                                                <p className=\"text-sm text-muted-foreground leading-relaxed max-w-xs\">\n                                                                    {integration.description}\n                                                                </p>\n                                                            </div>\n                                                        </div>\n                                                    </div>\n\n                                                    {/* Action area */}\n                                                    <div className=\"flex justify-end pt-2\">\n                                                        {isBlocked ? (\n                                                            <div className=\"flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground bg-muted/60 rounded-lg border border-dashed\">\n                                                                <Lock className=\"h-4 w-4\"/>\n                                                                <span>Public project required</span>\n                                                            </div>\n                                                        ) : (\n                                                            <Button\n                                                                variant=\"outline\"\n                                                                size=\"sm\"\n                                                                onClick={() => {\n                                                                    if (integration.action.type === 'navigate') {\n                                                                        router.push(integration.action.path!)\n                                                                    } else if (integration.action.type === 'external') {\n                                                                        window.open(integration.action.url, '_blank')\n                                                                    }\n                                                                }}\n                                                                className=\"gap-2 transition-all hover:scale-105 hover:shadow-sm bg-background hover:bg-accent\"\n                                                            >\n                                                                {integration.action.label}\n                                                                {integration.action.type === 'external' ? (\n                                                                    <ExternalLink className=\"h-4 w-4\" />\n                                                                ) : (\n                                                                    <ArrowRight className=\"h-4 w-4\" />\n                                                                )}\n                                                            </Button>\n                                                        )}\n                                                    </div>\n                                                </div>\n                                            </CardContent>\n\n                                            {/* Subtle overlay for blocked integrations */}\n                                            {isBlocked && (\n                                                <div className=\"absolute inset-0 bg-gradient-to-br from-background/10 to-background/30 pointer-events-none\" />\n                                            )}\n                                        </Card>\n                                    )\n                                })}\n                            </div>\n\n                            {/* Coming Soon Section */}\n                            <div className=\"pt-6 border-t border-border/50\">\n                                <div className=\"flex items-center gap-2 mb-4\">\n                                    <div className=\"p-1 rounded bg-muted\">\n                                        <Clock className=\"h-3 w-3 text-muted-foreground\" />\n                                    </div>\n                                    <h3 className=\"text-sm font-medium text-muted-foreground\">Coming Soon</h3>\n                                </div>\n                                <div className=\"grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3\">\n                                    {comingSoonIntegrations.map((name) => (\n                                        <div\n                                            key={name}\n                                            className=\"group flex items-center justify-center h-14 rounded-lg border border-dashed border-border/60 bg-muted/20 text-xs text-muted-foreground font-medium transition-colors hover:bg-muted/40 hover:border-border\"\n                                        >\n                                            {name}\n                                        </div>\n                                    ))}\n                                </div>\n                            </div>\n                        </CardContent>\n                    </Card>\n                )\n\n            case 'tags':\n                return (\n                    <>\n                        <TagManagement projectId={project.id}/>\n                        {/*TODO: redo this later */}\n                        {/*<Card className=\"mt-4\">*/}\n                        {/*    <CardHeader>*/}\n                        {/*        <CardTitle>Default Tags</CardTitle>*/}\n                        {/*        <CardDescription>*/}\n                        {/*            Manage default tags for changelog entries*/}\n                        {/*        </CardDescription>*/}\n                        {/*    </CardHeader>*/}\n                        {/*    <CardContent>*/}\n                        {/*        <div className=\"space-y-4\">*/}\n                        {/*            <div className=\"flex gap-2\">*/}\n                        {/*                <div className=\"relative flex-1 max-w-sm\">*/}\n                        {/*                    <Input*/}\n                        {/*                        value={newTag}*/}\n                        {/*                        onChange={(e) => setNewTag(e.target.value)}*/}\n                        {/*                        onKeyDown={(e) => e.key === 'Enter' && handleAddTag()}*/}\n                        {/*                        placeholder=\"Add new tag...\"*/}\n                        {/*                    />*/}\n                        {/*                </div>*/}\n                        {/*                <Button*/}\n                        {/*                    onClick={() => handleAddTag()}*/}\n                        {/*                    disabled={!newTag.trim()}*/}\n                        {/*                >*/}\n                        {/*                    <Plus className=\"h-4 w-4 mr-2\"/>*/}\n                        {/*                    Add Tag*/}\n                        {/*                </Button>*/}\n                        {/*            </div>*/}\n\n                        {/*            <div className=\"flex flex-wrap gap-2\">*/}\n                        {/*                {project.defaultTags.map((tag) => (*/}\n                        {/*                    <Badge*/}\n                        {/*                        key={tag}*/}\n                        {/*                        variant=\"secondary\"*/}\n                        {/*                        className=\"flex items-center gap-1 px-3 py-1\"*/}\n                        {/*                        color={tag.color}*/}\n                        {/*                    >*/}\n                        {/*                        <Tag className=\"h-3 w-3\"/>*/}\n                        {/*                        {tag}*/}\n                        {/*                        {user?.role === 'ADMIN' ? (*/}\n                        {/*                            <button*/}\n                        {/*                                onClick={() => handleTagDeletion(tag)}*/}\n                        {/*                                className=\"ml-1 hover:text-destructive\"*/}\n                        {/*                            >*/}\n                        {/*                                <X className=\"h-3 w-3\"/>*/}\n                        {/*                            </button>*/}\n                        {/*                        ) : (*/}\n                        {/*                            <DestructiveActionRequest*/}\n                        {/*                                projectId={projectId}*/}\n                        {/*                                action=\"DELETE_TAG\"*/}\n                        {/*                                targetId={tag}*/}\n                        {/*                                targetName={tag}*/}\n                        {/*                            />*/}\n                        {/*                        )}*/}\n                        {/*                    </Badge>*/}\n                        {/*                ))}*/}\n                        {/*            </div>*/}\n                        {/*        </div>*/}\n                        {/*    </CardContent>*/}\n                        {/*</Card>*/}\n                    </>\n                )\n\n            case 'danger':\n                return (\n                    <Card>\n                        <CardHeader>\n                            <CardTitle className=\"text-destructive\">Danger Zone</CardTitle>\n                            <CardDescription>\n                                Destructive actions that require approval\n                            </CardDescription>\n                        </CardHeader>\n                        <CardContent>\n                            <div className=\"space-y-4\">\n                                <div className=\"rounded-md border border-destructive p-4\">\n                                    <h4 className=\"font-medium mb-2\">Delete Project</h4>\n                                    <p className=\"text-sm text-muted-foreground mb-4\">\n                                        Permanently remove this project and all its data\n                                    </p>\n                                    {user?.role === 'ADMIN' ? (\n                                        <AlertDialog>\n                                            <AlertDialogTrigger asChild>\n                                                <Button variant=\"destructive\">Delete Project</Button>\n                                            </AlertDialogTrigger>\n                                            <AlertDialogContent>\n                                                <AlertDialogHeader>\n                                                    <AlertDialogTitle>Delete Project?</AlertDialogTitle>\n                                                    <AlertDialogDescription>\n                                                        This will permanently delete the project and all associated\n                                                        data.\n                                                        This action cannot be undone.\n                                                    </AlertDialogDescription>\n                                                </AlertDialogHeader>\n                                                <AlertDialogFooter>\n                                                    <AlertDialogCancel>Cancel</AlertDialogCancel>\n                                                    <AlertDialogAction\n                                                        className=\"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\n                                                        onClick={() => {\n                                                            setIsDeleting(true)\n                                                            deleteProject.mutate()\n                                                        }}\n                                                        disabled={isDeleting}\n                                                    >\n                                                        {isDeleting ? (\n                                                            <>\n                                                                <Loader2 className=\"mr-2 h-4 w-4 animate-spin\"/>\n                                                                Deleting...\n                                                            </>\n                                                        ) : (\n                                                            'Delete Project'\n                                                        )}\n                                                    </AlertDialogAction>\n                                                </AlertDialogFooter>\n                                            </AlertDialogContent>\n                                        </AlertDialog>\n                                    ) : (\n                                        <DestructiveActionRequest\n                                            projectId={projectId}\n                                            action=\"DELETE_PROJECT\"\n                                            onSuccess={() => router.push('/dashboard/projects')}\n                                        />\n                                    )}\n                                </div>\n                            </div>\n                        </CardContent>\n                    </Card>\n                )\n        }\n    }\n\n    return (\n        <div className=\"min-h-screen bg-background\">\n            <div className=\"container max-w-screen-xl px-4 py-4 md:py-8\">\n                <div className=\"flex flex-col md:flex-row gap-6\">\n                    {/* Left sidebar */}\n                    <div className=\"w-full md:w-64 shrink-0\">\n                        <h1 className=\"text-2xl font-bold mb-4\">Settings</h1>\n                        <nav className=\"flex md:flex-col gap-1 overflow-x-auto md:overflow-x-visible pb-4 md:pb-0\">\n                            {tabs.map(({id, label, icon: Icon, className}) => (\n                                <button\n                                    key={id}\n                                    onClick={() => setActiveTab(id)}\n                                    className={`\n                                        flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium\n                                        ${activeTab === id ? 'bg-secondary' : 'hover:bg-secondary/50'}\n                                        ${className || 'text-foreground'}\n                                        whitespace-nowrap\n                                    `}\n                                >\n                                    <Icon className=\"h-4 w-4\"/>\n                                    {label}\n                                </button>\n                            ))}\n                        </nav>\n                    </div>\n\n                    {/* Main content */}\n                    <div className=\"flex-1\">\n                        {renderTabContent()}\n                    </div>\n                </div>\n            </div>\n        </div>\n    )\n}"
  },
  {
    "path": "app/dashboard/projects/new/page.tsx",
    "content": "'use client'\n\nimport {useState} from 'react'\nimport {useRouter} from 'next/navigation'\nimport {useMutation} from '@tanstack/react-query'\nimport {motion, AnimatePresence} from 'framer-motion'\nimport confetti from 'canvas-confetti'\nimport {Button} from '@/components/ui/button'\nimport {Input} from '@/components/ui/input'\nimport {Label} from '@/components/ui/label'\nimport {Card, CardContent, CardHeader} from '@/components/ui/card'\nimport {Alert, AlertDescription} from '@/components/ui/alert'\nimport {useToast} from '@/hooks/use-toast'\nimport {\n    Loader2,\n    Rocket,\n    CheckCircle2,\n    FileText\n} from 'lucide-react'\n\nexport default function NewProjectPage() {\n    const router = useRouter()\n    const {toast} = useToast()\n    const [name, setName] = useState('')\n    const [showSuccess, setShowSuccess] = useState(false)\n\n    const createProject = useMutation({\n        mutationFn: async (data: { name: string }) => {\n            const response = await fetch('/api/projects', {\n                method: 'POST',\n                credentials: 'include',\n                headers: {\n                    'Content-Type': 'application/json',\n                },\n                body: JSON.stringify(data)\n            })\n\n            if (!response.ok) {\n                const errorData = await response.json().catch(() => ({}))\n                throw new Error(errorData.error || 'Failed to create project')\n            }\n\n            return response.json()\n        },\n        // eslint-disable-next-line @typescript-eslint/no-unused-vars\n        onSuccess: (data) => {\n            // Trigger confetti celebration\n            confetti({\n                particleCount: 100,\n                spread: 70,\n                origin: {y: 0.6},\n                colors: ['#3b82f6', '#8b5cf6', '#06b6d4', '#10b981']\n            })\n\n            setShowSuccess(true)\n\n            // Navigate after showing success\n            setTimeout(() => {\n                toast({\n                    title: 'Success',\n                    description: 'Project created successfully'\n                })\n                router.push('/dashboard/projects')\n            }, 1500)\n        },\n        onError: (error: Error) => {\n            toast({\n                title: 'Error',\n                description: error.message || 'Failed to create project',\n                variant: 'destructive'\n            })\n        }\n    })\n\n    const handleSubmit = async (e: React.FormEvent) => {\n        e.preventDefault()\n        if (!name.trim()) return\n        createProject.mutate({name: name.trim()})\n    }\n\n    if (showSuccess) {\n        return (\n            <div className=\"min-h-screen bg-background flex items-center justify-center\">\n                <motion.div\n                    initial={{opacity: 0, scale: 0.8}}\n                    animate={{opacity: 1, scale: 1}}\n                    className=\"text-center space-y-6\"\n                >\n                    <motion.div\n                        initial={{scale: 0}}\n                        animate={{scale: 1}}\n                        transition={{delay: 0.2, type: \"spring\"}}\n                        className=\"w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mx-auto\"\n                    >\n                        <CheckCircle2 className=\"w-8 h-8 text-green-600 dark:text-green-400\"/>\n                    </motion.div>\n\n                    <div className=\"space-y-2\">\n                        <h1 className=\"text-2xl font-bold\">Project Created!</h1>\n                        <p className=\"text-muted-foreground\">\n                            <span className=\"font-medium text-foreground\">&ldquo;{name}&rdquo;</span> is ready to go\n                        </p>\n                    </div>\n\n                    <div className=\"flex items-center justify-center gap-2 text-sm text-muted-foreground\">\n                        <Loader2 className=\"w-4 h-4 animate-spin\"/>\n                        <span>Redirecting...</span>\n                    </div>\n                </motion.div>\n            </div>\n        )\n    }\n\n    return (\n        <div className=\"min-h-screen bg-background overflow-hidden\">\n            {/* Main Content */}\n            <div className=\"container py-8 flex items-center justify-center min-h-[calc(100vh-4rem)]\">\n                <div className=\"w-full max-w-md space-y-8\">\n                    {/* Hero */}\n                    <motion.div\n                        initial={{opacity: 0, y: 20}}\n                        animate={{opacity: 1, y: 0}}\n                        className=\"text-center space-y-4\"\n                    >\n                        <div className=\"w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center mx-auto\">\n                            <Rocket className=\"w-6 h-6 text-primary\"/>\n                        </div>\n                        <div>\n                            <h1 className=\"text-2xl font-bold mb-2\">Create New Project</h1>\n                            <p className=\"text-muted-foreground\">\n                                Start tracking changes for your project\n                            </p>\n                        </div>\n                    </motion.div>\n\n                    {/* Error Alert */}\n                    <AnimatePresence>\n                        {createProject.isError && (\n                            <motion.div\n                                initial={{opacity: 0, height: 0}}\n                                animate={{opacity: 1, height: 'auto'}}\n                                exit={{opacity: 0, height: 0}}\n                            >\n                                <Alert variant=\"destructive\">\n                                    <AlertDescription>\n                                        {createProject.error?.message || 'Failed to create project'}\n                                    </AlertDescription>\n                                </Alert>\n                            </motion.div>\n                        )}\n                    </AnimatePresence>\n\n                    {/* Form */}\n                    <motion.div\n                        initial={{opacity: 0, y: 20}}\n                        animate={{opacity: 1, y: 0}}\n                        transition={{delay: 0.1}}\n                    >\n                        <Card>\n                            <CardHeader className=\"pb-4\">\n                                <div className=\"flex items-center gap-3\">\n                                    <div\n                                        className=\"w-8 h-8 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center\">\n                                        <FileText className=\"w-4 h-4 text-blue-600 dark:text-blue-400\"/>\n                                    </div>\n                                    <div>\n                                        <h2 className=\"font-semibold\">Project Details</h2>\n                                        <p className=\"text-sm text-muted-foreground\">Give your project a name</p>\n                                    </div>\n                                </div>\n                            </CardHeader>\n\n                            <CardContent>\n                                <form onSubmit={handleSubmit} className=\"space-y-6\">\n                                    <div className=\"space-y-3\">\n                                        <Label htmlFor=\"name\" className=\"text-sm font-medium\">\n                                            Project Name\n                                        </Label>\n                                        <Input\n                                            id=\"name\"\n                                            value={name}\n                                            onChange={(e) => setName(e.target.value)}\n                                            placeholder=\"e.g., My Website, Mobile App, API Service\"\n                                            className=\"h-11\"\n                                            disabled={createProject.isPending}\n                                            autoFocus\n                                        />\n                                    </div>\n\n                                    <div className=\"flex gap-3 pt-2\">\n                                        <Button\n                                            type=\"button\"\n                                            variant=\"outline\"\n                                            onClick={() => router.push('/dashboard/projects')}\n                                            disabled={createProject.isPending}\n                                            className=\"flex-1\"\n                                        >\n                                            Cancel\n                                        </Button>\n\n                                        <Button\n                                            type=\"submit\"\n                                            disabled={!name.trim() || createProject.isPending}\n                                            className=\"flex-1 gap-2\"\n                                        >\n                                            {createProject.isPending ? (\n                                                <>\n                                                    <Loader2 className=\"h-4 w-4 animate-spin\"/>\n                                                    Creating...\n                                                </>\n                                            ) : (\n                                                <>\n                                                    <Rocket className=\"h-4 w-4\"/>\n                                                    Create Project\n                                                </>\n                                            )}\n                                        </Button>\n                                    </div>\n                                </form>\n                            </CardContent>\n                        </Card>\n                    </motion.div>\n                </div>\n            </div>\n        </div>\n    )\n}"
  },
  {
    "path": "app/dashboard/projects/page.tsx",
    "content": "'use client'\n\nimport { useEffect } from 'react'\nimport { useRouter } from 'next/navigation'\nimport { useAuth } from '@/context/auth'\nimport ProjectsPage from \"@/components/project/ProjectSettingsPage\";\n\ninterface PageProps {\n    params: Promise<{ projectId: string }>\n}\n\nexport default function SettingsPageWrapper({ }: PageProps) {\n    const router = useRouter()\n    const { user } = useAuth()\n\n    useEffect(() => {\n        if (user && user.role !== 'ADMIN') {\n            router.push('/dashboard/projects')\n        }\n    }, [user, router])\n\n    return <ProjectsPage/>\n}"
  },
  {
    "path": "app/dashboard/providers.tsx",
    "content": "'use client'\n\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { useState } from 'react'\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n    const [queryClient] = useState(() => new QueryClient({\n        defaultOptions: {\n            queries: {\n                retry: false,\n                refetchOnWindowFocus: false,\n            },\n        },\n    }))\n\n    return (\n        <QueryClientProvider client={queryClient}>\n            {children}\n        </QueryClientProvider>\n    )\n}"
  },
  {
    "path": "app/dashboard/requests/page.tsx",
    "content": "'use client';\n\nimport { useState, useEffect } from 'react';\nimport { useRouter } from 'next/navigation';\nimport { useAuth } from '@/context/auth';\nimport { useToast } from '@/hooks/use-toast';\nimport { formatDistanceToNow } from 'date-fns';\nimport { Role } from '@prisma/client';\nimport { motion } from 'framer-motion';\n\n// UI Components from shadcn\nimport { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';\nimport { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport { Skeleton } from '@/components/ui/skeleton';\nimport {\n    AlertCircle,\n    CheckCircle,\n    Clock,\n    XCircle,\n    ArrowUpRight,\n    AlertTriangle,\n    FileText,\n    Tag,\n    Package,\n    Send\n} from 'lucide-react';\nimport { Separator } from '@/components/ui/separator';\nimport { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';\nimport { Avatar, AvatarFallback } from '@/components/ui/avatar';\nimport { ScrollArea } from '@/components/ui/scroll-area';\n\n// Types\ninterface RequestData {\n    id: string;\n    type: string;\n    status: string;\n    createdAt: string;\n    reviewedAt: string | null;\n    project: {\n        id: string;\n        name: string;\n    };\n    ChangelogEntry?: {\n        id: string;\n        title: string;\n    } | null;\n    ChangelogTag?: {\n        id: string;\n        targetId: string;\n    } | null;\n    admin?: {\n        id: string;\n        name: string | null;\n        email: string;\n    } | null;\n}\n\nexport default function RequestsPage() {\n    const { user, isLoading: authLoading } = useAuth();\n    const router = useRouter();\n    const { toast } = useToast();\n    const [requests, setRequests] = useState<RequestData[]>([]);\n    const [isLoading, setIsLoading] = useState(true);\n    const [error, setError] = useState<string | null>(null);\n    const [activeTab, setActiveTab] = useState('all');\n\n    useEffect(() => {\n        // Redirect to login if not authenticated\n        if (!authLoading && !user) {\n            router.push('/login');\n            return;\n        }\n\n        // Only staff and admin can access this page\n        if (user && user.role === Role.VIEWER) {\n            router.push('/dashboard');\n            toast({\n                title: 'Access Denied',\n                description: 'You do not have permission to view this page.',\n                variant: 'destructive'\n            });\n            return;\n        }\n\n        async function fetchRequests() {\n            try {\n                setIsLoading(true);\n                const response = await fetch('/api/requests');\n\n                if (!response.ok) {\n                    const data = await response.json();\n                    throw new Error(data.error || 'Failed to fetch requests');\n                }\n\n                const data = await response.json();\n                setRequests(data.requests);\n            } catch (err) {\n                const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred';\n                setError(errorMessage);\n                toast({\n                    title: 'Error',\n                    description: errorMessage,\n                    variant: 'destructive'\n                });\n            } finally {\n                setIsLoading(false);\n            }\n        }\n\n        if (user) {\n            fetchRequests();\n        }\n    }, [user, authLoading, router, toast]);\n\n    // Filter requests based on active tab\n    const filteredRequests = requests.filter(request => {\n        if (activeTab === 'all') return true;\n        return request.status.toLowerCase() === activeTab;\n    });\n\n    // Get request icon based on type\n    const getRequestIcon = (type: string) => {\n        switch (type) {\n            case 'DELETE_ENTRY':\n                return <FileText className=\"h-5 w-5 text-red-500\" />;\n            case 'DELETE_TAG':\n                return <Tag className=\"h-5 w-5 text-amber-500\" />;\n            case 'DELETE_PROJECT':\n                return <Package className=\"h-5 w-5 text-red-600\" />;\n            case 'ALLOW_PUBLISH':\n                return <Send className=\"h-5 w-5 text-blue-500\" />;\n            default:\n                return <AlertCircle className=\"h-5 w-5\" />;\n        }\n    };\n\n    // Helper function to get the target name based on request type\n    const getTargetName = (request: RequestData) => {\n        if (request.type === 'DELETE_ENTRY' && request.ChangelogEntry) {\n            return request.ChangelogEntry.title;\n        } else if (request.type === 'DELETE_TAG' && request.ChangelogTag) {\n            return request.ChangelogTag.targetId;\n        } else if (request.type === 'DELETE_PROJECT') {\n            return request.project.name;\n        } else if (request.type === 'ALLOW_PUBLISH' && request.ChangelogEntry) {\n            return request.ChangelogEntry.title;\n        }\n        return 'Unknown';\n    };\n\n    // Helper function to get human-readable request type\n    const getReadableType = (type: string) => {\n        switch (type) {\n            case 'DELETE_ENTRY':\n                return 'Delete Entry';\n            case 'DELETE_TAG':\n                return 'Delete Tag';\n            case 'DELETE_PROJECT':\n                return 'Delete Project';\n            case 'ALLOW_PUBLISH':\n                return 'Publish Entry';\n            default:\n                return type.replace(/_/g, ' ').toLowerCase();\n        }\n    };\n\n    // Helper function to render status badge\n    const renderStatusBadge = (status: string) => {\n        switch (status) {\n            case 'PENDING':\n                return (\n                    <Badge variant=\"outline\" className=\"bg-yellow-50 text-yellow-700 border-yellow-200 hover:bg-yellow-50\">\n                        <Clock className=\"mr-1 h-3 w-3\" /> Pending\n                    </Badge>\n                );\n            case 'APPROVED':\n                return (\n                    <Badge variant=\"outline\" className=\"bg-green-50 text-green-700 border-green-200 hover:bg-green-50\">\n                        <CheckCircle className=\"mr-1 h-3 w-3\" /> Approved\n                    </Badge>\n                );\n            case 'REJECTED':\n                return (\n                    <Badge variant=\"outline\" className=\"bg-red-50 text-red-700 border-red-200 hover:bg-red-50\">\n                        <XCircle className=\"mr-1 h-3 w-3\" /> Rejected\n                    </Badge>\n                );\n            default:\n                return (\n                    <Badge variant=\"outline\">\n                        {status}\n                    </Badge>\n                );\n        }\n    };\n\n    // Navigate to view project details\n    const navigateToProject = (projectId: string) => {\n        router.push(`/dashboard/projects/${projectId}`);\n    };\n\n    // Render admin info if request has been reviewed\n    const renderReviewer = (request: RequestData) => {\n        if (request.status === 'PENDING' || !request.admin) return null;\n\n        const reviewerName = request.admin.name || request.admin.email;\n        const initials = reviewerName\n            .split(' ')\n            .map(name => name[0])\n            .join('')\n            .toUpperCase()\n            .slice(0, 2);\n\n        return (\n            <div className=\"flex items-center gap-2 mt-2 pt-2 border-t border-gray-100\">\n                <Avatar className=\"h-6 w-6\">\n                    <AvatarFallback className=\"text-xs\">{initials}</AvatarFallback>\n                </Avatar>\n                <div className=\"text-sm\">\n                    <span className=\"text-muted-foreground\">Reviewed by </span>\n                    <span className=\"font-medium\">{reviewerName}</span>\n                    {request.reviewedAt && (\n                        <span className=\"text-muted-foreground text-xs ml-1\">\n              ({formatDistanceToNow(new Date(request.reviewedAt), { addSuffix: true })})\n            </span>\n                    )}\n                </div>\n            </div>\n        );\n    };\n\n    if (isLoading) {\n        return (\n            <div className=\"container mx-auto px-4 py-8 max-w-6xl\">\n                <div className=\"flex items-center justify-between mb-6\">\n                    <div>\n                        <h1 className=\"text-3xl font-bold\">Changelog Requests</h1>\n                        <p className=\"text-muted-foreground mt-1\">Track the status of your submitted requests</p>\n                    </div>\n                </div>\n                <Separator className=\"my-6\" />\n                <div className=\"space-y-4\">\n                    {[1, 2, 3].map((i) => (\n                        <Card key={i} className=\"overflow-hidden\">\n                            <CardHeader className=\"pb-2\">\n                                <Skeleton className=\"h-4 w-1/3\" />\n                                <Skeleton className=\"h-8 w-2/3\" />\n                            </CardHeader>\n                            <CardContent>\n                                <Skeleton className=\"h-4 w-full mb-2\" />\n                                <Skeleton className=\"h-4 w-1/2\" />\n                            </CardContent>\n                        </Card>\n                    ))}\n                </div>\n            </div>\n        );\n    }\n\n    if (error) {\n        return (\n            <div className=\"container mx-auto px-4 py-8 max-w-6xl\">\n                <div className=\"flex items-center justify-between mb-6\">\n                    <div>\n                        <h1 className=\"text-3xl font-bold\">Changelog Requests</h1>\n                        <p className=\"text-muted-foreground mt-1\">Track the status of your submitted requests</p>\n                    </div>\n                </div>\n                <Separator className=\"my-6\" />\n                <Alert variant=\"destructive\" className=\"my-8\">\n                    <AlertCircle className=\"h-4 w-4\" />\n                    <AlertTitle>Error</AlertTitle>\n                    <AlertDescription>\n                        {error}\n                    </AlertDescription>\n                </Alert>\n                <div className=\"flex justify-center mt-6\">\n                    <Button\n                        onClick={() => window.location.reload()}\n                        variant=\"outline\"\n                    >\n                        Try Again\n                    </Button>\n                </div>\n            </div>\n        );\n    }\n\n    const pendingCount = requests.filter(r => r.status === 'PENDING').length;\n    const approvedCount = requests.filter(r => r.status === 'APPROVED').length;\n    const rejectedCount = requests.filter(r => r.status === 'REJECTED').length;\n\n    return (\n        <div className=\"container mx-auto px-4 py-8 max-w-6xl\">\n            <div className=\"flex items-center justify-between mb-6\">\n                <div>\n                    <h1 className=\"text-3xl font-bold\">Changelog Requests</h1>\n                    <p className=\"text-muted-foreground mt-1\">Track the status of your submitted requests</p>\n                </div>\n            </div>\n\n            <Tabs\n                defaultValue=\"all\"\n                value={activeTab}\n                onValueChange={setActiveTab}\n                className=\"w-full\"\n            >\n                <div className=\"flex justify-between items-center mb-4\">\n                    <TabsList className=\"grid grid-cols-4 md:w-auto\">\n                        <TabsTrigger value=\"all\" className=\"px-4\">\n                            All\n                            <Badge variant=\"secondary\" className=\"ml-2\">{requests.length}</Badge>\n                        </TabsTrigger>\n                        <TabsTrigger value=\"pending\" className=\"px-4\">\n                            Pending\n                            {pendingCount > 0 && <Badge variant=\"secondary\" className=\"ml-2\">{pendingCount}</Badge>}\n                        </TabsTrigger>\n                        <TabsTrigger value=\"approved\" className=\"px-4\">\n                            Approved\n                            {approvedCount > 0 && <Badge variant=\"secondary\" className=\"ml-2\">{approvedCount}</Badge>}\n                        </TabsTrigger>\n                        <TabsTrigger value=\"rejected\" className=\"px-4\">\n                            Rejected\n                            {rejectedCount > 0 && <Badge variant=\"secondary\" className=\"ml-2\">{rejectedCount}</Badge>}\n                        </TabsTrigger>\n                    </TabsList>\n                </div>\n\n                <TabsContent value={activeTab} className=\"mt-0\">\n                    {filteredRequests.length === 0 ? (\n                        <motion.div\n                            initial={{ opacity: 0, y: 10 }}\n                            animate={{ opacity: 1, y: 0 }}\n                            className=\"text-center py-16 px-4 bg-muted/30 rounded-lg\"\n                        >\n                            <div className=\"inline-flex items-center justify-center w-16 h-16 rounded-full bg-muted mb-4\">\n                                {activeTab === 'pending' ? (\n                                    <Clock className=\"h-8 w-8 text-muted-foreground\" />\n                                ) : activeTab === 'approved' ? (\n                                    <CheckCircle className=\"h-8 w-8 text-muted-foreground\" />\n                                ) : activeTab === 'rejected' ? (\n                                    <XCircle className=\"h-8 w-8 text-muted-foreground\" />\n                                ) : (\n                                    <AlertTriangle className=\"h-8 w-8 text-muted-foreground\" />\n                                )}\n                            </div>\n                            <h3 className=\"text-xl font-semibold mb-2\">No {activeTab !== 'all' ? activeTab : ''} Requests Found</h3>\n                            <p className=\"text-muted-foreground max-w-md mx-auto mb-6\">\n                                {activeTab === 'pending'\n                                    ? \"You don't have any pending requests awaiting approval.\"\n                                    : activeTab === 'approved'\n                                        ? \"You don't have any approved requests yet.\"\n                                        : activeTab === 'rejected'\n                                            ? \"You don't have any rejected requests.\"\n                                            : \"You haven't submitted any requests yet.\"}\n                            </p>\n                        </motion.div>\n                    ) : (\n                        <ScrollArea className=\"h-[calc(100vh-250px)] pr-4\">\n                            <div className=\"space-y-4\">\n                                {filteredRequests.map((request, index) => (\n                                    <motion.div\n                                        key={request.id}\n                                        initial={{ opacity: 0, y: 20 }}\n                                        animate={{ opacity: 1, y: 0 }}\n                                        transition={{ delay: index * 0.05 }}\n                                    >\n                                        <Card className=\"overflow-hidden border-l-4 shadow-sm hover:shadow-md transition-shadow\"\n                                              style={{\n                                                  borderLeftColor: request.status === 'PENDING'\n                                                      ? 'rgb(234 179 8)'\n                                                      : request.status === 'APPROVED'\n                                                          ? 'rgb(22 163 74)'\n                                                          : 'rgb(220 38 38)'\n                                              }}\n                                        >\n                                            <CardHeader className=\"pb-2 flex flex-row items-center justify-between\">\n                                                <div className=\"flex items-center gap-3\">\n                                                    <div className=\"rounded-md bg-muted p-2\">\n                                                        {getRequestIcon(request.type)}\n                                                    </div>\n                                                    <div>\n                                                        <div className=\"flex items-center gap-2\">\n                                                            <CardTitle className=\"text-lg\">\n                                                                {getTargetName(request)}\n                                                            </CardTitle>\n                                                            {renderStatusBadge(request.status)}\n                                                        </div>\n                                                        <p className=\"text-sm text-muted-foreground mt-1\">\n                                                            {getReadableType(request.type)} in <span className=\"font-medium\">{request.project.name}</span>\n                                                        </p>\n                                                    </div>\n                                                </div>\n                                                <div className=\"text-sm text-muted-foreground\">\n                                                    {formatDistanceToNow(new Date(request.createdAt), { addSuffix: true })}\n                                                </div>\n                                            </CardHeader>\n\n                                            <CardContent>\n                                                {renderReviewer(request)}\n                                            </CardContent>\n\n                                            <CardFooter className=\"pt-0 flex justify-end\">\n                                                <Button\n                                                    variant=\"ghost\"\n                                                    size=\"sm\"\n                                                    className=\"gap-1 text-primary\"\n                                                    onClick={() => navigateToProject(request.project.id)}\n                                                >\n                                                    View Project\n                                                    <ArrowUpRight className=\"h-4 w-4\" />\n                                                </Button>\n                                            </CardFooter>\n                                        </Card>\n                                    </motion.div>\n                                ))}\n                            </div>\n                        </ScrollArea>\n                    )}\n                </TabsContent>\n            </Tabs>\n        </div>\n    );\n}"
  },
  {
    "path": "app/dashboard/settings/page.tsx",
    "content": "'use client'\n\nimport React, {useState, useEffect} from 'react';\nimport {useAuth} from '@/context/auth';\nimport {useRouter} from 'next/navigation';\nimport {useThemeWithLoading} from '@/components/theme-provider';\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card';\nimport {Button} from '@/components/ui/button';\nimport {Input} from '@/components/ui/input';\nimport {Label} from '@/components/ui/label';\nimport {useToast} from '@/hooks/use-toast';\nimport {Loader2, Moon, Sun, Save, Bell, Lock, Globe} from 'lucide-react';\nimport {motion, AnimatePresence} from 'framer-motion';\nimport {useMediaQuery} from '@/hooks/use-media-query';\nimport {Switch} from '@/components/ui/switch';\nimport {Separator} from '@/components/ui/separator';\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogFooter,\n    DialogHeader,\n    DialogTitle,\n} from '@/components/ui/dialog';\nimport {PasskeysSection} from \"@/components/settings/passkeys-section\";\nimport ConnectedSsoProviders from '@/components/settings/connected-sso-section';\nimport {SearchableSelect} from '@/components/ui/searchable-select';\nimport {getTimezonesByRegion} from '@/lib/constants/timezones';\n\ninterface FormState {\n    name: string;\n    enableNotifications: boolean;\n    timezone: string | null;\n}\n\ninterface TimezoneConfig {\n    allowUserTimezone: boolean;\n    timezone: string;\n    source: 'user' | 'system';\n}\n\ninterface OAuthProvider {\n    id: string;\n    name: string;\n    enabled: boolean;\n    isDefault: boolean;\n}\n\ninterface OAuthConnection {\n    id: string;\n    providerId: string;\n    provider: OAuthProvider;\n    providerUserId: string;\n    expiresAt: string | null;\n    createdAt: string;\n    updatedAt: string;\n}\n\ninterface SAMLProvider {\n    id: string;\n    name: string;\n    enabled: boolean;\n    isDefault: boolean;\n}\n\ninterface SAMLConnection {\n    id: string;\n    providerId: string;\n    provider: SAMLProvider;\n    nameId: string;\n    createdAt: string;\n    updatedAt: string;\n}\n\ninterface SsoData {\n    connections: OAuthConnection[];\n    allProviders: OAuthProvider[];\n    samlConnections: SAMLConnection[];\n    allSAMLProviders: SAMLProvider[];\n}\n\nexport default function SettingsPage() {\n    const {user} = useAuth();\n    const {theme, setTheme, isLoading: themeLoading} = useThemeWithLoading();\n    const router = useRouter();\n    const {toast} = useToast();\n    const [isLoading, setIsLoading] = useState(false);\n    const [isFetching, setIsFetching] = useState(true);\n    const [isResetDialogOpen, setIsResetDialogOpen] = useState(false);\n    const [isResettingPassword, setIsResettingPassword] = useState(false);\n    const isMobile = useMediaQuery(\"(max-width: 640px)\");\n\n    // SSO connections state\n    const [ssoData, setSsoData] = useState<SsoData>({\n        connections: [],\n        allProviders: [],\n        samlConnections: [],\n        allSAMLProviders: [],\n    });\n    const [isSsoLoading, setIsSsoLoading] = useState(true);\n\n    // Original saved values - these don't change unless we explicitly save\n    const [savedValues, setSavedValues] = useState<FormState | null>(null);\n\n    const [timezoneConfig, setTimezoneConfig] = useState<TimezoneConfig>({\n        allowUserTimezone: true,\n        timezone: 'UTC',\n        source: 'system',\n    });\n\n    // Current form values that the user is editing\n    const [formState, setFormState] = useState<FormState>({\n        name: '',\n        enableNotifications: true,\n        timezone: null,\n    });\n\n    // Fetch current settings\n    useEffect(() => {\n        async function fetchSettings() {\n            try {\n                setIsFetching(true);\n                const [settingsRes, tzRes] = await Promise.all([\n                    fetch('/api/auth/settings'),\n                    fetch('/api/config/timezone'),\n                ]);\n\n                if (tzRes.ok) {\n                    const tzData = await tzRes.json();\n                    setTimezoneConfig(tzData);\n                }\n\n                if (settingsRes.ok) {\n                    const data = await settingsRes.json();\n\n                    const initialValues: FormState = {\n                        name: user?.name || '',\n                        enableNotifications: data.enableNotifications !== undefined\n                            ? data.enableNotifications\n                            : true,\n                        timezone: data.timezone ?? null,\n                    };\n\n                    setSavedValues(initialValues);\n                    setFormState(initialValues);\n                }\n            } catch (error) {\n                console.error('Failed to fetch settings:', error);\n                toast({\n                    title: 'Error',\n                    description: 'Failed to load your settings. Please try again.',\n                    variant: 'destructive',\n                });\n            } finally {\n                setIsFetching(false);\n            }\n        }\n\n        if (user) {\n            fetchSettings();\n        }\n    }, [user, toast]);\n\n    // Fetch SSO connections\n    useEffect(() => {\n        async function fetchSsoConnections() {\n            try {\n                setIsSsoLoading(true);\n                const response = await fetch('/api/auth/connections');\n                if (response.ok) {\n                    const data = await response.json();\n                    setSsoData(data);\n                }\n            } catch (error) {\n                console.error('Failed to fetch SSO connections:', error);\n                // Don't show error toast for SSO data as it's not critical\n            } finally {\n                setIsSsoLoading(false);\n            }\n        }\n\n        if (user) {\n            fetchSsoConnections();\n        }\n    }, [user]);\n\n    // Check if there are unsaved changes\n    const hasChanges = savedValues ? (\n        formState.name !== savedValues.name ||\n        formState.enableNotifications !== savedValues.enableNotifications ||\n        formState.timezone !== savedValues.timezone\n    ) : false;\n\n    // Handle theme toggle - now much simpler\n    const handleThemeToggle = (newTheme: string) => {\n        setTheme(newTheme);\n    };\n\n    // Handle name change\n    const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n        setFormState(prev => ({...prev, name: e.target.value}));\n    };\n\n    // Handle notification toggle\n    const handleNotificationToggle = (checked: boolean) => {\n        setFormState(prev => ({...prev, enableNotifications: checked}));\n    };\n\n    // Handle password reset request\n    const handlePasswordReset = async () => {\n        setIsResettingPassword(true);\n        try {\n            const response = await fetch('/api/auth/reset-password/request', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n            });\n\n            if (!response.ok) {\n                throw new Error('Failed to request password reset');\n            }\n\n            await response.json();\n\n            toast({\n                title: 'Password reset email sent',\n                description: 'Check your email for a link to reset your password.',\n            });\n\n            setIsResetDialogOpen(false);\n        } catch (error) {\n            toast({\n                title: 'Error',\n                description: error instanceof Error ? error.message : 'An error occurred',\n                variant: 'destructive',\n            });\n        } finally {\n            setIsResettingPassword(false);\n        }\n    };\n\n    // Handle save\n    const handleSave = async () => {\n        if (!hasChanges || !savedValues) return;\n\n        setIsLoading(true);\n        try {\n            const response = await fetch('/api/auth/settings', {\n                method: 'PATCH',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify(formState),\n            });\n\n            if (!response.ok) throw new Error('Failed to update settings');\n\n            // Update saved values to reflect the new saved state\n            setSavedValues(formState);\n\n            toast({\n                title: 'Settings saved',\n                description: 'Your settings have been updated successfully.',\n            });\n\n            // Refresh the router to update any server-side data\n            router.refresh();\n\n        } catch (error) {\n            toast({\n                title: 'Error',\n                description: error instanceof Error ? error.message : 'An error occurred',\n                variant: 'destructive',\n            });\n        } finally {\n            setIsLoading(false);\n        }\n    };\n\n    // Handle cancel/revert changes\n    const handleCancel = () => {\n        if (savedValues) {\n            setFormState(savedValues);\n        }\n    };\n\n    if (isFetching || themeLoading) {\n        return (\n            <div className=\"flex items-center justify-center h-full\">\n                <Loader2 className=\"h-8 w-8 animate-spin text-primary\"/>\n            </div>\n        );\n    }\n\n    return (\n        <div className=\"min-h-screen bg-background\">\n\n            <form\n                onSubmit={(e) => {\n                    e.preventDefault();\n                    handleSave();\n                }}\n                className=\"container max-w-2xl px-4 md:px-6 space-y-4 md:space-y-6 pb-20 md:pb-8\"\n            >\n                {/* Desktop header */}\n                <div\n                    className=\"hidden md:flex sticky top-0 z-10 bg-background pt-4 pb-2 mb-4 flex-col sm:flex-row sm:items-center sm:justify-between gap-4\">\n                    <div className=\"flex items-center\">\n                        <div>\n                            <h1 className=\"text-2xl md:text-3xl font-bold tracking-tight\">Settings</h1>\n                            <p className=\"text-sm text-muted-foreground mt-1\">\n                                Manage your account settings and preferences.\n                            </p>\n                        </div>\n                    </div>\n\n                    <AnimatePresence>\n                        {hasChanges && (\n                            <motion.div\n                                initial={{opacity: 0, y: 20}}\n                                animate={{opacity: 1, y: 0}}\n                                exit={{opacity: 0, y: 20}}\n                                transition={{duration: 0.2}}\n                                className=\"flex gap-2 w-full sm:w-auto\"\n                            >\n                                <Button\n                                    type=\"button\"\n                                    variant=\"outline\"\n                                    onClick={handleCancel}\n                                    className=\"flex-1 sm:flex-initial\"\n                                >\n                                    Cancel\n                                </Button>\n                                <Button\n                                    type=\"submit\"\n                                    disabled={isLoading}\n                                    className=\"flex-1 sm:flex-initial sm:min-w-[100px]\"\n                                >\n                                    {isLoading ? (\n                                        <Loader2 className=\"h-4 w-4 animate-spin\"/>\n                                    ) : (\n                                        <>\n                                            <Save className=\"mr-2 h-4 w-4\"/>\n                                            Save Changes\n                                        </>\n                                    )}\n                                </Button>\n                            </motion.div>\n                        )}\n                    </AnimatePresence>\n                </div>\n\n                {/* Theme selection card */}\n                <Card className=\"border shadow-sm\">\n                    <CardHeader className=\"pb-3\">\n                        <CardTitle className=\"text-lg md:text-xl\">Appearance</CardTitle>\n                        <CardDescription className=\"text-sm\">\n                            Choose your preferred theme. Your selection is automatically saved.\n                        </CardDescription>\n                    </CardHeader>\n                    <CardContent>\n                        <div className=\"grid grid-cols-2 gap-3\">\n                            <ThemeButton\n                                isActive={theme === 'light'}\n                                onClick={() => handleThemeToggle('light')}\n                                icon={<Sun className=\"h-4 w-4 md:h-5 md:w-5\"/>}\n                                name=\"Light\"\n                                disabled={themeLoading}\n                            />\n                            <ThemeButton\n                                isActive={theme === 'dark'}\n                                onClick={() => handleThemeToggle('dark')}\n                                icon={<Moon className=\"h-4 w-4 md:h-5 md:w-5\"/>}\n                                name=\"Dark\"\n                                disabled={themeLoading}\n                            />\n                        </div>\n                    </CardContent>\n                </Card>\n\n                {/* Timezone card */}\n                {timezoneConfig.allowUserTimezone && (\n                    <Card className=\"border shadow-sm\">\n                        <CardHeader className=\"pb-3\">\n                            <CardTitle className=\"text-lg md:text-xl flex items-center gap-2\">\n                                <Globe className=\"h-5 w-5 text-muted-foreground\" />\n                                Timezone\n                            </CardTitle>\n                            <CardDescription className=\"text-sm\">\n                                Set your preferred timezone. Leave as system default to use the global setting ({timezoneConfig.timezone}).\n                            </CardDescription>\n                        </CardHeader>\n                        <CardContent>\n                            <SearchableSelect\n                                value={formState.timezone ?? '__system__'}\n                                onValueChange={(value) =>\n                                    setFormState(prev => ({\n                                        ...prev,\n                                        timezone: value === '__system__' ? null : value,\n                                    }))\n                                }\n                                placeholder=\"Select timezone\"\n                                searchPlaceholder=\"Search timezones...\"\n                                items={[\n                                    {\n                                        value: '__system__',\n                                        label: `System Default (${timezoneConfig.timezone})`,\n                                        searchValue: 'system default',\n                                    },\n                                ]}\n                                groups={Object.entries(getTimezonesByRegion()).map(([region, tzs]) => ({\n                                    heading: region,\n                                    items: tzs.map(tz => ({\n                                        value: tz.value,\n                                        label: `${tz.label} (${tz.value})`,\n                                        searchValue: `${tz.label} ${tz.value} ${region}`,\n                                    })),\n                                }))}\n                            />\n                        </CardContent>\n                    </Card>\n                )}\n\n                {/* Notification settings card */}\n                <Card className=\"border shadow-sm\">\n                    <CardHeader className=\"pb-3\">\n                        <CardTitle className=\"text-lg md:text-xl\">Notifications</CardTitle>\n                        <CardDescription className=\"text-sm\">\n                            Manage your notification preferences.\n                        </CardDescription>\n                    </CardHeader>\n                    <CardContent className=\"space-y-4\">\n                        <div className=\"flex items-start justify-between space-x-3\">\n                            <div className=\"flex items-start space-x-3 flex-1\">\n                                <Bell className=\"h-5 w-5 text-muted-foreground mt-0.5 flex-shrink-0\"/>\n                                <div className=\"min-w-0 flex-1\">\n                                    <p className=\"font-medium text-sm md:text-base\">Email Notifications</p>\n                                    <p className=\"text-xs md:text-sm text-muted-foreground mt-1\">\n                                        Receive email notifications for important events like request approvals.\n                                    </p>\n                                </div>\n                            </div>\n                            <Switch\n                                checked={formState.enableNotifications}\n                                onCheckedChange={handleNotificationToggle}\n                                className=\"flex-shrink-0\"\n                            />\n                        </div>\n                    </CardContent>\n                </Card>\n\n                {/* Profile card */}\n                <Card className=\"border shadow-sm\">\n                    <CardHeader className=\"pb-3\">\n                        <CardTitle className=\"text-lg md:text-xl\">Profile</CardTitle>\n                        <CardDescription className=\"text-sm\">\n                            Update your profile information.\n                        </CardDescription>\n                    </CardHeader>\n                    <CardContent className=\"space-y-4 md:space-y-5\">\n                        <div className=\"space-y-2\">\n                            <Label htmlFor=\"name\" className=\"text-sm font-medium\">Display Name</Label>\n                            <Input\n                                id=\"name\"\n                                value={formState.name}\n                                onChange={handleNameChange}\n                                placeholder=\"Enter your display name\"\n                                className=\"h-10 md:h-11\"\n                            />\n                        </div>\n                        <div className=\"space-y-2\">\n                            <Label htmlFor=\"email\" className=\"text-sm font-medium\">Email</Label>\n                            <Input\n                                id=\"email\"\n                                value={user?.email}\n                                disabled\n                                className=\"h-10 md:h-11 bg-muted cursor-not-allowed\"\n                            />\n                            <p className=\"text-xs text-muted-foreground mt-1\">\n                                Your email address cannot be changed.\n                            </p>\n                        </div>\n\n                        <Separator className=\"my-4\"/>\n\n                        {/* Password Reset Section */}\n                        <div className=\"pt-2\">\n                            <div className=\"flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3\">\n                                <div className=\"space-y-1 flex-1\">\n                                    <h3 className=\"text-sm font-medium flex items-center\">\n                                        <Lock className=\"h-4 w-4 mr-1\"/>\n                                        Password\n                                    </h3>\n                                    <p className=\"text-xs text-muted-foreground\">\n                                        Reset your account password via email.\n                                    </p>\n                                </div>\n                                <Button\n                                    variant=\"outline\"\n                                    size=\"sm\"\n                                    onClick={() => setIsResetDialogOpen(true)}\n                                    className=\"w-full sm:w-auto\"\n                                >\n                                    Reset Password\n                                </Button>\n                            </div>\n                        </div>\n                    </CardContent>\n                </Card>\n\n                {/* Connected SSO Providers Section */}\n                <ConnectedSsoProviders\n                    connections={ssoData.connections}\n                    allProviders={ssoData.allProviders}\n                    samlConnections={ssoData.samlConnections}\n                    allSAMLProviders={ssoData.allSAMLProviders}\n                    isLoading={isSsoLoading}\n                />\n\n                {/* Passkeys Section */}\n                <PasskeysSection/>\n\n                {/* Password Reset Dialog */}\n                <Dialog open={isResetDialogOpen} onOpenChange={setIsResetDialogOpen}>\n                    <DialogContent>\n                        <DialogHeader>\n                            <DialogTitle>Reset Password</DialogTitle>\n                            <DialogDescription>\n                                This will send a password reset link to your email address: {user?.email}\n                            </DialogDescription>\n                        </DialogHeader>\n                        <p className=\"text-sm text-muted-foreground\">\n                            For security reasons, you will be logged out of all devices after resetting your password.\n                        </p>\n                        <DialogFooter>\n                            <Button\n                                variant=\"outline\"\n                                onClick={() => setIsResetDialogOpen(false)}\n                            >\n                                Cancel\n                            </Button>\n                            <Button\n                                onClick={handlePasswordReset}\n                                disabled={isResettingPassword}\n                            >\n                                {isResettingPassword ? (\n                                    <>\n                                        <Loader2 className=\"mr-2 h-4 w-4 animate-spin\"/>\n                                        Sending...\n                                    </>\n                                ) : 'Send Reset Link'}\n                            </Button>\n                        </DialogFooter>\n                    </DialogContent>\n                </Dialog>\n\n                {/* Mobile fixed save button */}\n                {isMobile && hasChanges && (\n                    <motion.div\n                        initial={{y: 100, opacity: 0}}\n                        animate={{y: 0, opacity: 1}}\n                        exit={{y: 100, opacity: 0}}\n                        className=\"fixed bottom-0 left-0 right-0 p-4 bg-background/95 backdrop-blur-sm border-t z-40\"\n                    >\n                        <div className=\"flex gap-3 max-w-sm mx-auto\">\n                            <Button\n                                type=\"button\"\n                                variant=\"outline\"\n                                onClick={handleCancel}\n                                className=\"flex-1 h-11\"\n                            >\n                                Cancel\n                            </Button>\n                            <Button\n                                type=\"submit\"\n                                disabled={isLoading}\n                                className=\"flex-1 h-11\"\n                            >\n                                {isLoading ? (\n                                    <Loader2 className=\"h-4 w-4 animate-spin\"/>\n                                ) : (\n                                    <>\n                                        <Save className=\"mr-2 h-4 w-4\"/>\n                                        Save\n                                    </>\n                                )}\n                            </Button>\n                        </div>\n                    </motion.div>\n                )}\n            </form>\n        </div>\n    );\n}\n\n// A component for theme selection buttons\nfunction ThemeButton({\n                         isActive,\n                         onClick,\n                         icon,\n                         name,\n                         disabled = false\n                     }: {\n    isActive: boolean;\n    onClick: () => void;\n    icon: React.ReactNode;\n    name: string;\n    disabled?: boolean;\n}) {\n    return (\n        <Button\n            type=\"button\"\n            variant={isActive ? \"default\" : \"outline\"}\n            className=\"w-full relative h-14 justify-start px-5\"\n            onClick={onClick}\n            disabled={disabled}\n        >\n            <div className=\"flex items-center\">\n                <div className=\"mr-3\">\n                    {icon}\n                </div>\n                <span className=\"font-medium\">{name}</span>\n            </div>\n\n            {isActive && (\n                <motion.span\n                    initial={{opacity: 0, scale: 0.8}}\n                    animate={{opacity: 1, scale: 1}}\n                    className=\"absolute right-3 text-xs bg-primary-foreground text-primary px-2 py-1 rounded-full\"\n                >\n                    Active\n                </motion.span>\n            )}\n        </Button>\n    );\n}"
  },
  {
    "path": "app/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nbody {\n  font-family: Arial, Helvetica, sans-serif;\n}\n\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --foreground: 222.2 84% 4.9%;\n    --card: 0 0% 100%;\n    --card-foreground: 222.2 84% 4.9%;\n    --popover: 0 0% 100%;\n    --popover-foreground: 222.2 84% 4.9%;\n    --primary: 222.2 47.4% 11.2%;\n    --primary-foreground: 210 40% 98%;\n    --secondary: 210 40% 96.1%;\n    --secondary-foreground: 222.2 47.4% 11.2%;\n    --muted: 210 40% 96.1%;\n    --muted-foreground: 215.4 16.3% 46.9%;\n    --accent: 210 40% 96.1%;\n    --accent-foreground: 222.2 47.4% 11.2%;\n    --destructive: 0 84.2% 60.2%;\n    --destructive-foreground: 210 40% 98%;\n    --success: 142.1 76.2% 36.3%;\n    --success-foreground: 355.7 100% 97.3%;\n    --border: 214.3 31.8% 91.4%;\n    --input: 214.3 31.8% 91.4%;\n    --ring: 222.2 84% 4.9%;\n    --chart-1: 12 76% 61%;\n    --chart-2: 173 58% 39%;\n    --chart-3: 197 37% 24%;\n    --chart-4: 43 74% 66%;\n    --chart-5: 27 87% 67%;\n    --radius: 0.5rem;\n  }\n\n  .dark {\n    --background: 222.2 84% 4.9%;\n    --foreground: 210 40% 98%;\n    --card: 222.2 84% 4.9%;\n    --card-foreground: 210 40% 98%;\n    --popover: 222.2 84% 4.9%;\n    --popover-foreground: 210 40% 98%;\n    --primary: 210 40% 98%;\n    --primary-foreground: 222.2 47.4% 11.2%;\n    --secondary: 217.2 32.6% 17.5%;\n    --secondary-foreground: 210 40% 98%;\n    --muted: 217.2 32.6% 17.5%;\n    --muted-foreground: 215 20.2% 65.1%;\n    --accent: 217.2 32.6% 17.5%;\n    --accent-foreground: 210 40% 98%;\n    --destructive: 0 62.8% 30.6%;\n    --destructive-foreground: 210 40% 98%;\n    --success: 142.1 70.6% 45.3%;\n    --success-foreground: 144.9 80.4% 10%;\n    --border: 217.2 32.6% 17.5%;\n    --input: 217.2 32.6% 17.5%;\n    --ring: 212.7 26.8% 83.9%;\n    --chart-1: 220 70% 50%;\n    --chart-2: 160 60% 45%;\n    --chart-3: 30 80% 55%;\n    --chart-4: 280 65% 60%;\n    --chart-5: 340 75% 55%;\n  }\n}\n\n@layer base {\n  * {\n    @apply border-border;\n  }\n\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\n/* Smooth theme transitions */\nhtml {\n  transition: color-scheme 0.3s ease;\n}\n\nhtml[data-theme=\"dark\"] {\n  color-scheme: dark;\n}\n\nhtml[data-theme=\"light\"] {\n  color-scheme: light;\n}\n\n/* Smooth transitions for theme-aware elements */\n* {\n  transition-property: background-color, border-color, color, fill, stroke, box-shadow;\n  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n  transition-duration: 150ms;\n}\n\n/* Disable transitions for elements that shouldn't animate */\n.no-transition,\n.no-transition * {\n  transition: none !important;\n}\n\n/* Improved focus styles for dark mode */\n.dark *:focus-visible {\n  outline-color: hsl(var(--ring));\n}\n\n/* Theme-aware scrollbar (Webkit browsers) */\n::-webkit-scrollbar {\n  width: 8px;\n  height: 8px;\n}\n\n::-webkit-scrollbar-track {\n  background: hsl(var(--background));\n}\n\n::-webkit-scrollbar-thumb {\n  background: hsl(var(--muted));\n  border-radius: 4px;\n}\n\n::-webkit-scrollbar-thumb:hover {\n  background: hsl(var(--muted-foreground) / 0.5);\n}"
  },
  {
    "path": "app/layout.tsx",
    "content": "// app/layout.tsx\nimport type {Metadata} from 'next'\nimport {Inter} from 'next/font/google'\nimport {AuthProvider} from '@/context/auth'\nimport './globals.css'\nimport React from \"react\";\nimport {ThemeProvider} from \"@/components/theme-provider\";\nimport {Toaster} from \"@/components/ui/toaster\";\nimport {Providers} from \"@/app/dashboard/providers\";\n\nconst inter = Inter({subsets: ['latin']})\n\nexport const metadata: Metadata = {\n    title: 'Changerawr',\n    description: 'Changelog management system',\n}\n\nexport default function RootLayout({\n                                       children,\n                                   }: {\n    children: React.ReactNode\n}) {\n    return (\n        <html lang=\"en\" suppressHydrationWarning>\n        <body className={inter.className}>\n        <AuthProvider>\n            <ThemeProvider>\n                <Providers>\n                    {children}\n                    <Toaster/>\n                </Providers>\n            </ThemeProvider>\n        </AuthProvider>\n        </body>\n        </html>\n    )\n}"
  },
  {
    "path": "app/page.tsx",
    "content": "'use client'\n\nimport { useState, useEffect, useRef } from 'react'\nimport { useAuth } from '@/context/auth'\nimport { useRouter } from 'next/navigation'\nimport { motion, AnimatePresence } from 'framer-motion'\nimport { Button } from '@/components/ui/button'\nimport {\n  AlertCircle,\n  ArrowRight,\n  Bug,\n  Sparkles,\n  Zap,\n  Hammer,\n  Trophy,\n  Clock,\n  ChevronUp,\n  Flame,\n  Star,\n  Shield,\n  Rocket,\n  Calendar,\n  TimerReset,\n  Hourglass\n} from 'lucide-react'\nimport Link from 'next/link'\nimport { toast } from '@/hooks/use-toast'\nimport { Progress } from '@/components/ui/progress'\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { Badge } from \"@/components/ui/badge\"\n\ntype ChangelogType = \"feature\" | \"bugfix\" | \"improvement\" | \"breaking\" | \"security\" | \"performance\" | \"hotfix\" | \"deprecation\";\n\ntype ChangelogEntry = {\n  text: string;\n  type: ChangelogType;\n  difficulty: number;\n  tricky?: boolean;\n  critical?: boolean; // Added critical flag for time-sensitive entries\n  points?: number; // Custom point value for special entries\n};\n\n// Changelog entry types (expanded)\nconst CHANGE_TYPES = [\n  { type: 'feature', icon: Sparkles, color: 'text-green-500', label: 'Feature' },\n  { type: 'bugfix', icon: Bug, color: 'text-red-500', label: 'Bug Fix' },\n  { type: 'improvement', icon: Zap, color: 'text-blue-500', label: 'Improvement' },\n  { type: 'breaking', icon: Hammer, color: 'text-orange-500', label: 'Breaking Change' },\n  { type: 'security', icon: Shield, color: 'text-purple-500', label: 'Security' },\n  { type: 'performance', icon: Rocket, color: 'text-yellow-500', label: 'Performance' },\n  { type: 'hotfix', icon: Flame, color: 'text-rose-500', label: 'Hotfix' }, // New entry type\n  { type: 'deprecation', icon: Hourglass, color: 'text-slate-500', label: 'Deprecation' }, // New entry type\n]\n\n// Expanded changelog entries for the game with different difficulty levels\nconst CHANGELOG_ENTRIES: ChangelogEntry[] = [\n  // Easy\n  { text: \"Added dark mode support\", type: \"feature\", difficulty: 1 },\n  { text: \"Fixed login screen crash\", type: \"bugfix\", difficulty: 1 },\n  { text: \"Enhanced loading performance\", type: \"improvement\", difficulty: 1 },\n  { text: \"Removed deprecated API endpoint\", type: \"breaking\", difficulty: 1 },\n  { text: \"Patched XSS vulnerability\", type: \"security\", difficulty: 1 },\n  { text: \"Optimized image loading\", type: \"performance\", difficulty: 1 },\n  { text: \"Emergency fix for payment processing bug\", type: \"hotfix\", difficulty: 1 },\n  { text: \"Legacy authentication method marked for removal\", type: \"deprecation\", difficulty: 1 },\n\n  // Medium\n  { text: \"Introduced experimental voice commands\", type: \"feature\", difficulty: 2 },\n  { text: \"Resolved intermittent data synchronization issues\", type: \"bugfix\", difficulty: 2 },\n  { text: \"Refined mobile responsiveness\", type: \"improvement\", difficulty: 2 },\n  { text: \"Modified authentication flow requiring re-login\", type: \"breaking\", difficulty: 2 },\n  { text: \"Enhanced encryption for sensitive data fields\", type: \"security\", difficulty: 2 },\n  { text: \"Reduced database query execution time by 40%\", type: \"performance\", difficulty: 2 },\n  { text: \"Critical patch for data loss during export\", type: \"hotfix\", difficulty: 2, critical: true, points: 250 },\n  { text: \"Legacy theme engine will be removed next month\", type: \"deprecation\", difficulty: 2 },\n\n  // Hard\n  { text: \"Implemented AI-assisted content suggestions in editor\", type: \"feature\", difficulty: 3 },\n  { text: \"Fixed edge case causing incorrect date calculations near timezone boundaries\", type: \"bugfix\", difficulty: 3 },\n  { text: \"Revamped information architecture for clearer navigation hierarchies\", type: \"improvement\", difficulty: 3 },\n  { text: \"Changed data export format from XML to JSON with different schema\", type: \"breaking\", difficulty: 3 },\n  { text: \"Implemented OWASP recommended protection against CSRF attacks\", type: \"security\", difficulty: 3 },\n  { text: \"Enabled parallel processing for batch operations reducing wait time by 75%\", type: \"performance\", difficulty: 3 },\n  { text: \"Urgent patch for zero-day vulnerability in authentication flow\", type: \"hotfix\", difficulty: 3, critical: true, points: 400 },\n  { text: \"Legacy plugin architecture marked for sunset in 90 days\", type: \"deprecation\", difficulty: 3 },\n\n  // Tricky ones\n  { text: \"Added view customization panel\", type: \"feature\", difficulty: 2, tricky: true },  // Sounds like improvement\n  { text: \"Updated error handling in network layer\", type: \"improvement\", difficulty: 2, tricky: true },  // Could be bugfix\n  { text: \"Refactored authentication module for security enhancements\", type: \"security\", difficulty: 3, tricky: true },  // Could be improvement\n  { text: \"Optimized rendering engine\", type: \"improvement\", difficulty: 2, tricky: true },  // Could be performance\n  { text: \"Fixed critical memory usage in background tasks\", type: \"performance\", difficulty: 3, tricky: true },  // Could be bugfix\n  { text: \"Upgraded to TLS 1.3 with legacy fallback option\", type: \"security\", difficulty: 3, tricky: true },  // Could be improvement or breaking\n\n  // New super tricky ones\n  { text: \"Increased logging verbosity for failed API requests\", type: \"improvement\", difficulty: 3, tricky: true }, // Could be bugfix or feature\n  { text: \"Server-side implementation of form validation rules\", type: \"security\", difficulty: 3, tricky: true }, // Could be feature or improvement\n  { text: \"Emergency release to resolve payment gateway timeout\", type: \"hotfix\", difficulty: 3, tricky: true, critical: true, points: 350 }, // Sounds like bugfix or performance\n  { text: \"Legacy configuration format will no longer be supported\", type: \"deprecation\", difficulty: 3, tricky: true }, // Could be breaking\n\n  // Special \"Daily Challenge\" entries\n  { text: \"Migrated from React class components to functional hooks\", type: \"improvement\", difficulty: 3, points: 300 },\n  { text: \"Added accessibility enhancements for screen readers\", type: \"feature\", difficulty: 2, points: 250 },\n  { text: \"Fixed critical race condition in concurrent data updates\", type: \"bugfix\", difficulty: 3, critical: true, points: 350 },\n  { text: \"Completely redesigned UI with new component library\", type: \"breaking\", difficulty: 3, points: 300 },\n  { text: \"Implemented Zero Trust security architecture\", type: \"security\", difficulty: 3, points: 300 },\n  { text: \"Redesigned database schema for 10x query performance\", type: \"performance\", difficulty: 3, points: 300 },\n]\n\n// Added daily challenge entries\nconst DAILY_CHALLENGE_ENTRIES: ChangelogEntry[] = [\n  { text: \"Migrated to React Server Components\", type: \"breaking\", difficulty: 3, points: 400 },\n  { text: \"Added end-to-end encryption for all communication\", type: \"security\", difficulty: 3, points: 400 },\n  { text: \"Implemented streaming updates to reduce initial load time by 90%\", type: \"performance\", difficulty: 3, points: 400 },\n  { text: \"Fixed CVE-2025-4587 affecting user authentication\", type: \"hotfix\", difficulty: 3, critical: true, points: 500 },\n  { text: \"Added AI-powered code completion\", type: \"feature\", difficulty: 3, points: 400 },\n  { text: \"Final notice: Legacy API v1 endpoints shutting down next week\", type: \"deprecation\", difficulty: 3, points: 400 },\n]\n\nexport default function Home() {\n  const { user, isLoading } = useAuth()\n  const router = useRouter()\n  const [gameActive, setGameActive] = useState(false)\n  const [gameStarted, setGameStarted] = useState(false)\n  const [gameOver, setGameOver] = useState(false)\n  const [timeLeft, setTimeLeft] = useState(10)\n  const [score, setScore] = useState(0)\n  const [highScore, setHighScore] = useState(0)\n  const [lives, setLives] = useState(3)\n  const [combo, setCombo] = useState(0)\n  const [level, setLevel] = useState(1)\n  const [currentEntry, setCurrentEntry] = useState<ChangelogEntry | null>(null);\n  const [powerUp, setPowerUp] = useState<string | null>(null)\n  const [powerUpTimeLeft, setPowerUpTimeLeft] = useState(0)\n  const [perfectRound, setPerfectRound] = useState(false)\n  const [unlockedAchievements, setUnlockedAchievements] = useState<string[]>([])\n  const [showFinalScore, setShowFinalScore] = useState(false)\n\n  // New states\n  const [gameMode, setGameMode] = useState<\"classic\" | \"daily\" | \"blitz\">(\"classic\")\n  const [streakBreakerActive, setStreakBreakerActive] = useState(false)\n  const [criticalEntryActive, setCriticalEntryActive] = useState(false)\n  const [dailyChallengeAvailable, setDailyChallengeAvailable] = useState(false)\n  const [dailyChallengeCompleted, setDailyChallengeCompleted] = useState(false)\n  const [showGameModeSelect, setShowGameModeSelect] = useState(false)\n  const [criticalTimeLeft, setCriticalTimeLeft] = useState(0)\n  const [criticalTimerRef, setCriticalTimerRef] = useState<NodeJS.Timeout | null>(null)\n  const [longestStreak, setLongestStreak] = useState(0)\n\n  // Particle effect state\n  const [particles, setParticles] = useState<Array<{\n    id: number;\n    x: number;\n    y: number;\n    vx: number;\n    vy: number;\n    size: number;\n    color: string;\n    rotation: number;\n    opacity: number;\n  }>>([])\n\n  // Trigger particle effects\n  const triggerParticles = (count: number, type: string, isLevelUp: boolean = false) => {\n    // Get type-specific colors based on the entry type\n    const getTypeColor = (type: string) => {\n      const typeColors: Record<string, string[]> = {\n        'feature': ['#34D399', '#10B981', '#059669'],\n        'bugfix': ['#F87171', '#EF4444', '#DC2626'],\n        'improvement': ['#60A5FA', '#3B82F6', '#2563EB'],\n        'breaking': ['#F59E0B', '#D97706', '#B45309'],\n        'security': ['#A78BFA', '#8B5CF6', '#7C3AED'],\n        'performance': ['#FBBF24', '#F59E0B', '#D97706'],\n        'hotfix': ['#FB7185', '#E11D48', '#BE123C'],\n        'deprecation': ['#94A3B8', '#64748B', '#475569']\n      };\n\n      return typeColors[type] || ['#9CA3AF', '#6B7280', '#4B5563'];\n    };\n\n    const typeColors = getTypeColor(type);\n    const levelUpColors = ['#FBBF24', '#F59E0B', '#FBBF24', '#F87171', '#34D399', '#60A5FA'];\n\n    // Generate random particles with physics\n    const newParticles = Array.from({ length: count }).map((_, i) => {\n      // Randomize velocity for different spread patterns\n      const angle = Math.random() * Math.PI * 2;\n      const speed = Math.random() * 8 + (isLevelUp ? 5 : 2);\n      const size = Math.random() * (isLevelUp ? 20 : 12) + 6;\n\n      return {\n        id: Date.now() + i,\n        x: 50, // center X position (%)\n        y: 50, // center Y position (%)\n        vx: Math.cos(angle) * speed,\n        vy: Math.sin(angle) * speed,\n        size,\n        color: isLevelUp\n            ? levelUpColors[Math.floor(Math.random() * levelUpColors.length)]\n            : typeColors[Math.floor(Math.random() * typeColors.length)],\n        rotation: Math.random() * 360,\n        opacity: 1\n      };\n    });\n\n    setParticles(prev => [...prev, ...newParticles]);\n\n    // Clean up particles after animation completes\n    setTimeout(() => {\n      setParticles(prev => prev.filter(p => !newParticles.some(np => np.id === p.id)));\n    }, 2000);\n  };\n\n  // Secret unlock pattern tracking\n  const [clickSequence, setClickSequence] = useState<number[]>([])\n  const [lastClickTime, setLastClickTime] = useState(0)\n  const [shiftPressed, setShiftPressed] = useState(false)\n\n  const timerRef = useRef<NodeJS.Timeout | null>(null)\n  const powerUpTimerRef = useRef<NodeJS.Timeout | null>(null)\n\n  // Secret unlock pattern - extremely specific\n  // Users must click on specific parts of error message while holding shift key\n  // in a precise sequence: center-top-right-left-center-bottom\n  const handleSecretClick = (position: number) => {\n    if (!shiftPressed) {\n      // Must hold shift key for pattern to work\n      setClickSequence([])\n      return\n    }\n\n    const now = Date.now()\n    const elapsed = now - lastClickTime\n    setLastClickTime(now)\n\n    // Reset pattern if too much time elapsed (2 seconds between clicks)\n    if (elapsed > 2000 && clickSequence.length > 0) {\n      setClickSequence([])\n      return\n    }\n\n    // Add to sequence\n    const newSequence = [...clickSequence, position]\n    setClickSequence(newSequence)\n\n    // Check if sequence matches the secret pattern: 0-1-2-3-0-4\n    const secretPattern = [0, 1, 2, 3, 0, 4]\n    const isMatch = newSequence.length === secretPattern.length &&\n        newSequence.every((val, idx) => val === secretPattern[idx])\n\n    if (isMatch) {\n      unlockGame()\n      setClickSequence([])\n    } else if (newSequence.length >= secretPattern.length) {\n      // Reset if sequence is wrong but same length\n      setClickSequence([])\n    }\n  }\n\n  // Track shift key state\n  useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'Shift') {\n        setShiftPressed(true)\n      }\n    }\n\n    const handleKeyUp = (e: KeyboardEvent) => {\n      if (e.key === 'Shift') {\n        setShiftPressed(false)\n        setClickSequence([]) // Reset sequence when shift is released\n      }\n    }\n\n    window.addEventListener('keydown', handleKeyDown)\n    window.addEventListener('keyup', handleKeyUp)\n\n    return () => {\n      window.removeEventListener('keydown', handleKeyDown)\n      window.removeEventListener('keyup', handleKeyUp)\n    }\n  }, [])\n\n  const unlockGame = () => {\n    setGameActive(true)\n    toast({\n      title: \"Changelog Hero Unlocked!\",\n      description: \"You found the secret game! How did you do that?\",\n    })\n  }\n\n  // Check if daily challenge is available - based on the current day\n  useEffect(() => {\n    const checkDailyChallenge = () => {\n      const today = new Date().toDateString();\n      const lastPlayed = localStorage.getItem('changelogHeroDailyDate');\n\n      setDailyChallengeAvailable(lastPlayed !== today);\n      setDailyChallengeCompleted(lastPlayed === today);\n    };\n\n    checkDailyChallenge();\n\n    // Check daily challenge availability every minute\n    const interval = setInterval(checkDailyChallenge, 60000);\n    return () => clearInterval(interval);\n  }, []);\n\n  // Load high score from localStorage\n  useEffect(() => {\n    const savedHighScore = localStorage.getItem('changelogHeroHighScore')\n    if (savedHighScore) {\n      setHighScore(parseInt(savedHighScore))\n    }\n\n    const savedAchievements = localStorage.getItem('changelogHeroAchievements')\n    if (savedAchievements) {\n      setUnlockedAchievements(JSON.parse(savedAchievements))\n    }\n\n    const savedLongestStreak = localStorage.getItem('changelogHeroLongestStreak')\n    if (savedLongestStreak) {\n      setLongestStreak(parseInt(savedLongestStreak))\n    }\n\n    return () => {\n      cleanup()\n    }\n  }, [])\n\n  const cleanup = () => {\n    if (timerRef.current) clearInterval(timerRef.current)\n    if (powerUpTimerRef.current) clearInterval(powerUpTimerRef.current)\n    if (criticalTimerRef) clearInterval(criticalTimerRef)\n  }\n\n  // Main game logic\n  const startGame = (mode: \"classic\" | \"daily\" | \"blitz\" = \"classic\") => {\n    setGameMode(mode);\n    setGameStarted(true);\n    setGameOver(false);\n    setScore(0);\n    setLives(mode === \"blitz\" ? 1 : 3);\n    setTimeLeft(mode === \"blitz\" ? 6 : 10);\n    setCombo(0);\n    setLevel(1);\n    setPowerUp(null);\n    setPowerUpTimeLeft(0);\n    setPerfectRound(true);\n    setShowFinalScore(false);\n    setStreakBreakerActive(false);\n    setCriticalEntryActive(false);\n\n    if (mode === \"daily\") {\n      // Set a timestamp to prevent replay of daily challenge\n      localStorage.setItem('changelogHeroDailyDate', new Date().toDateString());\n      setDailyChallengeAvailable(false);\n      setDailyChallengeCompleted(true);\n    }\n\n    pickRandomEntry();\n\n    if (timerRef.current) {\n      clearInterval(timerRef.current);\n    }\n\n    timerRef.current = setInterval(() => {\n      setTimeLeft(prev => {\n        // In blitz mode, time doesn't reset on correct answers\n        // but you get +1 second for each correct answer\n        if (prev <= 1) {\n          handleWrongAnswer();\n          return getTimerDuration();\n        }\n        return prev - 1;\n      })\n    }, 1000);\n  }\n\n  const endGame = () => {\n    cleanup();\n\n    if (score > highScore) {\n      setHighScore(score);\n      localStorage.setItem('changelogHeroHighScore', score.toString());\n    }\n\n    if (combo > longestStreak) {\n      setLongestStreak(combo);\n      localStorage.setItem('changelogHeroLongestStreak', combo.toString());\n    }\n\n    setGameOver(true);\n    setShowFinalScore(true);\n    checkAchievements();\n  }\n\n  const checkAchievements = () => {\n    const newAchievements = [...unlockedAchievements];\n    let changed = false;\n\n    // Check different conditions for achievements\n    if (!newAchievements.includes(\"Changelog Novice\") && score >= 500) {\n      newAchievements.push(\"Changelog Novice\");\n      changed = true;\n    }\n\n    if (!newAchievements.includes(\"Perfect Round\") && perfectRound) {\n      newAchievements.push(\"Perfect Round\");\n      changed = true;\n    }\n\n    if (!newAchievements.includes(\"Speed Demon\") && combo >= 5) {\n      newAchievements.push(\"Speed Demon\");\n      changed = true;\n    }\n\n    if (!newAchievements.includes(\"Changelog Master\") && score >= 2000) {\n      newAchievements.push(\"Changelog Master\");\n      changed = true;\n    }\n\n    // New achievements\n    if (!newAchievements.includes(\"Hotfix Hero\") && gameMode === \"blitz\" && score >= 1000) {\n      newAchievements.push(\"Hotfix Hero\");\n      changed = true;\n    }\n\n    if (!newAchievements.includes(\"Daily Devotion\") && gameMode === \"daily\" && score >= 1500) {\n      newAchievements.push(\"Daily Devotion\");\n      changed = true;\n    }\n\n    if (!newAchievements.includes(\"Streak Master\") && combo >= 10) {\n      newAchievements.push(\"Streak Master\");\n      changed = true;\n    }\n\n    if (!newAchievements.includes(\"Critical Response\") &&\n        CHANGELOG_ENTRIES.some(entry => entry.critical && entry.type === \"hotfix\")) {\n      newAchievements.push(\"Critical Response\");\n      changed = true;\n    }\n\n    // Save achievements\n    if (changed) {\n      setUnlockedAchievements(newAchievements);\n      localStorage.setItem('changelogHeroAchievements', JSON.stringify(newAchievements));\n    }\n  }\n\n  // Get timer duration based on current level and game mode\n  const getTimerDuration = () => {\n    if (gameMode === \"blitz\") {\n      return 6; // Fixed time for blitz mode\n    }\n\n    const baseDuration = 10;\n    return Math.max(4, baseDuration - (level - 1));\n  }\n\n  // Pick a random changelog entry based on current level\n  const pickRandomEntry = () => {\n    let eligibleEntries;\n\n    // For daily challenge, use the daily entries\n    if (gameMode === \"daily\") {\n      eligibleEntries = DAILY_CHALLENGE_ENTRIES;\n    } else {\n      // For classic and blitz modes, filter entries by difficulty matching current level\n      eligibleEntries = CHANGELOG_ENTRIES.filter(entry => entry.difficulty <= level);\n\n      // In blitz mode, increase chance of critical and tricky entries\n      if (gameMode === \"blitz\") {\n        // Add 50% more weight to critical entries in blitz mode\n        eligibleEntries = [\n          ...eligibleEntries,\n          ...eligibleEntries.filter(entry => entry.critical)\n        ];\n      }\n    }\n\n    // Streak breaker mechanic - with some probability, introduce a really tricky entry\n    // Increases with combo length\n    const streakBreakerChance = Math.min(0.05 + (combo * 0.005), 0.25);\n    if (combo >= 5 && Math.random() < streakBreakerChance && !streakBreakerActive) {\n      // Filter for tricky entries\n      const trickyEntries = CHANGELOG_ENTRIES.filter(entry => entry.tricky && entry.difficulty >= level);\n      if (trickyEntries.length > 0) {\n        const randomTrickyIndex = Math.floor(Math.random() * trickyEntries.length);\n        setCurrentEntry(trickyEntries[randomTrickyIndex]);\n        setStreakBreakerActive(true);\n\n        toast({\n          title: \"Streak Breaker!\",\n          description: \"This one's extra tricky - watch out!\",\n          variant: \"destructive\",\n        });\n\n        setTimeLeft(getTimerDuration());\n        return;\n      }\n    } else {\n      setStreakBreakerActive(false);\n    }\n\n    // Add chance for power-up if no current power-up\n    if (Math.random() < 0.15 && !powerUp) {\n      const availablePowerUps = [\"doublePoints\", \"extraLife\", \"slowTime\", \"skipEntry\", \"levelBoost\"];\n      const selectedPowerUp = availablePowerUps[Math.floor(Math.random() * availablePowerUps.length)];\n      setPowerUp(selectedPowerUp);\n      setPowerUpTimeLeft(10);\n\n      if (powerUpTimerRef.current) {\n        clearInterval(powerUpTimerRef.current);\n      }\n\n      powerUpTimerRef.current = setInterval(() => {\n        setPowerUpTimeLeft(prev => {\n          if (prev <= 1) {\n            setPowerUp(null);\n            if (powerUpTimerRef.current) clearInterval(powerUpTimerRef.current);\n            return 0;\n          }\n          return prev - 1;\n        });\n      }, 1000);\n    }\n\n    // Critical entry chance increases with level\n    const criticalChance = 0.1 * level;\n    if (Math.random() < criticalChance && !criticalEntryActive) {\n      const criticalEntries = eligibleEntries.filter(entry => entry.critical);\n      if (criticalEntries.length > 0) {\n        const randomCriticalIndex = Math.floor(Math.random() * criticalEntries.length);\n        const criticalEntry = criticalEntries[randomCriticalIndex];\n        setCurrentEntry(criticalEntry);\n        setCriticalEntryActive(true);\n\n        // Critical entries have their own timer\n        setCriticalTimeLeft(5);\n\n        if (criticalTimerRef) {\n          clearInterval(criticalTimerRef);\n        }\n\n        const timerInterval = setInterval(() => {\n          setCriticalTimeLeft(prev => {\n            if (prev <= 1) {\n              // Time's up for critical entry - penalty\n              handleWrongAnswer();\n              setCriticalEntryActive(false);\n              if (criticalTimerRef) clearInterval(criticalTimerRef);\n              pickRandomEntry();\n              return 0;\n            }\n            return prev - 1;\n          });\n        }, 1000);\n\n        setCriticalTimerRef(timerInterval);\n\n        toast({\n          title: \"Critical Entry!\",\n          description: \"Respond quickly for bonus points!\",\n          variant: \"destructive\",\n        });\n\n        return;\n      }\n    }\n\n    // Standard random entry selection\n    const randomIndex = Math.floor(Math.random() * eligibleEntries.length);\n    setCurrentEntry(eligibleEntries[randomIndex]);\n    setCriticalEntryActive(false);\n    setTimeLeft(getTimerDuration());\n  }\n\n  // Skip the current entry - used by skipEntry power-up\n  const skipCurrentEntry = () => {\n    toast({\n      title: \"Entry Skipped!\",\n      description: \"That one looked tough!\",\n    });\n\n    // Don't reset combo when skipping\n    pickRandomEntry();\n    setPowerUp(null);\n    if (powerUpTimerRef.current) clearInterval(powerUpTimerRef.current);\n  }\n\n  // Handle answer\n  const handleAnswer = (selectedType: string) => {\n    if (!currentEntry) return;\n\n    // Base points calculation\n    const basePoints = currentEntry.points || 100;\n    const difficultyMultiplier = currentEntry.difficulty * 0.5 + 1;\n    const timeBonus = Math.round(timeLeft * 10);\n    const comboMultiplier = combo > 0 ? (1 + combo * 0.1) : 1;\n\n    // Critical bonus - extra points for fast answers to critical entries\n    const criticalBonus = (criticalEntryActive && criticalTimeLeft > 2) ? 1.5 : 1;\n\n    if (selectedType === currentEntry.type) {\n      // Correct answer\n      let pointsEarned = Math.round(basePoints * difficultyMultiplier + timeBonus);\n      pointsEarned = Math.round(pointsEarned * comboMultiplier * criticalBonus);\n\n      // Apply double points power-up\n      if (powerUp === \"doublePoints\") {\n        pointsEarned *= 2;\n      }\n\n      setScore(prev => prev + pointsEarned);\n      setCombo(prev => prev + 1);\n\n      // Update longest streak if combo is greater\n      if (combo + 1 > longestStreak) {\n        setLongestStreak(combo + 1);\n      }\n\n      // In blitz mode, successful answers add time\n      if (gameMode === \"blitz\") {\n        setTimeLeft(prev => Math.min(prev + 1, 10)); // Add 1 second, max at 10\n      }\n\n      // Level up after every 5 correct answers\n      if (combo > 0 && combo % 5 === 0 && level < 3) {\n        setLevel(prev => prev + 1);\n        toast({\n          title: `Level ${level + 1}!`,\n          description: \"Harder challenges, faster pace, more points!\",\n        });\n\n        // Special celebration for level up\n        triggerParticles(40, selectedType, true);\n      } else {\n        // Regular correct answer celebration\n        triggerParticles(20, selectedType);\n      }\n\n      // Extra level boost power-up effect\n      if (powerUp === \"levelBoost\" && level < 3) {\n        setLevel(prev => prev + 1);\n        toast({\n          title: \"Level Boost!\",\n          description: \"Jumped to next level - higher points!\",\n        });\n        setPowerUp(null);\n        if (powerUpTimerRef.current) clearInterval(powerUpTimerRef.current);\n      }\n\n      // Show toast with points breakdown\n      let toastDescription = `+${pointsEarned} points`;\n      if (comboMultiplier > 1) toastDescription += ` (${combo}x combo!)`;\n      if (powerUp === \"doublePoints\") toastDescription += \" (DOUBLE POINTS!)\";\n      if (criticalBonus > 1) toastDescription += \" (CRITICAL BONUS!)\";\n\n      toast({\n        title: \"Correct!\",\n        description: toastDescription,\n      });\n    } else {\n      // Wrong answer\n      handleWrongAnswer();\n      setPerfectRound(false);\n    }\n\n    // Reset critical timer if active\n    if (criticalEntryActive && criticalTimerRef) {\n      clearInterval(criticalTimerRef);\n      setCriticalEntryActive(false);\n    }\n\n    // Reset timer and pick new entry\n    pickRandomEntry();\n  }\n\n  const handleWrongAnswer = () => {\n    setCombo(0); // Reset combo\n\n    // Don't lose a life if extraLife power-up is active\n    if (powerUp !== \"extraLife\") {\n      setLives(prev => prev - 1);\n    } else {\n      toast({\n        title: \"Extra Life Used!\",\n        description: \"Your power-up saved you!\",\n      });\n      setPowerUp(null);\n      if (powerUpTimerRef.current) clearInterval(powerUpTimerRef.current);\n    }\n\n    toast({\n      title: \"Wrong!\",\n      description: `That was a ${currentEntry?.type || \"unknown type\"}`,\n      variant: \"destructive\",\n    });\n  }\n\n  // Effect to check game over\n  useEffect(() => {\n    if (lives <= 0 && gameStarted) {\n      endGame();\n    }\n  }, [lives, gameStarted]);\n\n  // If user isn't logged in, redirect to login\n  useEffect(() => {\n    if (!isLoading && !user) {\n      router.push('/login');\n    }\n  }, [user, isLoading, router]);\n\n  if (isLoading || !user) {\n    return (\n        <div className=\"flex min-h-screen items-center justify-center\">\n          <div className=\"animate-pulse text-lg\">Loading...</div>\n        </div>\n    );\n  }\n\n  // Power-up descriptions\n  const powerUpDescriptions = {\n    \"doublePoints\": \"Double Points: All points are doubled!\",\n    \"extraLife\": \"Extra Life: Your next mistake won't cost a life!\",\n    \"slowTime\": \"Slow Time: Time moves at half speed!\",\n    \"skipEntry\": \"Skip Entry: Skip this challenging entry!\",\n    \"levelBoost\": \"Level Boost: Jump to the next level for higher points!\"\n  };\n\n  return (\n      <div className=\"relative flex min-h-screen flex-col items-center justify-center bg-background px-4 py-12\">\n        {/* No hints shown to user */}\n\n        {/* Particle effect layer */}\n        <div className=\"absolute inset-0 overflow-hidden pointer-events-none\">\n          <AnimatePresence>\n            {particles.map(particle => (\n                <motion.div\n                    key={particle.id}\n                    className=\"absolute z-50\"\n                    style={{\n                      left: `${particle.x}%`,\n                      top: `${particle.y}%`,\n                      width: particle.size,\n                      height: particle.size,\n                      backgroundColor: particle.color,\n                      borderRadius: Math.random() > 0.5 ? '50%' : '0%',\n                      rotate: `${particle.rotation}deg`,\n                    }}\n                    animate={{\n                      x: `calc(${particle.vx * 10}vh)`,\n                      y: `calc(${particle.vy * 10}vh)`,\n                      opacity: 0,\n                      scale: Math.random() * 0.5 + 0.5,\n                      rotate: `${particle.rotation + (Math.random() > 0.5 ? 180 : -180)}deg`\n                    }}\n                    initial={{ opacity: 1, scale: 1 }}\n                    transition={{ duration: 1.5, ease: \"easeOut\" }}\n                    exit={{ opacity: 0 }}\n                />\n            ))}\n          </AnimatePresence>\n        </div>\n\n        <AnimatePresence mode=\"wait\">\n          {!gameActive ? (\n              <motion.div\n                  className=\"text-center max-w-md relative\"\n                  initial={{ opacity: 0, y: 20 }}\n                  animate={{ opacity: 1, y: 0 }}\n                  exit={{ opacity: 0, y: -20 }}\n              >\n                {/* Center click target (0) */}\n                <div\n                    className=\"mx-auto mb-6 flex h-24 w-24 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/30 cursor-default relative\"\n                    onClick={() => handleSecretClick(0)} // Center\n                >\n                  <AlertCircle className=\"h-12 w-12 text-red-600 dark:text-red-400\" />\n\n                  {/* Top click target (1) - Hidden */}\n                  <div\n                      className=\"absolute top-0 left-1/2 transform -translate-x-1/2 w-10 h-5 cursor-default\"\n                      onClick={(e) => {\n                        e.stopPropagation();\n                        handleSecretClick(1); // Top\n                      }}\n                  />\n\n                  {/* Right click target (2) - Hidden */}\n                  <div\n                      className=\"absolute right-0 top-1/2 transform -translate-y-1/2 w-5 h-10 cursor-default\"\n                      onClick={(e) => {\n                        e.stopPropagation();\n                        handleSecretClick(2); // Right\n                      }}\n                  />\n\n                  {/* Left click target (3) - Hidden */}\n                  <div\n                      className=\"absolute left-0 top-1/2 transform -translate-y-1/2 w-5 h-10 cursor-default\"\n                      onClick={(e) => {\n                        e.stopPropagation();\n                        handleSecretClick(3); // Left\n                      }}\n                  />\n\n                  {/* Bottom click target (4) - Hidden */}\n                  <div\n                      className=\"absolute bottom-0 left-1/2 transform -translate-x-1/2 w-10 h-5 cursor-default\"\n                      onClick={(e) => {\n                        e.stopPropagation();\n                        handleSecretClick(4); // Bottom\n                      }}\n                  />\n                </div>\n\n                <h1 className=\"mb-4 text-2xl font-bold\">\n                  Oops! You&apos;re not supposed to be here\n                </h1>\n\n                <p className=\"mb-8 text-muted-foreground\">\n                  Looks like you&apos;ve stumbled onto a page that doesn&apos;t have much to see.\n                  Head back to the dashboard to manage your changelogs.\n                </p>\n\n                <Button asChild>\n                  <Link href=\"/dashboard\">\n                    Go to Dashboard\n                    <ArrowRight className=\"ml-2 h-4 w-4\" />\n                  </Link>\n                </Button>\n              </motion.div>\n          ) : (\n              // Game Interface\n              <motion.div\n                  className=\"max-w-2xl w-full\"\n                  initial={{ opacity: 0, scale: 0.9 }}\n                  animate={{ opacity: 1, scale: 1 }}\n                  exit={{ opacity: 0, scale: 0.9 }}\n              >\n                <div className=\"text-center mb-4\">\n                  <h1 className=\"text-3xl font-bold mb-1\">Changelog Hero</h1>\n                  <p className=\"text-muted-foreground\">Categorize changelog entries to score points!</p>\n                </div>\n\n                {!gameStarted || gameOver ? (\n                    // Start/Game Over Screen\n                    <motion.div\n                        className=\"bg-muted/40 border rounded-xl p-8 text-center\"\n                        initial={{ opacity: 0 }}\n                        animate={{ opacity: 1 }}\n                    >\n                      {gameOver ? (\n                          <AnimatePresence mode=\"wait\">\n                            {showFinalScore ? (\n                                <motion.div\n                                    key=\"finalScore\"\n                                    initial={{ opacity: 0 }}\n                                    animate={{ opacity: 1 }}\n                                    exit={{ opacity: 0 }}\n                                >\n                                  <Trophy className=\"h-16 w-16 text-primary mx-auto mb-4\" />\n                                  <h2 className=\"text-2xl font-bold mb-2\">Game Over!</h2>\n                                  <p className=\"text-xl mb-4\">Your score: <span className=\"font-bold\">{score}</span></p>\n                                  <p className=\"text-muted-foreground mb-2\">\n                                    {score > highScore ? \"New high score! 🎉\" : `High score: ${highScore}`}\n                                  </p>\n\n                                  {/* Game mode badge */}\n                                  <div className=\"mb-4\">\n                                    <Badge variant=\"outline\" className=\"text-primary\">\n                                      {gameMode === \"classic\" ? \"Classic Mode\" :\n                                          gameMode === \"daily\" ? \"Daily Challenge\" : \"Blitz Mode\"}\n                                    </Badge>\n                                  </div>\n\n                                  {/* Stats breakdown */}\n                                  <div className=\"mb-6 bg-background/60 rounded-lg p-4\">\n                                    <h3 className=\"font-semibold mb-2\">Performance</h3>\n                                    <div className=\"grid grid-cols-2 gap-2 text-sm\">\n                                      <div className=\"text-left text-muted-foreground\">Highest Level:</div>\n                                      <div className=\"text-right\">{level}</div>\n                                      <div className=\"text-left text-muted-foreground\">Highest Combo:</div>\n                                      <div className=\"text-right\">{combo}x</div>\n                                      <div className=\"text-left text-muted-foreground\">Perfect Round:</div>\n                                      <div className=\"text-right\">{perfectRound ? \"Yes ✨\" : \"No\"}</div>\n                                      <div className=\"text-left text-muted-foreground\">Longest Streak:</div>\n                                      <div className=\"text-right\">{longestStreak}x</div>\n                                    </div>\n                                  </div>\n\n                                  {/* Achievements */}\n                                  {unlockedAchievements.length > 0 && (\n                                      <div className=\"mb-6\">\n                                        <h3 className=\"font-semibold mb-2\">Achievements Unlocked</h3>\n                                        <div className=\"flex flex-wrap justify-center gap-2\">\n                                          {unlockedAchievements.map(achievement => (\n                                              <div key={achievement} className=\"text-xs bg-primary/10 text-primary px-2 py-1 rounded-full\">\n                                                {achievement}\n                                              </div>\n                                          ))}\n                                        </div>\n                                      </div>\n                                  )}\n                                </motion.div>\n                            ) : null}\n                          </AnimatePresence>\n                      ) : (\n                          showGameModeSelect ? (\n                              <motion.div\n                                  key=\"gameModeSelect\"\n                                  initial={{ opacity: 0 }}\n                                  animate={{ opacity: 1 }}\n                                  exit={{ opacity: 0 }}\n                              >\n                                <h2 className=\"text-2xl font-bold mb-4\">Select Game Mode</h2>\n\n                                <div className=\"grid gap-4 mb-6\">\n                                  <motion.div\n                                      className=\"bg-card border rounded-lg p-4 text-left cursor-pointer hover:border-primary transition-colors\"\n                                      whileHover={{ scale: 1.02 }}\n                                      onClick={() => startGame(\"classic\")}\n                                  >\n                                    <div className=\"flex items-center\">\n                                      <div className=\"p-2 bg-primary/20 rounded-full mr-3\">\n                                        <Trophy className=\"h-5 w-5 text-primary\" />\n                                      </div>\n                                      <div>\n                                        <h3 className=\"font-semibold\">Classic Mode</h3>\n                                        <p className=\"text-sm text-muted-foreground\">Standard game with 3 lives and increasing difficulty</p>\n                                      </div>\n                                    </div>\n                                  </motion.div>\n\n                                  <motion.div\n                                      className={`bg-card border rounded-lg p-4 text-left transition-colors ${\n                                          dailyChallengeAvailable\n                                              ? \"cursor-pointer hover:border-primary\"\n                                              : \"opacity-70 cursor-not-allowed\"\n                                      }`}\n                                      whileHover={dailyChallengeAvailable ? { scale: 1.02 } : {}}\n                                      onClick={() => dailyChallengeAvailable && startGame(\"daily\")}\n                                  >\n                                    <div className=\"flex items-center\">\n                                      <div className=\"p-2 bg-blue-500/20 rounded-full mr-3\">\n                                        <Calendar className=\"h-5 w-5 text-blue-500\" />\n                                      </div>\n                                      <div>\n                                        <div className=\"flex items-center gap-2\">\n                                          <h3 className=\"font-semibold\">Daily Challenge</h3>\n                                          {dailyChallengeCompleted && (\n                                              <Badge variant=\"outline\" className=\"text-xs\">Completed</Badge>\n                                          )}\n                                        </div>\n                                        <p className=\"text-sm text-muted-foreground\">\n                                          {dailyChallengeAvailable\n                                              ? \"Special daily entries with higher rewards\"\n                                              : \"Come back tomorrow for a new challenge!\"}\n                                        </p>\n                                      </div>\n                                    </div>\n                                  </motion.div>\n\n                                  <motion.div\n                                      className=\"bg-card border rounded-lg p-4 text-left cursor-pointer hover:border-primary transition-colors\"\n                                      whileHover={{ scale: 1.02 }}\n                                      onClick={() => startGame(\"blitz\")}\n                                  >\n                                    <div className=\"flex items-center\">\n                                      <div className=\"p-2 bg-red-500/20 rounded-full mr-3\">\n                                        <Flame className=\"h-5 w-5 text-red-500\" />\n                                      </div>\n                                      <div>\n                                        <h3 className=\"font-semibold\">Blitz Mode</h3>\n                                        <p className=\"text-sm text-muted-foreground\">Fast-paced: 1 life, gain time with correct answers</p>\n                                      </div>\n                                    </div>\n                                  </motion.div>\n                                </div>\n\n                                <Button variant=\"outline\" size=\"sm\" onClick={() => setShowGameModeSelect(false)}>\n                                  Back\n                                </Button>\n                              </motion.div>\n                          ) : (\n                              <motion.div\n                                  key=\"welcomeScreen\"\n                                  initial={{ opacity: 0 }}\n                                  animate={{ opacity: 1 }}\n                                  exit={{ opacity: 0 }}\n                              >\n                                <div className=\"flex justify-center space-x-2 mb-6\">\n                                  {CHANGE_TYPES.map(type => (\n                                      <motion.div\n                                          key={type.type}\n                                          className={`${type.color} bg-muted/80 rounded-full p-3`}\n                                          whileHover={{ scale: 1.1 }}\n                                      >\n                                        <type.icon className=\"h-6 w-6\" />\n                                      </motion.div>\n                                  ))}\n                                </div>\n                                <h2 className=\"text-2xl font-bold mb-4\">Ready to play?</h2>\n                                <p className=\"text-muted-foreground mb-4\">\n                                  Categorize changelog entries by their type before time runs out!\n                                </p>\n                                <div className=\"mb-8 space-y-2 text-sm\">\n                                  <p>• Level up after 5 consecutive correct answers</p>\n                                  <p>• Higher levels have harder entries & faster pace</p>\n                                  <p>• Combo multiplier increases your score</p>\n                                  <p>• Watch for special power-ups to help you</p>\n                                  <p>• Respond quickly to critical entries for bonus points</p>\n                                </div>\n\n                                <div className=\"flex justify-center space-x-4\">\n                                  <Button onClick={() => setShowGameModeSelect(true)}>\n                                    Choose Mode\n                                  </Button>\n                                  <Button variant=\"outline\" asChild>\n                                    <Link href=\"/dashboard\">Go to Dashboard</Link>\n                                  </Button>\n                                </div>\n                              </motion.div>\n                          )\n                      )}\n                    </motion.div>\n                ) : (\n                    // Active Game Screen\n                    <div className=\"space-y-6\">\n                      {/* Game HUD */}\n                      <div className=\"flex flex-col space-y-2\">\n                        <div className=\"flex items-center justify-between bg-muted/40 border rounded-lg px-4 py-3\">\n                          <div className=\"flex items-center space-x-4\">\n                            <div>\n                              <div className=\"text-xs uppercase text-muted-foreground\">Score</div>\n                              <div className=\"font-bold\">{score}</div>\n                            </div>\n                            <div>\n                              <div className=\"text-xs uppercase text-muted-foreground\">Level</div>\n                              <div className=\"font-bold\">{level}</div>\n                            </div>\n                            {combo > 1 && (\n                                <div>\n                                  <div className=\"text-xs uppercase text-muted-foreground\">Combo</div>\n                                  <div className=\"text-primary font-bold\">{combo}x</div>\n                                </div>\n                            )}\n                          </div>\n\n                          <div className=\"flex items-center space-x-3\">\n                            <div>\n                              <div className=\"text-xs uppercase text-muted-foreground\">Lives</div>\n                              <div className=\"flex space-x-1\">\n                                {Array.from({ length: 3 }).map((_, i) => (\n                                    <div\n                                        key={i}\n                                        className={`w-3 h-3 rounded-full ${\n                                            i < lives ? 'bg-red-500' : 'bg-muted'\n                                        }`}\n                                    />\n                                ))}\n                              </div>\n                            </div>\n                            <div>\n                              <div className=\"text-xs uppercase text-muted-foreground\">Time</div>\n                              <div className=\"font-mono\">{timeLeft}s</div>\n                            </div>\n                          </div>\n                        </div>\n\n                        {/* Game mode badge */}\n                        <div className=\"flex justify-between items-center\">\n                          <Badge variant=\"outline\" className=\"text-xs\">\n                            {gameMode === \"classic\" ? \"Classic Mode\" :\n                                gameMode === \"daily\" ? \"Daily Challenge\" : \"Blitz Mode\"}\n                          </Badge>\n\n                          {/* Power-up display */}\n                          {powerUp && (\n                              <motion.div\n                                  className=\"bg-primary/20 text-primary text-sm rounded-lg px-3 py-1.5 flex items-center\"\n                                  initial={{ opacity: 0, y: -10 }}\n                                  animate={{ opacity: 1, y: 0 }}\n                              >\n                                <Flame className=\"h-4 w-4 mr-2 animate-pulse\" />\n                                <span>{powerUpDescriptions[powerUp as keyof typeof powerUpDescriptions]}</span>\n                                <span className=\"ml-2 text-xs\">{powerUpTimeLeft}s</span>\n                              </motion.div>\n                          )}\n                        </div>\n                      </div>\n\n                      {/* Current Entry */}\n                      {currentEntry && (\n                          <motion.div\n                              className={`bg-card border rounded-lg p-6 text-center relative overflow-hidden ${\n                                  criticalEntryActive ? 'border-red-500' :\n                                      streakBreakerActive ? 'border-yellow-500' : ''\n                              }`}\n                              key={currentEntry.text + Math.random()} // Ensure animation on change\n                              initial={{ opacity: 0 }}\n                              animate={{ opacity: 1 }}\n                          >\n                            {criticalEntryActive ? (\n                                // Critical entry timer\n                                <Progress\n                                    value={(criticalTimeLeft / 5) * 100}\n                                    className=\"h-1 absolute top-0 left-0 right-0 rounded-none bg-muted [&>div]:bg-red-500\"\n                                />\n                            ) : (\n                                // Regular timer\n                                <Progress\n                                    value={(timeLeft / getTimerDuration()) * 100}\n                                    className=\"h-1 absolute top-0 left-0 right-0 rounded-none bg-muted\"\n                                />\n                            )}\n\n                            {/* Show difficulty level indicator */}\n                            <div className=\"absolute top-2 right-2 flex\">\n                              {Array.from({ length: currentEntry.difficulty }).map((_, i) => (\n                                  <Star key={i} className=\"h-3 w-3 text-yellow-500\" />\n                              ))}\n                            </div>\n\n                            {/* Critical entry indicator */}\n                            {criticalEntryActive && (\n                                <motion.div\n                                    className=\"absolute top-2 left-2 text-xs text-red-500 font-bold flex items-center\"\n                                    initial={{ opacity: 0, x: -10 }}\n                                    animate={{ opacity: 1, x: 0 }}\n                                >\n                                  <Flame className=\"h-3 w-3 mr-1\" /> CRITICAL!\n                                </motion.div>\n                            )}\n\n                            {/* Streak breaker indicator */}\n                            {streakBreakerActive && (\n                                <motion.div\n                                    className=\"absolute top-2 left-2 text-xs text-yellow-500 font-bold flex items-center\"\n                                    initial={{ opacity: 0, x: -10 }}\n                                    animate={{ opacity: 1, x: 0 }}\n                                >\n                                  <AlertCircle className=\"h-3 w-3 mr-1\" /> TRICKY!\n                                </motion.div>\n                            )}\n\n                            {/* Show level upgrade indicator */}\n                            {combo > 0 && combo % 5 === 0 && combo % 5 < 2 && (\n                                <motion.div\n                                    className=\"absolute top-2 left-2 text-xs text-primary font-bold flex items-center\"\n                                    initial={{ opacity: 0, x: -10 }}\n                                    animate={{ opacity: 1, x: 0 }}\n                                >\n                                  <ChevronUp className=\"h-3 w-3 mr-1\" /> LEVEL UP!\n                                </motion.div>\n                            )}\n\n                            <Clock className=\"h-5 w-5 text-muted-foreground mx-auto mb-2\" />\n                            <p className=\"text-xl font-medium\">&ldquo;{currentEntry.text}&rdquo;</p>\n\n                            {/* Skip button for skipEntry power-up */}\n                            {powerUp === \"skipEntry\" && (\n                                <motion.button\n                                    className=\"mt-4 text-sm flex items-center mx-auto bg-primary/10 hover:bg-primary/20 text-primary px-3 py-1 rounded-md\"\n                                    onClick={skipCurrentEntry}\n                                    whileHover={{ scale: 1.05 }}\n                                    whileTap={{ scale: 0.95 }}\n                                >\n                                  <TimerReset className=\"h-3 w-3 mr-1\" /> Skip This Entry\n                                </motion.button>\n                            )}\n                          </motion.div>\n                      )}\n\n                      {/* Answer Buttons */}\n                      <div className=\"grid grid-cols-2 md:grid-cols-3 gap-3\">\n                        {CHANGE_TYPES.map(type => (\n                            <TooltipProvider key={type.type}>\n                              <Tooltip>\n                                <TooltipTrigger asChild>\n                                  <motion.button\n                                      className={`flex flex-col items-center justify-center space-y-2 p-4 rounded-lg border-2 hover:border-primary focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2`}\n                                      onClick={() => handleAnswer(type.type)}\n                                      whileHover={{ scale: 1.02 }}\n                                      whileTap={{ scale: 0.98 }}\n                                  >\n                                    <type.icon className={`h-5 w-5 ${type.color}`} />\n                                    <span className=\"text-sm\">{type.label}</span>\n                                  </motion.button>\n                                </TooltipTrigger>\n                                <TooltipContent side=\"bottom\">\n                                  <p className=\"text-xs\">{getTypeDescription(type.type)}</p>\n                                </TooltipContent>\n                              </Tooltip>\n                            </TooltipProvider>\n                        ))}\n                      </div>\n                    </div>\n                )}\n\n                <div className=\"mt-8 text-center text-xs text-muted-foreground opacity-50\">\n                  Changelog Hero v1.1\n                </div>\n              </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n  )\n}\n\n// Helper function to get descriptions for each changelog type\nfunction getTypeDescription(type: string): string {\n  const descriptions: Record<string, string> = {\n    'feature': 'New functionality added to the application',\n    'bugfix': 'Fix for something that wasn\\'t working correctly',\n    'improvement': 'Enhancement to existing functionality',\n    'breaking': 'Change that requires users to update their code or usage',\n    'security': 'Fixes or enhancements related to security',\n    'performance': 'Changes that improve speed or resource usage',\n    'hotfix': 'Urgent fixes deployed outside regular release cycle',\n    'deprecation': 'Features marked for removal in future releases'\n  };\n\n  return descriptions[type] || '';\n}"
  },
  {
    "path": "app/startup.ts",
    "content": "import {JobRunnerService} from '@/lib/services/jobs/job-runner.service';\nimport {TelemetryService} from '@/lib/services/telemetry/service';\nimport {ensureSystemUser} from '@/lib/services/core/system-user/service';\nimport {setupDailySslRenewal} from '@/lib/custom-domains/ssl/setup-renewal-job';\nimport {spawn, exec} from 'child_process';\nimport path from 'path';\nimport {promisify} from 'util';\n\nconst execAsync = promisify(exec);\n\nlet servicesStarted = false;\n\ninterface EnvironmentValidationError extends Error {\n    name: 'EnvironmentValidationError';\n    missingVariables: string[];\n}\n\nfunction createEnvironmentError(missingVars: string[]): EnvironmentValidationError {\n    const error = new Error(\n        `FTB (Failure to Boot): Missing required environment variables: ${missingVars.join(', ')}`\n    ) as EnvironmentValidationError;\n    error.name = 'EnvironmentValidationError';\n    error.missingVariables = missingVars;\n    return error;\n}\n\nfunction checkRequirements(): void {\n    const required = [\n        'NEXT_PUBLIC_APP_URL',\n        'DATABASE_URL',\n        'JWT_ACCESS_SECRET',\n        'ANALYTICS_SALT',\n        'GITHUB_ENCRYPTION_KEY'\n        // 'ENCRYPTION_KEY' - we do not need this actually.\n    ];\n\n    const missing: string[] = [];\n\n    for (const v of required) {\n        if (!process.env[v]) {\n            missing.push(v);\n        }\n    }\n\n    if (missing.length > 0) {\n        throw createEnvironmentError(missing);\n    }\n}\n\nasync function cleanup(): Promise<void> {\n    try {\n        if (process.platform === 'win32') {\n            await execAsync('taskkill /f /im node.exe /fi \"WINDOWTITLE eq FTB Error Server*\"').catch(() => {});\n        } else {\n            await execAsync('pkill -f \"scripts/ftb/server.js\"').catch(() => {});\n        }\n        console.log('✓ Cleaned up any existing error servers');\n    } catch (error) {\n        console.log(error);\n    }\n}\n\nfunction launchGuide(missing: string[]): void {\n    const serverPath = path.join(process.cwd(), 'scripts', 'ftb', 'server.js');\n\n    console.log('\\n🚨 LAUNCHING FAILURE TO BOOT ERROR SERVER 🚨');\n    console.log('Terminating Next.js server to free port 3000...\\n');\n\n    setTimeout(() => {\n        const guide = spawn('node', [serverPath, ...missing], {\n            stdio: 'inherit',\n            cwd: process.cwd(),\n            detached: true\n        });\n\n        guide.on('error', (err) => {\n            console.error('Failed to start error server:', err);\n            console.error('Please check that scripts/ftb/server.js exists and is accessible.');\n        });\n\n        guide.unref();\n\n        process.exit(1);\n    }, 1000);\n}\n\nexport async function startBackgroundServices(): Promise<void> {\n    // Check if services are already started\n    if (servicesStarted) {\n        return;\n    }\n\n    // Prevent multiple simultaneous starts\n    servicesStarted = true;\n\n    console.log('Starting background services...');\n\n    try {\n        await cleanup();\n\n        const skipCheck = process.env.BUILD_PHASE_SKIP_VALIDATION ||\n            process.env.CI_BUILD_MODE ||\n            process.env.DOCKER_BUILD === '1';\n\n        if (!skipCheck) {\n            checkRequirements();\n            console.log('✓ Environment validation passed');\n        }\n\n        await ensureSystemUser();\n\n        await TelemetryService.initialize();\n        console.log('✓ Telemetry service initialized');\n\n        JobRunnerService.start(60000);\n        console.log('✓ Job runner started');\n\n        // Setup SSL auto-renewal if SSL is enabled\n        if (process.env.NEXT_PUBLIC_SSL_ENABLED === 'true') {\n            await setupDailySslRenewal();\n            console.log('✓ SSL auto-renewal scheduled');\n        }\n\n        const handleShutdown = async (signal: string): Promise<void> => {\n            console.log(`Received ${signal}, shutting down gracefully...`);\n\n            JobRunnerService.stop();\n            console.log('✓ Job runner stopped');\n\n            await TelemetryService.shutdown();\n            console.log('✓ Telemetry service shutdown complete');\n\n            process.exit(0);\n        };\n\n        process.on('SIGINT', () => handleShutdown('SIGINT'));\n        process.on('SIGTERM', () => handleShutdown('SIGTERM'));\n\n        console.log('✓ All background services started successfully');\n    } catch (error) {\n        // Reset the flag if startup fails\n        servicesStarted = false;\n\n        if (error instanceof Error && error.name === 'EnvironmentValidationError') {\n            const envError = error as EnvironmentValidationError;\n\n            console.error('🚨 FTB Error:', error.message);\n            console.error('Starting error server to guide you through setup...\\n');\n\n            launchGuide(envError.missingVariables);\n\n            return;\n        } else {\n            console.error('Failed to start background services:', error);\n            throw error;\n        }\n    }\n}"
  },
  {
    "path": "components/CommandPalette.tsx",
    "content": "'use client';\n\nimport {Command} from 'cmdk';\nimport {useCallback, useEffect, useState} from 'react';\nimport {useDebounce} from 'use-debounce';\nimport {\n    ArrowDownIcon,\n    ArrowUpIcon,\n    BookOpenIcon,\n    ClockIcon,\n    MagnifyingGlassIcon,\n    XMarkIcon\n} from '@heroicons/react/24/outline';\nimport {AnimatePresence, motion} from 'framer-motion';\nimport {SearchX} from 'lucide-react';\nimport {useTimezone} from '@/hooks/use-timezone';\n\ninterface SearchResult {\n    id: string;\n    title: string;\n    content?: string;\n    type: 'entry';\n    url: string;\n    tags?: Array<{\n        id: string;\n        name: string;\n        color: string | null;\n    }>;\n    projectId?: string;\n    projectName?: string;\n    version?: string | null;\n    publishedAt?: Date | null;\n}\n\ninterface CommandPaletteProps {\n    isOpen: boolean;\n    onClose: () => void;\n}\n\nconst ENABLE_TAGS = false; // Feature flag for tags\n\nexport default function ChangelogCommandPalette({isOpen, onClose}: CommandPaletteProps) {\n    const timezone = useTimezone();\n    const [search, setSearch] = useState('');\n    const [results, setResults] = useState<SearchResult[]>([]);\n    const [loading, setLoading] = useState(false);\n    const [selectedIndex, setSelectedIndex] = useState(0);\n\n    const [debouncedSearch] = useDebounce(search, 300);\n\n    useEffect(() => {\n        const performSearch = async () => {\n            if (!debouncedSearch.trim() || debouncedSearch.length < 2) {\n                setResults([]);\n                setLoading(false);\n                return;\n            }\n\n            setLoading(true);\n            try {\n                const response = await fetch('/api/search', {\n                    method: 'POST',\n                    headers: {'Content-Type': 'application/json'},\n                    body: JSON.stringify({\n                        query: debouncedSearch,\n                        limit: 12,\n                        types: ['entry'] // Only search entries for now\n                    })\n                });\n\n                const data = await response.json();\n                setResults(data.results || []);\n                setSelectedIndex(0);\n            } catch (error) {\n                console.error('Search failed:', error);\n                setResults([]);\n            } finally {\n                setLoading(false);\n            }\n        };\n\n        performSearch();\n    }, [debouncedSearch]);\n\n    useEffect(() => {\n        if (search && search.length >= 2 && search !== debouncedSearch) {\n            setLoading(true);\n        }\n    }, [search, debouncedSearch]);\n\n    useEffect(() => {\n        const handleKeyDown = (e: KeyboardEvent) => {\n            if (!isOpen) return;\n\n            switch (e.key) {\n                case 'ArrowDown':\n                    e.preventDefault();\n                    setSelectedIndex(prev => prev < results.length - 1 ? prev + 1 : prev);\n                    break;\n                case 'ArrowUp':\n                    e.preventDefault();\n                    setSelectedIndex(prev => prev > 0 ? prev - 1 : prev);\n                    break;\n                case 'Enter':\n                    e.preventDefault();\n                    if (results[selectedIndex]) {\n                        handleSelect(results[selectedIndex]);\n                    }\n                    break;\n                case 'Escape':\n                    onClose();\n                    break;\n            }\n        };\n\n        window.addEventListener('keydown', handleKeyDown);\n        return () => window.removeEventListener('keydown', handleKeyDown);\n    }, [isOpen, results, selectedIndex, onClose]);\n\n    const handleSelect = useCallback((result: SearchResult) => {\n        onClose();\n        setSearch('');\n        setResults([]);\n        window.location.href = result.url;\n    }, [onClose]);\n\n    const formatRelativeTime = (date: Date | null | undefined) => {\n        if (!date) return null;\n\n        const now = new Date();\n        const diffInSeconds = Math.floor((now.getTime() - new Date(date).getTime()) / 1000);\n\n        if (diffInSeconds < 60) return 'just now';\n        if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;\n        if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;\n        if (diffInSeconds < 2592000) return `${Math.floor(diffInSeconds / 86400)}d ago`;\n\n        return new Date(date).toLocaleDateString('en-US', {\n            month: 'short',\n            day: 'numeric',\n            timeZone: timezone,\n        });\n    };\n\n    const formatVersion = (version: string | null | undefined) => {\n        if (!version) return null;\n        return version.toLowerCase().startsWith('v') ? version : `v${version}`;\n    };\n\n    const getTagStyles = (color: string | null) => {\n        if (!color) {\n            return {\n                backgroundColor: 'rgba(99, 102, 241, 0.08)',\n                color: '#6366f1',\n                borderColor: 'rgba(99, 102, 241, 0.2)'\n            };\n        }\n\n        const hexToRgb = (hex: string) => {\n            const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n            return result ? {\n                r: parseInt(result[1], 16),\n                g: parseInt(result[2], 16),\n                b: parseInt(result[3], 16)\n            } : null;\n        };\n\n        const rgb = hexToRgb(color);\n        if (!rgb) return {backgroundColor: color + '15', color};\n\n        return {\n            backgroundColor: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.1)`,\n            color: color,\n            borderColor: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.25)`\n        };\n    };\n\n    if (!isOpen) return null;\n\n    return (\n        <AnimatePresence>\n            <motion.div\n                initial={{opacity: 0}}\n                animate={{opacity: 1}}\n                exit={{opacity: 0}}\n                transition={{duration: 0.15}}\n                className=\"fixed inset-0 z-50 flex items-start justify-center pt-[12vh]\"\n                onClick={onClose}\n            >\n                {/* Backdrop with blur */}\n                <motion.div initial={{opacity: 0, scale: 0.96, y: -20}}\n                            animate={{opacity: 1, scale: 1, y: 0}}\n                            className=\"absolute inset-0 bg-black/40 backdrop-blur-md\"/>\n\n                <motion.div\n                    initial={{opacity: 0, scale: 0.96, y: -20}}\n                    animate={{opacity: 1, scale: 1, y: 0}}\n                    exit={{opacity: 0, scale: 0.96, y: -20}}\n                    transition={{\n                        type: \"spring\",\n                        duration: 0.4,\n                        bounce: 0.1\n                    }}\n                    className=\"relative w-full max-w-2xl mx-6 bg-white/95 dark:bg-gray-950/95 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 dark:border-gray-800/50 overflow-hidden\"\n                    onClick={(e) => e.stopPropagation()}\n                    style={{\n                        boxShadow: `\n              0 0 0 1px rgba(255, 255, 255, 0.05),\n              0 25px 50px -12px rgba(0, 0, 0, 0.25),\n              0 0 80px rgba(99, 102, 241, 0.1)\n            `\n                    }}\n                >\n                    {/* Subtle animated border */}\n                    <div\n                        className=\"absolute inset-0 rounded-2xl bg-gradient-to-r from-violet-500/20 via-cyan-500/20 to-violet-500/20 p-px\">\n                        <div className=\"w-full h-full bg-white/95 dark:bg-gray-950/95 rounded-2xl\"/>\n                    </div>\n\n                    <div className=\"relative\">\n                        <Command\n                            shouldFilter={false}\n                            className=\"[&_[cmdk-group-heading]]:px-4 [&_[cmdk-group-heading]]:py-3 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-bold [&_[cmdk-group-heading]]:text-gray-500 [&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:tracking-wider [&_[cmdk-group-heading]]:border-t [&_[cmdk-group-heading]]:border-gray-100 [&_[cmdk-group-heading]]:dark:border-gray-800 [&_[cmdk-group-heading]]:mt-2 [&_[cmdk-group-heading]:first-child]:border-t-0 [&_[cmdk-group-heading]:first-child]:mt-0\"\n                        >\n                            {/* Search Header */}\n                            <div\n                                className=\"relative flex items-center px-6 py-6 border-b border-gray-100/50 dark:border-gray-800/50\">\n                                <div className=\"absolute left-6 pointer-events-none\">\n                                    <MagnifyingGlassIcon className=\"w-5 h-5 text-indigo-500\"/>\n                                </div>\n\n                                <input\n                                    value={search}\n                                    onChange={(e) => setSearch(e.target.value)}\n                                    placeholder=\"Search everything...\"\n                                    className=\"w-full pl-12 pr-12 py-1 text-lg bg-transparent outline-none placeholder-gray-400 dark:text-white font-medium border-none\"\n                                    style={{\n                                        border: 'none',\n                                        outline: 'none',\n                                        boxShadow: 'none',\n                                        WebkitAppearance: 'none',\n                                        MozAppearance: 'none',\n                                        appearance: 'none',\n                                        background: 'transparent'\n                                    }}\n                                    autoFocus\n                                />\n\n                                <AnimatePresence>\n                                    {search && (\n                                        <motion.button\n                                            initial={{opacity: 0, scale: 0.8}}\n                                            animate={{opacity: 1, scale: 1}}\n                                            exit={{opacity: 0, scale: 0.8}}\n                                            onClick={() => setSearch('')}\n                                            className=\"absolute right-6 p-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors duration-200\"\n                                        >\n                                            <XMarkIcon className=\"w-4 h-4 text-gray-400\"/>\n                                        </motion.button>\n                                    )}\n                                </AnimatePresence>\n                            </div>\n\n                            <Command.List className=\"max-h-96 overflow-y-auto\">\n                                {/* Empty State */}\n                                {!loading && search && results.length === 0 && (\n                                    <motion.div\n                                        initial={{opacity: 0, y: 20}}\n                                        animate={{opacity: 1, y: 0}}\n                                        className=\"flex flex-col items-center justify-center py-16 px-6\"\n                                    >\n                                        <motion.div\n                                            initial={{scale: 0.8}}\n                                            animate={{scale: 1}}\n                                            transition={{delay: 0.1}}\n                                            className=\"relative mb-4\"\n                                        >\n                                            <div\n                                                className=\"w-16 h-16 bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-700 rounded-2xl flex items-center justify-center shadow-lg\">\n                                                <SearchX className=\"w-8 h-8 text-gray-400\"/>\n                                            </div>\n                                            <div\n                                                className=\"absolute -inset-2 bg-gradient-to-br from-indigo-500/20 to-purple-500/20 rounded-2xl blur-xl\"/>\n                                        </motion.div>\n                                        <h3 className=\"text-base font-semibold text-gray-700 dark:text-gray-200 mb-1\">\n                                            No results found\n                                        </h3>\n                                        <p className=\"text-sm text-gray-500 dark:text-gray-400 text-center\">\n                                            Try adjusting your search terms\n                                        </p>\n                                    </motion.div>\n                                )}\n\n                                {/* Search Results */}\n                                {results.length > 0 && (\n                                    <Command.Group heading=\"Changelog Entries\">\n                                        {results.map((result, index) => {\n                                            const isSelected = index === selectedIndex;\n\n                                            return (\n                                                <motion.div\n                                                    key={result.id}\n                                                    initial={{opacity: 0, y: 8}}\n                                                    animate={{opacity: 1, y: 0}}\n                                                    transition={{delay: index * 0.05, duration: 0.2}}\n                                                >\n                                                    <Command.Item\n                                                        onSelect={() => handleSelect(result)}\n                                                        className={`\n                              relative flex items-start p-4 mx-2 mb-1 rounded-xl cursor-pointer transition-all duration-200 group\n                              ${isSelected\n                                                            ? 'bg-gradient-to-r from-indigo-50 to-purple-50 dark:from-indigo-950/50 dark:to-purple-950/50 shadow-lg ring-1 ring-indigo-200/50 dark:ring-indigo-800/50'\n                                                            : 'hover:bg-gray-50 dark:hover:bg-gray-900/30'\n                                                        }\n                            `}\n                                                    >\n                                                        {/* Animated selection indicator */}\n                                                        <AnimatePresence>\n                                                            {isSelected && (\n                                                                <motion.div\n                                                                    layoutId=\"selection-indicator\"\n                                                                    className=\"absolute left-0 top-1/2 w-1 h-8 bg-gradient-to-b from-indigo-500 to-purple-500 rounded-r-full\"\n                                                                    initial={{opacity: 0, x: -4}}\n                                                                    animate={{opacity: 1, x: 0}}\n                                                                    exit={{opacity: 0, x: -4}}\n                                                                    transition={{type: \"spring\", bounce: 0.3}}\n                                                                />\n                                                            )}\n                                                        </AnimatePresence>\n\n                                                        {/* Content Icon */}\n                                                        <div\n                                                            className={`flex-shrink-0 mr-3 mt-0.5 transition-all duration-200 ${\n                                                                isSelected\n                                                                    ? 'text-indigo-600 dark:text-indigo-400 scale-110'\n                                                                    : 'text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-300'\n                                                            }`}>\n                                                            <BookOpenIcon className=\"w-5 h-5\"/>\n                                                        </div>\n\n                                                        <div className=\"flex-1 min-w-0\">\n                                                            {/* Title */}\n                                                            <h3 className={`font-semibold text-base mb-1 transition-colors duration-200 ${\n                                                                isSelected\n                                                                    ? 'text-indigo-900 dark:text-indigo-100'\n                                                                    : 'text-gray-900 dark:text-gray-100'\n                                                            }`}>\n                                                                {result.title}\n                                                            </h3>\n\n                                                            {/* Content Preview */}\n                                                            {result.content && (\n                                                                <p className=\"text-sm text-gray-600 dark:text-gray-400 mb-3 line-clamp-2 leading-relaxed\">\n                                                                    {result.content}\n                                                                </p>\n                                                            )}\n\n                                                            {/* Metadata */}\n                                                            <div className=\"flex items-center gap-2 mb-2\">\n                                                                {result.projectName && (\n                                                                    <span\n                                                                        className=\"inline-flex items-center px-2 py-1 text-xs font-medium bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-md\">\n                                    {result.projectName}\n                                  </span>\n                                                                )}\n\n                                                                {result.version && (\n                                                                    <span\n                                                                        className=\"inline-flex items-center px-2 py-1 text-xs font-medium bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300 rounded-md\">\n                                    {formatVersion(result.version)}\n                                  </span>\n                                                                )}\n\n                                                                {result.publishedAt && (\n                                                                    <span\n                                                                        className=\"inline-flex items-center text-xs text-gray-500 dark:text-gray-400\">\n                                    <ClockIcon className=\"w-3 h-3 mr-1\"/>\n                                                                        {formatRelativeTime(new Date(result.publishedAt))}\n                                  </span>\n                                                                )}\n                                                            </div>\n\n                                                            {/* Tags */}\n                                                            {ENABLE_TAGS && result.tags && result.tags.length > 0 && (\n                                                                <div className=\"flex flex-wrap gap-1.5\">\n                                                                    {result.tags.slice(0, 3).map((tag) => (\n                                                                        <span\n                                                                            key={tag.id}\n                                                                            className=\"inline-flex items-center px-2 py-0.5 text-xs font-medium rounded-md border\"\n                                                                            style={getTagStyles(tag.color)}\n                                                                        >\n                                      {tag.name}\n                                    </span>\n                                                                    ))}\n                                                                    {result.tags.length > 3 && (\n                                                                        <span\n                                                                            className=\"inline-flex items-center px-2 py-0.5 text-xs text-gray-500 dark:text-gray-400\">\n                                      +{result.tags.length - 3}\n                                    </span>\n                                                                    )}\n                                                                </div>\n                                                            )}\n                                                        </div>\n                                                    </Command.Item>\n                                                </motion.div>\n                                            );\n                                        })}\n                                    </Command.Group>\n                                )}\n                            </Command.List>\n\n                            {/* Footer */}\n                            <div\n                                className=\"border-t border-gray-100/50 dark:border-gray-800/50 px-4 py-3 bg-gray-50/50 dark:bg-gray-900/30\">\n                                <div\n                                    className=\"flex items-center justify-between text-xs text-gray-500 dark:text-gray-400\">\n                                    <div className=\"flex items-center gap-4\">\n                                        <div className=\"flex items-center gap-1.5\">\n                                            <kbd\n                                                className=\"px-2 py-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded text-xs font-mono shadow-sm\">\n                                                ↵\n                                            </kbd>\n                                            <span>select</span>\n                                        </div>\n                                        <div className=\"flex items-center gap-1.5\">\n                                            <div className=\"flex\">\n                                                <kbd\n                                                    className=\"px-1.5 py-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-l text-xs font-mono shadow-sm\">\n                                                    <ArrowUpIcon className=\"w-3 h-3\"/>\n                                                </kbd>\n                                                <kbd\n                                                    className=\"px-1.5 py-1 bg-white dark:bg-gray-800 border-t border-r border-b border-gray-200 dark:border-gray-700 rounded-r text-xs font-mono shadow-sm\">\n                                                    <ArrowDownIcon className=\"w-3 h-3\"/>\n                                                </kbd>\n                                            </div>\n                                            <span>navigate</span>\n                                        </div>\n                                    </div>\n                                    <div className=\"flex items-center gap-1.5\">\n                                        <kbd\n                                            className=\"px-2 py-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded text-xs font-mono shadow-sm\">\n                                            esc\n                                        </kbd>\n                                        <span>close</span>\n                                    </div>\n                                </div>\n                            </div>\n                        </Command>\n                    </div>\n                </motion.div>\n            </motion.div>\n        </AnimatePresence>\n    );\n}"
  },
  {
    "path": "components/DinoGame.tsx",
    "content": "// components/DinoGame.tsx\n\n'use client'\n\nimport React, { useEffect, useRef, useState, useCallback } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { X, RotateCcw, Play } from 'lucide-react'\nimport { motion, AnimatePresence } from 'framer-motion'\n\ninterface DinoGameProps {\n    isOpen: boolean\n    onClose: () => void\n}\n\ninterface Dino {\n    x: number\n    y: number\n    width: number\n    height: number\n    dy: number\n    grounded: boolean\n    isJumping: boolean\n    isDucking: boolean\n    runFrame: number\n}\n\ninterface Obstacle {\n    x: number\n    y: number\n    width: number\n    height: number\n    type: 'cactus_small' | 'cactus_large' | 'bird_high' | 'bird_low'\n}\n\ninterface Cloud {\n    x: number\n    y: number\n    width: number\n    height: number\n    speed: number\n}\n\ninterface GameState {\n    dino: Dino\n    obstacles: Obstacle[]\n    clouds: Cloud[]\n    speed: number\n    score: number\n    highScore: number\n    gameOver: boolean\n    gameStarted: boolean\n    keys: Record<string, boolean>\n    frameCount: number\n    lastObstacleSpawn: number\n}\n\nconst DinoGame: React.FC<DinoGameProps> = ({ isOpen, onClose }) => {\n    const canvasRef = useRef<HTMLCanvasElement>(null)\n    const gameLoopRef = useRef<number | null>(null)\n    const [score, setScore] = useState(0)\n    const [highScore, setHighScore] = useState(0)\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    const [gameOver, setGameOver] = useState(false)\n    const [gameStarted, setGameStarted] = useState(false)\n\n    const GRAVITY = 0.8\n    const JUMP_STRENGTH = -16\n    const GROUND_Y = 300\n    const CANVAS_WIDTH = 900\n    const CANVAS_HEIGHT = 400\n    const DINO_WIDTH = 44\n    const DINO_HEIGHT = 48\n    const DINO_DUCK_HEIGHT = 28\n\n    // Game state\n    const gameState = useRef<GameState>({\n        dino: {\n            x: 80,\n            y: GROUND_Y - DINO_HEIGHT,\n            width: DINO_WIDTH,\n            height: DINO_HEIGHT,\n            dy: 0,\n            grounded: true,\n            isJumping: false,\n            isDucking: false,\n            runFrame: 0\n        },\n        obstacles: [],\n        clouds: [],\n        speed: 6,\n        score: 0,\n        highScore: 0,\n        gameOver: false,\n        gameStarted: false,\n        keys: {},\n        frameCount: 0,\n        lastObstacleSpawn: 0\n    })\n\n    // Load high score from localStorage\n    useEffect(() => {\n        const savedHighScore = localStorage.getItem('dino-high-score')\n        if (savedHighScore) {\n            const score = parseInt(savedHighScore, 10)\n            setHighScore(score)\n            gameState.current.highScore = score\n        }\n    }, [])\n\n    const saveHighScore = useCallback((newScore: number) => {\n        if (newScore > gameState.current.highScore) {\n            gameState.current.highScore = newScore\n            setHighScore(newScore)\n            localStorage.setItem('dino-high-score', newScore.toString())\n        }\n    }, [])\n\n    const resetGame = useCallback(() => {\n        gameState.current = {\n            ...gameState.current,\n            dino: {\n                x: 80,\n                y: GROUND_Y - DINO_HEIGHT + 3, // Position dino so its bottom touches the ground line\n                width: DINO_WIDTH,\n                height: DINO_HEIGHT,\n                dy: 0,\n                grounded: true,\n                isJumping: false,\n                isDucking: false,\n                runFrame: 0\n            },\n            obstacles: [],\n            clouds: [\n                { x: 200, y: 60, width: 60, height: 25, speed: 0.8 },\n                { x: 450, y: 40, width: 80, height: 30, speed: 0.6 },\n                { x: 700, y: 80, width: 70, height: 20, speed: 0.7 },\n                { x: 300, y: 100, width: 50, height: 18, speed: 0.5 }\n            ],\n            speed: 6,\n            score: 0,\n            gameOver: false,\n            gameStarted: true,\n            keys: {},\n            frameCount: 0,\n            lastObstacleSpawn: 0\n        }\n        setScore(0)\n        setGameOver(false)\n        setGameStarted(true)\n    }, [])\n\n    const jump = useCallback(() => {\n        const state = gameState.current\n        if (state.dino.grounded && !state.gameOver && !state.dino.isDucking) {\n            state.dino.dy = JUMP_STRENGTH\n            state.dino.grounded = false\n            state.dino.isJumping = true\n        }\n    }, [])\n\n    const duck = useCallback((isDucking: boolean) => {\n        const state = gameState.current\n        if (!state.gameOver) {\n            if (isDucking && state.dino.grounded) {\n                state.dino.isDucking = true\n                state.dino.height = DINO_DUCK_HEIGHT\n                state.dino.y = GROUND_Y - DINO_DUCK_HEIGHT // Position so bottom touches ground\n            } else if (!isDucking && state.dino.isDucking) {\n                state.dino.isDucking = false\n                state.dino.height = DINO_HEIGHT\n                if (state.dino.grounded) {\n                    state.dino.y = GROUND_Y - DINO_HEIGHT // Position so bottom touches ground\n                }\n            }\n        }\n    }, [])\n\n    const handleKeyDown = useCallback((e: KeyboardEvent) => {\n        if (e.code === 'Space' || e.code === 'ArrowUp') {\n            e.preventDefault()\n            if (!gameState.current.gameStarted) {\n                resetGame()\n            } else {\n                jump()\n            }\n        }\n        if (e.code === 'ArrowDown') {\n            e.preventDefault()\n            duck(true)\n        }\n        gameState.current.keys[e.code] = true\n    }, [jump, resetGame, duck])\n\n    const handleKeyUp = useCallback((e: KeyboardEvent) => {\n        if (e.code === 'ArrowDown') {\n            duck(false)\n        }\n        gameState.current.keys[e.code] = false\n    }, [duck])\n\n    const spawnObstacle = useCallback(() => {\n        const state = gameState.current\n        const obstacleTypes: Obstacle['type'][] = ['cactus_small', 'cactus_large', 'bird_high', 'bird_low']\n        const obstacleType = obstacleTypes[Math.floor(Math.random() * obstacleTypes.length)]\n\n        let obstacle: Obstacle\n\n        switch (obstacleType) {\n            case 'cactus_small':\n                obstacle = {\n                    x: CANVAS_WIDTH,\n                    y: GROUND_Y - 35,\n                    width: 17,\n                    height: 35,\n                    type: 'cactus_small'\n                }\n                break\n            case 'cactus_large':\n                obstacle = {\n                    x: CANVAS_WIDTH,\n                    y: GROUND_Y - 50,\n                    width: 25,\n                    height: 50,\n                    type: 'cactus_large'\n                }\n                break\n            case 'bird_high':\n                obstacle = {\n                    x: CANVAS_WIDTH,\n                    y: GROUND_Y - 100, // Higher up - requires ducking when jumping\n                    width: 42,\n                    height: 30,\n                    type: 'bird_high'\n                }\n                break\n            case 'bird_low':\n                obstacle = {\n                    x: CANVAS_WIDTH,\n                    y: GROUND_Y - 55, // Lower - can be jumped over\n                    width: 42,\n                    height: 30,\n                    type: 'bird_low'\n                }\n                break\n        }\n\n        state.obstacles.push(obstacle)\n        state.lastObstacleSpawn = state.frameCount\n    }, [])\n\n    const updateClouds = useCallback(() => {\n        const state = gameState.current\n\n        state.clouds.forEach(cloud => {\n            cloud.x -= cloud.speed\n            if (cloud.x + cloud.width < 0) {\n                cloud.x = CANVAS_WIDTH + Math.random() * 300\n                cloud.y = 40 + Math.random() * 80\n                cloud.width = 50 + Math.random() * 40\n                cloud.height = 15 + Math.random() * 15\n            }\n        })\n    }, [])\n\n    // Improved collision detection with proper hitboxes\n    const checkCollision = useCallback((dino: Dino, obstacle: Obstacle): boolean => {\n        // More precise hitboxes with proper margins\n        const dinoHitbox = {\n            left: dino.x + 8,\n            right: dino.x + dino.width - 8,\n            top: dino.y + (dino.isDucking ? 4 : 8),\n            bottom: dino.y + dino.height - 4\n        }\n\n        const obstacleHitbox = {\n            left: obstacle.x + 4,\n            right: obstacle.x + obstacle.width - 4,\n            top: obstacle.y + 4,\n            bottom: obstacle.y + obstacle.height - 4\n        }\n\n        return (\n            dinoHitbox.left < obstacleHitbox.right &&\n            dinoHitbox.right > obstacleHitbox.left &&\n            dinoHitbox.top < obstacleHitbox.bottom &&\n            dinoHitbox.bottom > obstacleHitbox.top\n        )\n    }, [])\n\n    const update = useCallback(() => {\n        const state = gameState.current\n        if (state.gameOver || !state.gameStarted) return\n\n        state.frameCount++\n\n        // Update dino animation\n        if (state.dino.grounded && !state.dino.isDucking) {\n            state.dino.runFrame = Math.floor((state.frameCount / 8) % 2)\n        }\n\n        // Update dino physics\n        state.dino.dy += GRAVITY\n        state.dino.y += state.dino.dy\n\n        // Ground collision - dino should be ON the ground line, not above it\n        const targetGroundY = state.dino.isDucking\n            ? GROUND_Y - DINO_DUCK_HEIGHT + 3\n            : GROUND_Y - DINO_HEIGHT + 3; // Add 3 pixels here too\n\n        if (state.dino.y >= targetGroundY) {\n            state.dino.y = targetGroundY\n            state.dino.dy = 0\n            state.dino.grounded = true\n            state.dino.isJumping = false\n        }\n\n        // Improved obstacle spawning with better spacing and timing\n        const timeSinceLastSpawn = state.frameCount - state.lastObstacleSpawn\n        const minSpawnDistance = Math.max(120, 200 - Math.floor(state.score / 1000) * 10) // Minimum frames between spawns\n        const baseSpawnChance = 0.008 // Reduced base spawn rate\n        const speedBonus = Math.min((state.speed - 6) * 0.001, 0.004) // Cap the speed bonus\n        const spawnChance = baseSpawnChance + speedBonus\n\n        if (timeSinceLastSpawn > minSpawnDistance && Math.random() < spawnChance) {\n            const lastObstacle = state.obstacles[state.obstacles.length - 1]\n            const minDistance = Math.max(250, 350 - Math.floor(state.score / 2000) * 50) // Minimum pixel distance\n\n            if (!lastObstacle || lastObstacle.x < CANVAS_WIDTH - minDistance) {\n                spawnObstacle()\n            }\n        }\n\n        // Update obstacles\n        state.obstacles = state.obstacles.filter(obstacle => {\n            obstacle.x -= state.speed\n            return obstacle.x > -obstacle.width\n        })\n\n        // Update clouds\n        updateClouds()\n\n        // Collision detection with improved hitboxes\n        for (const obstacle of state.obstacles) {\n            if (checkCollision(state.dino, obstacle)) {\n                state.gameOver = true\n                setGameOver(true)\n                saveHighScore(Math.floor(state.score / 10))\n                return\n            }\n        }\n\n        // Update score and speed with more balanced progression\n        state.score += 1\n\n        // More balanced speed increase - slower progression\n        if (state.score % 500 === 0) { // Increased interval\n            state.speed += 0.2 // Reduced speed increment\n        }\n\n        // Cap the maximum speed to keep the game playable\n        state.speed = Math.min(state.speed, 12)\n\n        const currentScore = Math.floor(state.score / 10)\n        setScore(currentScore)\n    }, [spawnObstacle, updateClouds, saveHighScore, checkCollision])\n\n    const drawDino = useCallback((ctx: CanvasRenderingContext2D, dino: Dino) => {\n        const { x, y, width, height, isJumping, isDucking, runFrame } = dino\n\n        // Main body color\n        const bodyColor = gameState.current.gameOver ? '#ef4444' : '#22c55e'\n        const darkColor = gameState.current.gameOver ? '#dc2626' : '#16a34a'\n\n        if (isDucking) {\n            // Ducking dino - horizontal body\n            ctx.fillStyle = bodyColor\n            ctx.fillRect(x, y, width, height)\n\n            // Head\n            ctx.fillRect(x + width - 18, y - 8, 18, 20)\n\n            // Eye\n            ctx.fillStyle = '#000'\n            ctx.fillRect(x + width - 8, y - 4, 3, 3)\n\n            // Tail\n            ctx.fillStyle = darkColor\n            ctx.fillRect(x - 8, y + 4, 10, 8)\n\n            // Legs (running)\n            ctx.fillStyle = bodyColor\n            const legOffset = Math.floor(gameState.current.frameCount / 6) % 2\n            ctx.fillRect(x + 8, y + height, 6, 8 + legOffset)\n            ctx.fillRect(x + 20, y + height, 6, 8 + (1 - legOffset))\n\n        } else {\n            // Standing/jumping dino - vertical body\n            ctx.fillStyle = bodyColor\n            ctx.fillRect(x, y, width, height)\n\n            // Head details\n            ctx.fillStyle = darkColor\n            ctx.fillRect(x, y, width, 12) // head\n\n            // Eye\n            ctx.fillStyle = '#000'\n            ctx.fillRect(x + width - 12, y + 3, 4, 4)\n\n            // Nostril\n            ctx.fillRect(x + width - 6, y + 6, 2, 2)\n\n            // Arms\n            ctx.fillStyle = bodyColor\n            ctx.fillRect(x - 6, y + 15, 8, 4)\n            ctx.fillRect(x - 4, y + 19, 6, 8)\n\n            // Tail\n            ctx.fillStyle = darkColor\n            ctx.fillRect(x - 10, y + 10, 12, 6)\n\n            if (!isJumping) {\n                // Running legs animation\n                ctx.fillStyle = bodyColor\n                if (runFrame === 0) {\n                    ctx.fillRect(x + 8, y + height, 8, 12)\n                    ctx.fillRect(x + 20, y + height, 8, 8)\n                } else {\n                    ctx.fillRect(x + 8, y + height, 8, 8)\n                    ctx.fillRect(x + 20, y + height, 8, 12)\n                }\n\n                // Feet\n                ctx.fillStyle = darkColor\n                ctx.fillRect(x + 6, y + height + (runFrame === 0 ? 12 : 8), 12, 4)\n                ctx.fillRect(x + 22, y + height + (runFrame === 0 ? 8 : 12), 12, 4)\n            } else {\n                // Jumping legs - together\n                ctx.fillStyle = bodyColor\n                ctx.fillRect(x + 12, y + height, 12, 10)\n                ctx.fillStyle = darkColor\n                ctx.fillRect(x + 10, y + height + 10, 16, 4)\n            }\n        }\n\n        // Belly detail\n        ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'\n        if (isDucking) {\n            ctx.fillRect(x + 4, y + 4, width - 8, height - 8)\n        } else {\n            ctx.fillRect(x + 4, y + 16, width - 8, height - 20)\n        }\n    }, [])\n\n    const drawObstacle = useCallback((ctx: CanvasRenderingContext2D, obstacle: Obstacle) => {\n        const { x, y, width, height, type } = obstacle\n\n        if (type.startsWith('cactus')) {\n            // Cactus green\n            ctx.fillStyle = '#16a34a'\n            ctx.fillRect(x, y, width, height)\n\n            // Cactus details\n            ctx.fillStyle = '#15803d'\n\n            if (type === 'cactus_large') {\n                // Large cactus with arms\n                ctx.fillRect(x - 6, y + 15, 12, 6)\n                ctx.fillRect(x + width - 6, y + 25, 12, 6)\n                // Vertical segments\n                ctx.fillRect(x + 2, y, 4, height)\n                ctx.fillRect(x + width - 6, y, 4, height)\n                // Spikes\n                for (let i = 0; i < height; i += 8) {\n                    ctx.fillRect(x - 2, y + i, 2, 3)\n                    ctx.fillRect(x + width, y + i + 4, 2, 3)\n                }\n            } else {\n                // Small cactus\n                ctx.fillRect(x + 2, y, 4, height)\n                // Spikes\n                for (let i = 0; i < height; i += 6) {\n                    ctx.fillRect(x - 1, y + i, 2, 2)\n                    ctx.fillRect(x + width - 1, y + i + 3, 2, 2)\n                }\n            }\n        } else {\n            // Bird/Pterodactyl\n            ctx.fillStyle = '#525252'\n\n            // Body\n            ctx.fillRect(x + 8, y + 8, width - 16, height - 16)\n\n            // Head\n            ctx.fillRect(x + width - 12, y + 4, 12, 12)\n\n            // Beak\n            ctx.fillRect(x + width - 4, y + 8, 6, 4)\n\n            // Wing animation\n            const wingFlap = Math.floor(gameState.current.frameCount / 6) % 2\n            if (wingFlap) {\n                // Wings up\n                ctx.fillRect(x, y, width, 8)\n                ctx.fillRect(x + 4, y - 4, width - 8, 4)\n            } else {\n                // Wings down\n                ctx.fillRect(x, y + height - 8, width, 8)\n                ctx.fillRect(x + 4, y + height, width - 8, 4)\n            }\n\n            // Eye\n            ctx.fillStyle = '#000'\n            ctx.fillRect(x + width - 8, y + 6, 2, 2)\n\n            // Tail\n            ctx.fillStyle = '#404040'\n            ctx.fillRect(x - 4, y + 12, 8, 6)\n        }\n    }, [])\n\n    const drawCloud = useCallback((ctx: CanvasRenderingContext2D, cloud: Cloud) => {\n        ctx.fillStyle = '#e5e7eb'\n\n        // Draw cloud with multiple circles for fluffy effect\n        const circles = [\n            { x: cloud.x, y: cloud.y, r: cloud.height * 0.4 },\n            { x: cloud.x + cloud.width * 0.3, y: cloud.y - cloud.height * 0.2, r: cloud.height * 0.5 },\n            { x: cloud.x + cloud.width * 0.6, y: cloud.y, r: cloud.height * 0.4 },\n            { x: cloud.x + cloud.width * 0.8, y: cloud.y + cloud.height * 0.1, r: cloud.height * 0.3 },\n            { x: cloud.x + cloud.width, y: cloud.y, r: cloud.height * 0.4 }\n        ]\n\n        ctx.beginPath()\n        circles.forEach(circle => {\n            ctx.arc(circle.x, circle.y, circle.r, 0, Math.PI * 2)\n        })\n        ctx.fill()\n    }, [])\n\n    const drawGround = useCallback((ctx: CanvasRenderingContext2D) => {\n        const state = gameState.current\n        const isDark = Math.floor(state.score / 2000) % 2 === 1\n\n        // Ground line\n        ctx.fillStyle = isDark ? '#666' : '#404040'\n        ctx.fillRect(0, GROUND_Y, CANVAS_WIDTH, 3)\n\n        // Ground texture - moving dots and lines\n        ctx.fillStyle = isDark ? '#555' : '#333'\n\n        // Moving ground pattern\n        const offset = (state.frameCount * state.speed / 2) % 40\n        for (let i = -40; i < CANVAS_WIDTH; i += 40) {\n            const x = i + offset\n            // Small rocks/debris\n            ctx.fillRect(x, GROUND_Y + 4, 3, 2)\n            ctx.fillRect(x + 15, GROUND_Y + 5, 2, 1)\n            ctx.fillRect(x + 25, GROUND_Y + 4, 4, 2)\n\n            // Ground cracks\n            ctx.fillRect(x + 8, GROUND_Y + 3, 8, 1)\n            ctx.fillRect(x + 30, GROUND_Y + 3, 6, 1)\n        }\n    }, [])\n\n    const draw = useCallback(() => {\n        const canvas = canvasRef.current\n        if (!canvas) return\n\n        const ctx = canvas.getContext('2d')\n        if (!ctx) return\n\n        const state = gameState.current\n\n        // Day/night cycle\n        const isDark = Math.floor(state.score / 2000) % 2 === 1\n\n        // Gradient sky\n        const gradient = ctx.createLinearGradient(0, 0, 0, CANVAS_HEIGHT)\n        if (isDark) {\n            gradient.addColorStop(0, '#0f172a')\n            gradient.addColorStop(1, '#1e293b')\n        } else {\n            gradient.addColorStop(0, '#dbeafe')\n            gradient.addColorStop(1, '#f0f9ff')\n        }\n\n        ctx.fillStyle = gradient\n        ctx.fillRect(0, 0, canvas.width, canvas.height)\n\n        // Draw stars if dark\n        if (isDark) {\n            ctx.fillStyle = '#ffffff'\n            for (let i = 0; i < 20; i++) {\n                const x = (i * 73 + state.frameCount * 0.1) % CANVAS_WIDTH\n                const y = 20 + (i * 31) % 80\n                if (Math.sin(state.frameCount * 0.05 + i) > 0.5) {\n                    ctx.fillRect(x, y, 1, 1)\n                }\n            }\n        }\n\n        // Draw clouds\n        state.clouds.forEach(cloud => drawCloud(ctx, cloud))\n\n        // Draw ground\n        drawGround(ctx)\n\n        // Draw dino\n        drawDino(ctx, state.dino)\n\n        // Draw obstacles\n        state.obstacles.forEach(obstacle => drawObstacle(ctx, obstacle))\n\n        // Draw UI\n        ctx.fillStyle = isDark ? '#fff' : '#1f2937'\n        ctx.font = 'bold 20px monospace'\n        ctx.fillText(`${Math.floor(state.score / 10).toString().padStart(5, '0')}`, CANVAS_WIDTH - 120, 35)\n\n        ctx.font = '14px monospace'\n        ctx.fillText(`HI ${state.highScore.toString().padStart(5, '0')}`, CANVAS_WIDTH - 120, 55)\n\n        if (!state.gameStarted) {\n            // Start screen\n            ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'\n            ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)\n\n            ctx.fillStyle = '#fff'\n            ctx.font = 'bold 32px monospace'\n            ctx.textAlign = 'center'\n            ctx.fillText('🦖 CHANGERAWR DINO', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 - 40)\n\n            ctx.font = '18px monospace'\n            ctx.fillText('Press SPACE to start running!', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2)\n\n            ctx.font = '14px monospace'\n            ctx.fillStyle = '#bbb'\n            ctx.fillText('↑ SPACE: Jump  •  ↓: Duck', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 + 30)\n            ctx.textAlign = 'left'\n        }\n\n        if (state.gameOver) {\n            // Game over overlay\n            ctx.fillStyle = 'rgba(0, 0, 0, 0.85)'\n            ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)\n\n            ctx.fillStyle = '#fff'\n            ctx.font = 'bold 36px monospace'\n            ctx.textAlign = 'center'\n            ctx.fillText('GAME OVER', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 - 30)\n\n            ctx.font = '20px monospace'\n            ctx.fillText(`Score: ${Math.floor(state.score / 10)}`, CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 + 10)\n\n            if (Math.floor(state.score / 10) === state.highScore && state.highScore > 0) {\n                ctx.fillStyle = '#ffd700'\n                ctx.font = 'bold 18px monospace'\n                ctx.fillText('🏆 NEW HIGH SCORE! 🏆', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 + 40)\n            }\n\n            ctx.fillStyle = '#ccc'\n            ctx.font = '16px monospace'\n            ctx.fillText('Press Start Game to play again', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 + 70)\n            ctx.textAlign = 'left'\n        }\n    }, [drawDino, drawObstacle, drawCloud, drawGround])\n\n    const gameLoop = useCallback(() => {\n        update()\n        draw()\n        gameLoopRef.current = requestAnimationFrame(gameLoop)\n    }, [update, draw])\n\n    useEffect(() => {\n        if (isOpen) {\n            window.addEventListener('keydown', handleKeyDown)\n            window.addEventListener('keyup', handleKeyUp)\n            gameLoopRef.current = requestAnimationFrame(gameLoop)\n\n            return () => {\n                window.removeEventListener('keydown', handleKeyDown)\n                window.removeEventListener('keyup', handleKeyUp)\n                if (gameLoopRef.current) {\n                    cancelAnimationFrame(gameLoopRef.current)\n                }\n            }\n        }\n    }, [isOpen, handleKeyDown, handleKeyUp, gameLoop])\n\n    useEffect(() => {\n        if (!isOpen) {\n            setGameStarted(false)\n            setGameOver(false)\n            setScore(0)\n            if (gameLoopRef.current) {\n                cancelAnimationFrame(gameLoopRef.current)\n                gameLoopRef.current = null\n            }\n        }\n    }, [isOpen])\n\n    if (!isOpen) return null\n\n    return (\n        <AnimatePresence>\n            <motion.div\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                exit={{ opacity: 0 }}\n                className=\"fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4\"\n            >\n                <motion.div\n                    initial={{ scale: 0.9, opacity: 0, y: 20 }}\n                    animate={{ scale: 1, opacity: 1, y: 0 }}\n                    exit={{ scale: 0.9, opacity: 0, y: 20 }}\n                    transition={{ type: \"spring\", damping: 20, stiffness: 300 }}\n                    className=\"bg-background border-2 border-border rounded-xl p-6 w-full max-w-5xl shadow-2xl\"\n                >\n                    <div className=\"flex items-center justify-between mb-6\">\n                        <div className=\"flex items-center gap-4\">\n                            <h2 className=\"text-3xl font-bold bg-gradient-to-r from-green-600 to-emerald-600 bg-clip-text text-transparent\">\n                                🦖 Changerawr Dino\n                            </h2>\n                            <div className=\"text-lg font-mono text-muted-foreground\">\n                                {score.toString().padStart(5, '0')}\n                            </div>\n                        </div>\n                        <Button variant=\"ghost\" size=\"icon\" onClick={onClose} className=\"text-muted-foreground hover:text-foreground\">\n                            <X className=\"h-5 w-5\" />\n                        </Button>\n                    </div>\n\n                    <div className=\"bg-gradient-to-b from-sky-100 to-sky-50 dark:from-slate-800 dark:to-slate-900 rounded-lg p-4 mb-6 shadow-inner\">\n                        <canvas\n                            ref={canvasRef}\n                            width={CANVAS_WIDTH}\n                            height={CANVAS_HEIGHT}\n                            className=\"w-full rounded border-2 border-slate-300 dark:border-slate-600 shadow-lg\"\n                            tabIndex={0}\n                        />\n                    </div>\n\n                    <div className=\"text-center space-y-4\">\n                        <div className=\"flex justify-center gap-8 text-sm text-muted-foreground\">\n                            <div className=\"flex items-center gap-2\">\n                                <kbd className=\"px-3 py-1 bg-muted rounded-md font-mono text-xs border\">SPACE</kbd>\n                                <span>Jump</span>\n                            </div>\n                            <div className=\"flex items-center gap-2\">\n                                <kbd className=\"px-3 py-1 bg-muted rounded-md font-mono text-xs border\">↓</kbd>\n                                <span>Duck</span>\n                            </div>\n                        </div>\n\n                        <div className=\"flex justify-center gap-4\">\n                            <Button\n                                onClick={resetGame}\n                                variant=\"default\"\n                                size=\"lg\"\n                                className=\"flex items-center gap-2 bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700\"\n                            >\n                                {gameStarted ? <RotateCcw className=\"h-4 w-4\"/> : <Play className=\"h-4 w-4\"/>}\n                                {gameStarted ? 'Restart Game' : 'Start Game'}\n                            </Button>\n                            <Button onClick={onClose} variant=\"outline\" size=\"lg\">\n                                Close\n                            </Button>\n                        </div>\n\n                        {highScore > 0 && (\n                            <div className=\"text-sm text-muted-foreground\">\n                                High Score: <span className=\"font-mono font-bold\">{highScore}</span>\n                            </div>\n                        )}\n                    </div>\n                </motion.div>\n            </motion.div>\n        </AnimatePresence>\n    )\n}\n\nexport default DinoGame"
  },
  {
    "path": "components/Logo.tsx",
    "content": "import * as React from \"react\";\n\nconst SVGComponent = (props: React.JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>) => (\n    <svg\n        width={325}\n        height={50}\n        viewBox=\"0 0 325 50\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        {...props}\n    >\n        <defs>\n            <linearGradient\n                id=\"rainbowGradient\"\n                x1=\"0%\"\n                y1=\"0%\"\n                x2=\"100%\"\n                y2=\"100%\"\n                gradientUnits=\"objectBoundingBox\"\n            >\n                <stop offset=\"0%\" stopColor=\"#186cb8\"/>\n                <stop offset=\"19%\" stopColor=\"#186cb8\"/>\n                <stop offset=\"20%\" stopColor=\"#2a9a9f\"/>\n                <stop offset=\"39%\" stopColor=\"#2a9a9f\"/>\n                <stop offset=\"40%\" stopColor=\"#f1b211\"/>\n                <stop offset=\"59%\" stopColor=\"#f1b211\"/>\n                <stop offset=\"60%\" stopColor=\"#e83611\"/>\n                <stop offset=\"79%\" stopColor=\"#e83611\"/>\n                <stop offset=\"80%\" stopColor=\"#f9002f\"/>\n                <stop offset=\"100%\" stopColor=\"#f9002f\"/>\n            </linearGradient>\n        </defs>\n        <g transform=\"translate(10, -13)\">\n            <ellipse cx={35} cy={38} rx={18} ry={15} fill=\"white\"/>\n            <ellipse cx={58} cy={25} rx={15} ry={12} fill=\"white\"/>\n            <ellipse cx={70} cy={28} rx={8} ry={6} fill=\"white\"/>\n            <circle cx={65} cy={22} r={2.5} fill=\"#333\"/>\n            <circle cx={75} cy={26} r={1} fill=\"#666\"/>\n            <path\n                d=\"M 62 32 Q 72 35 76 32\"\n                stroke=\"white\"\n                strokeWidth={2}\n                fill=\"none\"\n            />\n            <ellipse cx={48} cy={30} rx={10} ry={8} fill=\"white\"/>\n            <ellipse cx={18} cy={42} rx={15} ry={6} fill=\"white\"/>\n            <ellipse cx={5} cy={45} rx={8} ry={4} fill=\"white\"/>\n            <ellipse cx={42} cy={48} rx={5} ry={12} fill=\"white\"/>\n            <ellipse cx={28} cy={48} rx={5} ry={12} fill=\"white\"/>\n            <ellipse cx={42} cy={62} rx={6} ry={3} fill=\"white\"/>\n            <ellipse cx={28} cy={62} rx={6} ry={3} fill=\"white\"/>\n            <ellipse cx={55} cy={35} rx={2} ry={5} fill=\"white\"/>\n            <ellipse cx={50} cy={38} rx={2} ry={5} fill=\"white\"/>\n            <circle cx={56} cy={40} r={0.8} fill=\"white\"/>\n            <circle cx={51} cy={43} r={0.8} fill=\"white\"/>\n        </g>\n        <text\n            x={100}\n            y={40}\n            fontFamily=\"Exo, Arial Black, sans-serif\"\n            fontSize={36}\n            fontWeight={900}\n            fill=\"white\"\n            stroke=\"black\"\n            strokeWidth={2}\n            paintOrder=\"stroke\"\n            letterSpacing=\"1px\"\n        >\n            {\"Change\"}\n        </text>\n        <text\n            x={238}\n            y={40}\n            fontFamily=\"Exo, Arial Black, sans-serif\"\n            fontSize={36}\n            fontWeight={900}\n            fill=\"url(#rainbowGradient)\"\n            stroke=\"black\"\n            strokeWidth={2}\n            paintOrder=\"stroke\"\n            letterSpacing=\"1px\"\n        >\n            {\"rawr\"}\n        </text>\n    </svg>\n);\nexport default SVGComponent;\n"
  },
  {
    "path": "components/MarkdownEditor.tsx",
    "content": "\"use client\"\n\nimport React, { useState, useCallback, useEffect, useRef } from 'react';\nimport { Card, CardContent } from '@/components/ui/card';\nimport { Button } from '@/components/ui/button';\nimport { Textarea } from '@/components/ui/textarea';\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';\nimport {\n    DropdownMenu,\n    DropdownMenuContent,\n    DropdownMenuItem,\n    DropdownMenuTrigger,\n    DropdownMenuSeparator,\n} from '@/components/ui/dropdown-menu';\nimport {\n    Tooltip,\n    TooltipContent,\n    TooltipProvider,\n    TooltipTrigger,\n} from '@/components/ui/tooltip';\nimport {\n    Text,\n    List,\n    Link2,\n    Quote,\n    FileCode,\n    RotateCcw,\n    RotateCw,\n    Copy,\n    Scissors,\n    Table,\n    CheckSquare,\n    ChevronDown,\n    Image,\n    Bold,\n    Italic,\n    Code,\n    Heading1,\n    Heading2,\n    Heading3,\n    Heading4,\n    Heading5,\n    ListOrdered,\n    Check,\n} from 'lucide-react';\nimport { motion, AnimatePresence } from 'framer-motion';\n\ninterface DOMPurifyInterface {\n    sanitize: (html: string, options?: Record<string, unknown>) => string;\n    setConfig: (config: Record<string, unknown>) => void;\n}\n\n// Import DOMPurify with SSR safety\nlet DOMPurify: DOMPurifyInterface = {\n    sanitize: (html: string) => html,\n    setConfig: () => {}\n};\n\n// Only import DOMPurify on client-side to avoid SSR issues\nif (typeof window !== 'undefined') {\n    import('dompurify').then(module => {\n        DOMPurify = module.default;\n    }).catch(err => {\n        console.error('Failed to load DOMPurify', err);\n    });\n}\n\ninterface MarkdownFeatures {\n    headings?: boolean;\n    anchors?: boolean;\n    bold?: boolean;\n    italic?: boolean;\n    strikethrough?: boolean;\n    blockquotes?: boolean;\n    code?: boolean;\n    inlineCode?: boolean;\n    links?: boolean;\n    images?: boolean;\n    lists?: boolean;\n    taskLists?: boolean;\n    tables?: boolean;\n    footnotes?: boolean;\n    lineBreaks?: boolean;\n    horizontalRules?: boolean;\n}\n\ninterface RenderMarkdownProps {\n    children: string;\n    className?: string;\n    features?: Partial<MarkdownFeatures>;\n}\n\nconst defaultFeatures: MarkdownFeatures = {\n    headings: true,\n    anchors: true,\n    bold: true,\n    italic: true,\n    strikethrough: true,\n    blockquotes: true,\n    code: true,\n    inlineCode: true,\n    links: true,\n    images: true,\n    lists: true,\n    taskLists: true,\n    tables: true,\n    footnotes: true,\n    lineBreaks: true,\n    horizontalRules: true,\n};\n\nconst ALLOWED_TAGS = [\n    'p', 'div', 'span', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n    'ul', 'ol', 'li', 'blockquote', 'pre', 'code', 'em',\n    'strong', 'del', 'a', 'img', 'br', 'hr', 'table', 'thead',\n    'tbody', 'tr', 'th', 'td', 'sup', 'input'\n];\n\nconst ALLOWED_ATTR = [\n    'class', 'id', 'href', 'target', 'rel', 'src', 'alt',\n    'title', 'type', 'checked', 'disabled'\n];\n\nexport const RenderMarkdown: React.FC<RenderMarkdownProps> = ({\n                                                                  children,\n                                                                  className = '',\n                                                                  features = defaultFeatures\n                                                              }) => {\n    // Use useEffect to ensure DOMPurify is only configured on client-side\n    useEffect(() => {\n        if (typeof window !== 'undefined' && typeof DOMPurify?.setConfig === 'function') {\n            DOMPurify.setConfig({\n                ADD_TAGS: ALLOWED_TAGS,\n                ADD_ATTR: ALLOWED_ATTR,\n                ALLOW_DATA_ATTR: false,\n                USE_PROFILES: { html: true },\n                FORBID_TAGS: ['script', 'style', 'iframe', 'frame', 'object', 'embed', 'form'],\n                FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout', 'style'],\n                SANITIZE_DOM: true,\n                KEEP_CONTENT: true\n            });\n        }\n    }, []);\n\n    const mergedFeatures = { ...defaultFeatures, ...features };\n\n    const escapeHtml = (unsafe: string): string => {\n        return unsafe\n            .replace(/&/g, \"&amp;\")\n            .replace(/</g, \"&lt;\")\n            .replace(/>/g, \"&gt;\")\n            .replace(/\"/g, \"&quot;\")\n            .replace(/'/g, \"&#039;\");\n    };\n\n    const processCodeBlocks = (input: string): string => {\n        if (!mergedFeatures.code) return input;\n        return input.replace(/```([a-z]*)\\n([\\s\\S]*?)\\n```/g, (_, lang, code) => {\n            const escapedCode = escapeHtml(code.trim());\n            return `<pre class=\"bg-muted p-4 rounded-md overflow-x-auto my-4\"><code class=\"language-${escapeHtml(lang)}\">${escapedCode}</code></pre>`;\n        });\n    };\n\n    const processInlineCode = (input: string): string => {\n        if (!mergedFeatures.inlineCode) return input;\n        return input.replace(/`([^`]+)`/g, (_, code) =>\n            `<code class=\"bg-muted px-1.5 py-0.5 rounded text-sm font-mono\">${escapeHtml(code)}</code>`\n        );\n    };\n\n    const processLists = (input: string): string => {\n        if (!mergedFeatures.lists) return input;\n\n        // First, process task lists separately\n        if (mergedFeatures.taskLists) {\n            // Replace task list items with a special marker to be processed later\n            input = input.replace(/^(\\s*)-\\s\\[([ xX])\\]\\s(.+)$/gm, (match, spaces, checked, content) => {\n                const isChecked = checked.toLowerCase() === 'x';\n                const indentation = spaces.length;\n                const escapedContent = escapeHtml(content);\n\n                // Use a special delimiter that won't appear in normal content\n                return `__TASK_ITEM_${indentation}_${isChecked ? 'CHECKED' : 'UNCHECKED'}_${escapedContent}__`;\n            });\n        }\n\n        let inList = false;\n        let listType = '';\n        let depth = 0;\n\n        // Process regular lists\n        let processedContent = input.split('\\n').map(line => {\n            // Skip task items for now (they'll be processed later)\n            if (line.includes('__TASK_ITEM_')) {\n                return line;\n            }\n\n            // Ordered list\n            const orderedMatch = line.match(/^(\\s*)\\d+\\.\\s(.+)/);\n            if (orderedMatch) {\n                const [, spaces, content] = orderedMatch;\n                const currentDepth = spaces.length / 2;\n                const escapedContent = escapeHtml(content);\n\n                if (!inList || listType !== 'ol') {\n                    inList = true;\n                    listType = 'ol';\n                    depth = currentDepth;\n                    return `<ol class=\"list-decimal list-inside space-y-2 my-4\">\\n<li>${escapedContent}</li>`;\n                }\n\n                if (currentDepth > depth) {\n                    depth = currentDepth;\n                    return `<ol class=\"list-decimal list-inside ml-4 space-y-1\">\\n<li>${escapedContent}</li>`;\n                }\n\n                if (currentDepth < depth) {\n                    depth = currentDepth;\n                    return `</ol>\\n<li>${escapedContent}</li>`;\n                }\n\n                return `<li>${escapedContent}</li>`;\n            }\n\n            // Unordered list\n            const unorderedMatch = line.match(/^(\\s*)-\\s(.+)/);\n            if (unorderedMatch) {\n                const [, spaces, content] = unorderedMatch;\n                const currentDepth = spaces.length / 2;\n                const escapedContent = escapeHtml(content);\n\n                if (!inList || listType !== 'ul') {\n                    inList = true;\n                    listType = 'ul';\n                    depth = currentDepth;\n                    return `<ul class=\"list-disc list-inside space-y-2 my-4\">\\n<li>${escapedContent}</li>`;\n                }\n\n                if (currentDepth > depth) {\n                    depth = currentDepth;\n                    return `<ul class=\"list-disc list-inside ml-4 space-y-1\">\\n<li>${escapedContent}</li>`;\n                }\n\n                if (currentDepth < depth) {\n                    depth = currentDepth;\n                    return `</ul>\\n<li>${escapedContent}</li>`;\n                }\n\n                return `<li>${escapedContent}</li>`;\n            }\n\n            if (inList) {\n                inList = false;\n                return `${listType === 'ol' ? '</ol>' : '</ul>'}\\n${escapeHtml(line)}`;\n            }\n\n            return escapeHtml(line);\n        }).join('\\n');\n\n        // Now, process task list items\n        if (mergedFeatures.taskLists) {\n            processedContent = processedContent.replace(/__TASK_ITEM_(\\d+)_(CHECKED|UNCHECKED)_(.+?)__/g,\n                (_, indentation, checkedState, content) => {\n                    const isChecked = checkedState === 'CHECKED';\n                    const indent = parseInt(indentation);\n                    const marginLeft = indent > 0 ? `ml-${indent * 4}` : '';\n\n                    return `\n            <div class=\"flex items-center gap-2 my-2 task-list-item ${marginLeft}\">\n              <input type=\"checkbox\" ${isChecked ? 'checked' : ''} disabled \n                class=\"form-checkbox h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary\" />\n              <span${isChecked ? ' class=\"line-through text-muted-foreground\"' : ''}>${content}</span>\n            </div>\n          `;\n                }\n            );\n        }\n\n        return processedContent;\n    };\n\n    // Improved line breaks handling\n    const processLineBreaks = (input: string): string => {\n        if (!mergedFeatures.lineBreaks) return input;\n\n        // Handle single line breaks (when a line ends with two spaces)\n        input = input.replace(/ {2}\\n/g, '<br />');\n\n        // Handle paragraphs (double line breaks)\n        input = input.replace(/\\n\\n/g, '</p><p class=\"leading-7 mb-4\">');\n\n        return input;\n    };\n\n    const renderMarkdown = (text: string): string => {\n        let html = text;\n\n        // Process blocks in specific order\n        html = processCodeBlocks(html);\n        html = processLists(html);\n\n        // Images - Process before other elements to prevent interference\n        if (mergedFeatures.images) {\n            html = html.replace(/!\\[(.*?)\\]\\(([^)]+)(?:\\s+\"([^\"]+)\")?\\)/g, (_, alt, src, title) => {\n                const titleAttr = title ? ` title=\"${escapeHtml(title)}\"` : '';\n                const escapedAlt = escapeHtml(alt || '');\n                return `<img src=\"${src}\" alt=\"${escapedAlt}\"${titleAttr} class=\"max-w-full h-auto rounded-lg my-4\" loading=\"lazy\" />`;\n            });\n        }\n\n        // Headers (with anchor links)\n        if (mergedFeatures.headings) {\n            html = html.replace(/^(#{1,6})\\s(.+)$/gm, (_, level, content) => {\n                const headingLevel = level.length;\n                const escapedContent = escapeHtml(content);\n                const id = content.toLowerCase().replace(/[^\\w]+/g, '-');\n\n                // Apply different styling based on heading level\n                let headingClasses = 'group relative flex items-center gap-2';\n\n                switch(headingLevel) {\n                    case 1:\n                        headingClasses += ' text-3xl font-bold mt-8 mb-4';\n                        break;\n                    case 2:\n                        headingClasses += ' text-2xl font-semibold mt-6 mb-3';\n                        break;\n                    case 3:\n                        headingClasses += ' text-xl font-medium mt-5 mb-3';\n                        break;\n                    case 4:\n                        headingClasses += ' text-lg font-medium mt-4 mb-2';\n                        break;\n                    case 5:\n                        headingClasses += ' text-base font-medium mt-3 mb-2';\n                        break;\n                    case 6:\n                        headingClasses += ' text-sm font-medium mt-3 mb-2';\n                        break;\n                }\n\n                return `<h${headingLevel} id=\"${id}\" class=\"${headingClasses}\">\n          ${escapedContent}\n          ${mergedFeatures.anchors ? `\n            <a href=\"#${id}\" class=\"opacity-0 group-hover:opacity-100 text-muted-foreground transition-opacity\">\n              <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n                <path d=\"M7.5 4H5.75A3.75 3.75 0 002 7.75v.5a3.75 3.75 0 003.75 3.75h1.5m-1.5-4h3m1.5-4h1.75A3.75 3.75 0 0114 7.75v.5a3.75 3.75 0 01-3.75 3.75H8.5\"/>\n              </svg>\n            </a>\n          ` : ''}\n        </h${headingLevel}>`;\n            });\n        }\n\n        // Apply line break processing\n        html = processLineBreaks(html);\n\n        // Blockquotes\n        if (mergedFeatures.blockquotes) {\n            html = html.replace(/^(>+)\\s(.+)$/gm, (_, level, content) => {\n                const depth = level.length;\n                const padding = (depth - 1) * 1.5;\n                return `<blockquote class=\"pl-4 py-2 border-l-2 border-border ml-${padding} italic text-muted-foreground my-4\">${escapeHtml(content)}</blockquote>`;\n            });\n        }\n\n        // Tables - Completely rewritten for better handling\n        if (mergedFeatures.tables) {\n            // First, identify and process entire tables\n            html = html.replace(/(\\|.+\\|\\n)+/g, (tableBlock) => {\n                const rows = tableBlock.trim().split('\\n');\n\n                // Extract the header row\n                const headerRow = rows[0];\n                const headerCells = headerRow\n                    .split('|')\n                    .slice(1, -1)\n                    .map(cell => escapeHtml(cell.trim()));\n\n                // Check if we have alignment row\n                const isAlignmentRow = rows[1] && rows[1].includes('---');\n\n                // Process body rows\n                const bodyRows = rows.slice(isAlignmentRow ? 2 : 1);\n\n                // Create table HTML\n                let tableHtml = '<table class=\"w-full border-collapse border-2 rounded-md my-6 mx-auto\">\\n';\n\n                // Add header\n                tableHtml += '<thead class=\"bg-muted/50\">\\n<tr>\\n';\n                headerCells.forEach(cell => {\n                    tableHtml += `<th class=\"border px-4 py-2 text-left font-medium\">${cell}</th>\\n`;\n                });\n                tableHtml += '</tr>\\n</thead>\\n';\n\n                // Add body\n                tableHtml += '<tbody>\\n';\n                bodyRows.forEach(row => {\n                    const cells = row\n                        .split('|')\n                        .slice(1, -1)\n                        .map(cell => escapeHtml(cell.trim()));\n\n                    tableHtml += '<tr class=\"hover:bg-muted/20\">\\n';\n                    cells.forEach(cell => {\n                        tableHtml += `<td class=\"border px-4 py-2\">${cell}</td>\\n`;\n                    });\n                    tableHtml += '</tr>\\n';\n                });\n                tableHtml += '</tbody>\\n</table>';\n\n                return tableHtml;\n            });\n        }\n\n        // Process inline elements\n        html = processInlineCode(html);\n\n        // Bold\n        if (mergedFeatures.bold) {\n            html = html.replace(/\\*\\*(.*?)\\*\\*/g, (_, content) => `<strong>${escapeHtml(content)}</strong>`);\n        }\n\n        // Italic\n        if (mergedFeatures.italic) {\n            html = html.replace(/\\b_(.*?)_\\b/g, (_, content) => `<em>${escapeHtml(content)}</em>`);\n        }\n\n        // Strikethrough\n        if (mergedFeatures.strikethrough) {\n            html = html.replace(/~~(.*?)~~/g, (_, content) => `<del>${escapeHtml(content)}</del>`);\n        }\n\n        // Links\n        if (mergedFeatures.links) {\n            html = html.replace(\n                /\\[([^\\]]+)\\]\\(([^)\"]+)(?:\\s+\"([^\"]+)\")?\\)/g,\n                (_, text, url, title) => {\n                    const titleAttr = title ? ` title=\"${escapeHtml(title)}\"` : '';\n                    return `<a href=\"${url}\"${titleAttr} class=\"text-primary hover:underline inline-flex items-center gap-1\" target=\"_blank\" rel=\"noopener noreferrer\">\n            ${escapeHtml(text)}\n            <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-external-link\">\n              <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"></path>\n              <polyline points=\"15 3 21 3 21 9\"></polyline>\n              <line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\"></line>\n            </svg>\n          </a>`;\n                }\n            );\n        }\n\n        // Footnotes\n        if (mergedFeatures.footnotes) {\n            html = html.replace(/\\[\\^(\\d+)\\](?!:)/g, (_, num) =>\n                `<sup><a href=\"#fn${escapeHtml(num)}\" id=\"fnref${escapeHtml(num)}\">[${escapeHtml(num)}]</a></sup>`\n            );\n            html = html.replace(/\\[\\^(\\d+)\\]:\\s*(.+)$/gm, (_, num, content) => {\n                return `<div id=\"fn${escapeHtml(num)}\" class=\"text-sm text-muted-foreground mt-8 pt-2 border-t\">\n          ${escapeHtml(num)}. ${escapeHtml(content)}\n          <a href=\"#fnref${escapeHtml(num)}\" class=\"text-primary\">↩</a>\n        </div>`;\n            });\n        }\n\n        // Horizontal rules\n        if (mergedFeatures.horizontalRules) {\n            html = html.replace(/^---$/gm, '<hr class=\"my-6 border-t border-border\">');\n        }\n\n        // Wrap adjacent paragraphs\n        html = html.replace(/([^\\n]+?)(?:\\n\\n|$)/g, (_, content) => {\n            if (\n                content.startsWith('<') || // Skip if content starts with HTML tag\n                content.trim() === '' // Skip empty lines\n            ) {\n                return content;\n            }\n            return `<p class=\"leading-7 mb-4\">${content}</p>\\n`;\n        });\n\n        // Sanitize the final HTML\n        try {\n            if (typeof DOMPurify?.sanitize === 'function') {\n                return DOMPurify.sanitize(html, {\n                    ALLOWED_TAGS,\n                    ALLOWED_ATTR,\n                    ALLOW_DATA_ATTR: false,\n                    USE_PROFILES: { html: true },\n                    RETURN_DOM_FRAGMENT: false,\n                    RETURN_DOM: false,\n                    SANITIZE_DOM: true\n                });\n            }\n            // Fallback for SSR or if DOMPurify is not available\n            return html;\n        } catch {\n            // Return unsanitized HTML during SSR, client will sanitize on hydration\n            console.debug('DOMPurify sanitization skipped');\n            return html;\n        }\n    };\n\n    return (\n        <div\n            className={`prose max-w-none prose-img:my-4 prose-headings:mt-6 prose-headings:mb-4 prose-p:mb-4 prose-pre:my-4 prose-blockquote:my-4 ${className}`}\n            dangerouslySetInnerHTML={{ __html: renderMarkdown(children) }}\n            suppressHydrationWarning\n        />\n    );\n};\n\n// Instead of configuring DOMPurify globally, we'll only use it in the component function\n// This helps prevent SSR issues\n\n// Helper type for selection formatting actions\ninterface SelectionAction {\n    icon: React.ReactNode;\n    label: string;\n    prefix: string;\n    suffix: string;\n    shortcut?: string;\n}\n\ninterface MarkdownAction {\n    label: string;\n    icon: React.ReactNode;\n    prefix: string;\n    suffix: string;\n    shortcut?: string;\n    group?: string;\n}\n\ninterface HeadingAction extends SelectionAction {\n    level: number;\n}\n\ninterface EditorProps {\n    initialValue?: string;\n    onChange?: (value: string) => void;\n    onSave?: (value: string) => void;\n    placeholder?: string;\n    className?: string;\n    autoFocus?: boolean;\n    maxLength?: number;\n    height?: string;\n    resizable?: boolean;\n    features?: MarkdownFeatures;\n}\n\nconst HEADING_ACTIONS: HeadingAction[] = [\n    {\n        icon: <Heading1 className=\"w-4 h-4\" />,\n        label: 'Heading 1',\n        prefix: '# ',\n        suffix: '',\n        level: 1,\n        shortcut: '⌘+1'\n    },\n    {\n        icon: <Heading2 className=\"w-4 h-4\" />,\n        label: 'Heading 2',\n        prefix: '## ',\n        suffix: '',\n        level: 2,\n        shortcut: '⌘+2'\n    },\n    {\n        icon: <Heading3 className=\"w-4 h-4\" />,\n        label: 'Heading 3',\n        prefix: '### ',\n        suffix: '',\n        level: 3,\n        shortcut: '⌘+3'\n    },\n    {\n        icon: <Heading4 className=\"w-4 h-4\" />,\n        label: 'Heading 4',\n        prefix: '#### ',\n        suffix: '',\n        level: 4,\n        shortcut: '⌘+4'\n    },\n    {\n        icon: <Heading5 className=\"w-4 h-4\" />,\n        label: 'Heading 5',\n        prefix: '##### ',\n        suffix: '',\n        level: 5,\n        shortcut: '⌘+5'\n    }\n];\n\nconst SELECTION_ACTIONS: SelectionAction[] = [\n    {\n        icon: <Bold className=\"w-4 h-4\" />,\n        label: 'Bold',\n        prefix: '**',\n        suffix: '**',\n        shortcut: '⌘+B'\n    },\n    {\n        icon: <Italic className=\"w-4 h-4\" />,\n        label: 'Italic',\n        prefix: '_',\n        suffix: '_',\n        shortcut: '⌘+I'\n    },\n    {\n        icon: <Code className=\"w-4 h-4\" />,\n        label: 'Inline Code',\n        prefix: '`',\n        suffix: '`',\n        shortcut: '⌘+K'\n    },\n    {\n        icon: <Link2 className=\"w-4 h-4\" />,\n        label: 'Link',\n        prefix: '[',\n        suffix: '](url)',\n        shortcut: '⌘+L'\n    },\n    {\n        icon: <Quote className=\"w-4 h-4\" />,\n        label: 'Quote',\n        prefix: '> ',\n        suffix: '',\n    },\n    {\n        icon: <CheckSquare className=\"w-4 h-4\" />,\n        label: 'Task',\n        prefix: '- [ ] ',\n        suffix: '',\n    },\n    {\n        icon: <Check className=\"w-4 h-4\" />,\n        label: 'Done Task',\n        prefix: '- [x] ',\n        suffix: '',\n    }\n];\n\nconst DEFAULT_ACTIONS: MarkdownAction[] = [\n    {\n        label: 'Bold',\n        icon: <Bold className=\"w-4 h-4\" />,\n        prefix: '**',\n        suffix: '**',\n        shortcut: '⌘+B',\n        group: 'format'\n    },\n    {\n        label: 'Italic',\n        icon: <Italic className=\"w-4 h-4\" />,\n        prefix: '_',\n        suffix: '_',\n        shortcut: '⌘+I',\n        group: 'format'\n    },\n    {\n        label: 'Heading',\n        icon: <Heading2 className=\"w-4 h-4\" />,\n        prefix: '## ',\n        suffix: '',\n        shortcut: '⌘+H',\n        group: 'format'\n    },\n    {\n        label: 'List',\n        icon: <List className=\"w-4 h-4\" />,\n        prefix: '- ',\n        suffix: '',\n        group: 'format'\n    },\n    {\n        label: 'Ordered List',\n        icon: <ListOrdered className=\"w-4 h-4\" />,\n        prefix: '1. ',\n        suffix: '',\n        group: 'format'\n    },\n    {\n        label: 'Task List',\n        icon: <CheckSquare className=\"w-4 h-4\" />,\n        prefix: '- [ ] ',\n        suffix: '',\n        group: 'format'\n    },\n    {\n        label: 'Line Break',\n        icon: <Text className=\"w-4 h-4\" />,\n        prefix: '  \\n',\n        suffix: '',\n        shortcut: '⇧↵',\n        group: 'format'\n    },\n    {\n        label: 'Link',\n        icon: <Link2 className=\"w-4 h-4\" />,\n        prefix: '[',\n        suffix: '](url)',\n        group: 'insert'\n    },\n    {\n        label: 'Quote',\n        icon: <Quote className=\"w-4 h-4\" />,\n        prefix: '> ',\n        suffix: '',\n        group: 'insert'\n    },\n    {\n        label: 'Code',\n        icon: <FileCode className=\"w-4 h-4\" />,\n        prefix: '```\\n',\n        suffix: '\\n```',\n        group: 'insert'\n    },\n    {\n        label: 'Image',\n        icon: <Image className=\"w-4 h-4\" />,\n        prefix: '![',\n        suffix: '](url)',\n        group: 'insert'\n    },\n    {\n        label: 'Table',\n        icon: <Table className=\"w-4 h-4\" />,\n        prefix: '| Header 1 | Header 2 |\\n|----------|----------|\\n| Cell 1   | Cell 2   |',\n        suffix: '',\n        group: 'insert'\n    }\n];\n\nexport const MarkdownEditor: React.FC<EditorProps> = ({\n                                                          initialValue = '',\n                                                          onChange,\n                                                          onSave,\n                                                          placeholder = 'Start writing...',\n                                                          className = '',\n                                                          autoFocus = false,\n                                                          maxLength,\n                                                          height = '400px',\n                                                          resizable = false,\n                                                          features = defaultFeatures,\n                                                      }) => {\n    const [content, setContent] = useState(initialValue);\n    const [history, setHistory] = useState<string[]>([initialValue]);\n    const [historyIndex, setHistoryIndex] = useState(0);\n    const [activeTab, setActiveTab] = useState<string>(\"edit\");\n    const [wordCount, setWordCount] = useState(0);\n    const [charCount, setCharCount] = useState(0);\n    const [selectionState, setSelectionState] = useState({\n        start: 0,\n        end: 0,\n        text: '',\n        isSelecting: false,\n        position: { x: 0, y: 0 }\n    });\n    const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n    useEffect(() => {\n        if (initialValue !== content) {\n            setContent(initialValue);\n            setHistory([initialValue]);\n            setHistoryIndex(0);\n        }\n    }, [initialValue]);\n\n    useEffect(() => {\n        // Update word count\n        const words = content.trim() ? content.trim().split(/\\s+/).length : 0;\n        setWordCount(words);\n\n        // Update character count\n        setCharCount(content.length);\n    }, [content]);\n\n    const updateContent = useCallback((newContent: string) => {\n        if (newContent === content) return;\n\n        setContent(newContent);\n        onChange?.(newContent);\n\n        // Update history\n        const newHistory = [...history.slice(0, historyIndex + 1), newContent];\n        setHistory(newHistory);\n        setHistoryIndex(historyIndex + 1);\n    }, [onChange, historyIndex, content, history]);\n\n    const insertMarkdown = useCallback((prefix: string, suffix: string) => {\n        const textarea = textareaRef.current;\n        if (!textarea) return;\n\n        const start = textarea.selectionStart;\n        const end = textarea.selectionEnd;\n        const selectedText = content.substring(start, end);\n        const newContent =\n            content.substring(0, start) +\n            prefix +\n            selectedText +\n            suffix +\n            content.substring(end);\n\n        updateContent(newContent);\n\n        requestAnimationFrame(() => {\n            textarea.focus();\n            textarea.setSelectionRange(\n                start + prefix.length,\n                end + prefix.length\n            );\n        });\n    }, [content, updateContent]);\n\n    const undo = () => {\n        if (historyIndex > 0) {\n            setHistoryIndex(prev => prev - 1);\n            setContent(history[historyIndex - 1]);\n            onChange?.(history[historyIndex - 1]);\n        }\n    };\n\n    const redo = () => {\n        if (historyIndex < history.length - 1) {\n            setHistoryIndex(prev => prev + 1);\n            setContent(history[historyIndex + 1]);\n            onChange?.(history[historyIndex + 1]);\n        }\n    };\n\n    const handleKeyDown = (e: React.KeyboardEvent) => {\n        // Check for selection toolbar shortcuts\n        if (e.metaKey || e.ctrlKey) {\n            // Handle heading shortcuts\n            if (e.key >= \"1\" && e.key <= \"5\") {\n                e.preventDefault();\n                const headingLevel = parseInt(e.key);\n                const headingAction = HEADING_ACTIONS.find(h => h.level === headingLevel);\n                if (headingAction) {\n                    insertMarkdown(headingAction.prefix, headingAction.suffix);\n                }\n                return;\n            }\n\n            switch (e.key.toLowerCase()) {\n                case 'b':\n                    e.preventDefault();\n                    insertMarkdown('**', '**');\n                    break;\n                case 'i':\n                    e.preventDefault();\n                    insertMarkdown('_', '_');\n                    break;\n                case 'k':\n                    e.preventDefault();\n                    insertMarkdown('`', '`');\n                    break;\n                case 'l':\n                    e.preventDefault();\n                    insertMarkdown('[', '](url)');\n                    break;\n                case 'h':\n                    e.preventDefault();\n                    insertMarkdown('## ', '');\n                    break;\n                case 'z':\n                    e.preventDefault();\n                    if (e.shiftKey) {\n                        redo();\n                    } else {\n                        undo();\n                    }\n                    break;\n                case 's':\n                    e.preventDefault();\n                    onSave?.(content);\n                    break;\n            }\n        } else if (e.shiftKey && e.key === 'Enter') {\n            e.preventDefault();\n            insertMarkdown('  \\n', '');\n        }\n    };\n\n    const handleSelection = () => {\n        const textarea = textareaRef.current;\n        if (!textarea) return;\n\n        const start = textarea.selectionStart;\n        const end = textarea.selectionEnd;\n\n        if (start !== end) {\n            // There is a selection\n            const selectedText = content.substring(start, end);\n\n            // Calculate position for the floating toolbar\n            // This is more complex in practice and might need adjustment based on scroll position\n            const selectionRect = textarea.getBoundingClientRect();\n            const lineHeight = parseInt(getComputedStyle(textarea).lineHeight);\n\n            // Get the position of the cursor relative to the textarea\n            const textBeforeSelection = content.substring(0, start);\n            const linesBeforeSelection = textBeforeSelection.split('\\n').length;\n\n            // Approximate position calculation\n            const position = {\n                x: selectionRect.left + 50, // Arbitrary offset\n                y: selectionRect.top + linesBeforeSelection * lineHeight - textarea.scrollTop\n            };\n\n            setSelectionState({\n                start,\n                end,\n                text: selectedText,\n                isSelecting: true,\n                position\n            });\n        } else {\n            // No selection\n            setSelectionState(prev => ({\n                ...prev,\n                isSelecting: false\n            }));\n        }\n    };\n\n    const handleSelectionAction = (action: SelectionAction) => {\n        if (selectionState.isSelecting && textareaRef.current) {\n            const { start, end, text } = selectionState;\n\n            const newContent =\n                content.substring(0, start) +\n                action.prefix +\n                text +\n                action.suffix +\n                content.substring(end);\n\n            updateContent(newContent);\n\n            // Reset selection state\n            setSelectionState(prev => ({\n                ...prev,\n                isSelecting: false\n            }));\n\n            // Focus back on textarea and set cursor position\n            requestAnimationFrame(() => {\n                if (textareaRef.current) {\n                    textareaRef.current.focus();\n                    textareaRef.current.setSelectionRange(\n                        start + action.prefix.length,\n                        end + action.prefix.length\n                    );\n                }\n            });\n        }\n    };\n\n    const groupedActions = DEFAULT_ACTIONS.reduce((acc, action) => {\n        const group = action.group || 'other';\n        if (!acc[group]) acc[group] = [];\n        acc[group].push(action);\n        return acc;\n    }, {} as Record<string, MarkdownAction[]>);\n\n    return (\n        <Card className={`w-full ${className} overflow-hidden shadow-md rounded-lg`} style={{ height }}>\n            <CardContent className=\"p-0 h-full flex flex-col\">\n                <Tabs\n                    value={activeTab}\n                    onValueChange={setActiveTab}\n                    className=\"flex-1 flex flex-col\"\n                >\n                    <div className=\"border-b flex items-center gap-1 p-2 bg-muted/50\">\n                        <div className=\"flex items-center gap-1\">\n                            <TooltipProvider>\n                                <Tooltip>\n                                    <TooltipTrigger asChild>\n                                        <Button\n                                            variant=\"ghost\"\n                                            size=\"sm\"\n                                            onClick={undo}\n                                            disabled={historyIndex === 0}\n                                        >\n                                            <RotateCcw className=\"w-4 h-4\" />\n                                        </Button>\n                                    </TooltipTrigger>\n                                    <TooltipContent>Undo (⌘Z)</TooltipContent>\n                                </Tooltip>\n                            </TooltipProvider>\n\n                            <TooltipProvider>\n                                <Tooltip>\n                                    <TooltipTrigger asChild>\n                                        <Button\n                                            variant=\"ghost\"\n                                            size=\"sm\"\n                                            onClick={redo}\n                                            disabled={historyIndex === history.length - 1}\n                                        >\n                                            <RotateCw className=\"w-4 h-4\" />\n                                        </Button>\n                                    </TooltipTrigger>\n                                    <TooltipContent>Redo (⌘⇧Z)</TooltipContent>\n                                </Tooltip>\n                            </TooltipProvider>\n\n                            <div className=\"w-px h-4 bg-border mx-1\" />\n\n                            {/* Headings Dropdown */}\n                            <DropdownMenu>\n                                <DropdownMenuTrigger asChild>\n                                    <Button variant=\"ghost\" size=\"sm\" className=\"flex items-center gap-1\">\n                                        <Heading2 className=\"w-4 h-4\" />\n                                        <span>Headings</span>\n                                        <ChevronDown className=\"ml-1 w-3 h-3\" />\n                                    </Button>\n                                </DropdownMenuTrigger>\n                                <DropdownMenuContent>\n                                    {HEADING_ACTIONS.map((action) => (\n                                        <DropdownMenuItem\n                                            key={action.label}\n                                            onClick={() => insertMarkdown(action.prefix, action.suffix)}\n                                        >\n                                            <span className=\"mr-2\">{action.icon}</span>\n                                            <span>{action.label}</span>\n                                            {action.shortcut && (\n                                                <span className=\"ml-auto text-xs text-muted-foreground\">\n                          {action.shortcut}\n                        </span>\n                                            )}\n                                        </DropdownMenuItem>\n                                    ))}\n                                </DropdownMenuContent>\n                            </DropdownMenu>\n\n                            {/* Other dropdowns */}\n                            {Object.entries(groupedActions).map(([group, actions]) => (\n                                <DropdownMenu key={group}>\n                                    <DropdownMenuTrigger asChild>\n                                        <Button variant=\"ghost\" size=\"sm\" className=\"capitalize flex items-center gap-1\">\n                                            {group === 'format' ? <Bold className=\"w-4 h-4\" /> : <FileCode className=\"w-4 h-4\" />}\n                                            <span>{group}</span>\n                                            <ChevronDown className=\"ml-1 w-3 h-3\" />\n                                        </Button>\n                                    </DropdownMenuTrigger>\n                                    <DropdownMenuContent>\n                                        {actions.map((action) => (\n                                            <DropdownMenuItem\n                                                key={action.label}\n                                                onClick={() => insertMarkdown(action.prefix, action.suffix)}\n                                            >\n                                                <span className=\"mr-2\">{action.icon}</span>\n                                                <span>{action.label}</span>\n                                                {action.shortcut && (\n                                                    <span className=\"ml-auto text-xs text-muted-foreground\">\n                            {action.shortcut}\n                          </span>\n                                                )}\n                                            </DropdownMenuItem>\n                                        ))}\n                                    </DropdownMenuContent>\n                                </DropdownMenu>\n                            ))}\n                        </div>\n\n                        <div className=\"ml-auto flex items-center gap-2\">\n                            <div className=\"text-xs text-muted-foreground\">\n                                {wordCount} words | {charCount} chars\n                            </div>\n                            <TabsList>\n                                <TabsTrigger value=\"edit\">Edit</TabsTrigger>\n                                <TabsTrigger value=\"preview\">Preview</TabsTrigger>\n                                <TabsTrigger value=\"split\">Split</TabsTrigger>\n                            </TabsList>\n                        </div>\n                    </div>\n\n                    <div className=\"relative flex-1\">\n                        <TabsContent value=\"edit\" className=\"m-0 absolute inset-0 h-full\">\n                            <div className=\"h-full relative\">\n                                <Textarea\n                                    ref={textareaRef}\n                                    value={content}\n                                    onChange={(e) => updateContent(e.target.value)}\n                                    onKeyDown={handleKeyDown}\n                                    onMouseUp={handleSelection}\n                                    onKeyUp={handleSelection}\n                                    placeholder={placeholder}\n                                    className={`h-full w-full resize-none font-mono text-base border-0 rounded-none focus-visible:ring-0 p-4 ${resizable ? 'resize-y' : ''}`}\n                                    autoFocus={autoFocus}\n                                    maxLength={maxLength}\n                                    style={{\n                                        minHeight: '100%',\n                                        maxHeight: resizable ? 'none' : '100%',\n                                        whiteSpace: 'pre-wrap',\n                                        lineHeight: 1.5\n                                    }}\n                                />\n\n                                {/* Selection Toolbar */}\n                                <AnimatePresence>\n                                    {selectionState.isSelecting && (\n                                        <motion.div\n                                            initial={{ opacity: 0, y: -10 }}\n                                            animate={{ opacity: 1, y: 0 }}\n                                            exit={{ opacity: 0, y: -10 }}\n                                            className=\"absolute bg-popover shadow-md rounded-md p-1 flex items-center gap-1 z-50\"\n                                            style={{\n                                                left: selectionState.position.x,\n                                                top: selectionState.position.y - 40,\n                                            }}\n                                        >\n                                            {SELECTION_ACTIONS.map((action) => (\n                                                <TooltipProvider key={action.label}>\n                                                    <Tooltip>\n                                                        <TooltipTrigger asChild>\n                                                            <Button\n                                                                variant=\"ghost\"\n                                                                size=\"sm\"\n                                                                className=\"h-8 w-8 p-0\"\n                                                                onClick={() => handleSelectionAction(action)}\n                                                            >\n                                                                {action.icon}\n                                                            </Button>\n                                                        </TooltipTrigger>\n                                                        <TooltipContent side=\"top\">\n                                                            <p>{action.label} {action.shortcut && `(${action.shortcut})`}</p>\n                                                        </TooltipContent>\n                                                    </Tooltip>\n                                                </TooltipProvider>\n                                            ))}\n                                        </motion.div>\n                                    )}\n                                </AnimatePresence>\n\n                                <DropdownMenu>\n                                    <DropdownMenuTrigger />\n                                    <DropdownMenuContent>\n                                        <DropdownMenuItem onClick={() => document.execCommand('cut')}>\n                                            <Scissors className=\"w-4 h-4 mr-2\" />\n                                            Cut\n                                        </DropdownMenuItem>\n                                        <DropdownMenuItem onClick={() => document.execCommand('copy')}>\n                                            <Copy className=\"w-4 h-4 mr-2\" />\n                                            Copy\n                                        </DropdownMenuItem>\n                                        <DropdownMenuSeparator />\n                                        {DEFAULT_ACTIONS.map((action) => (\n                                            <DropdownMenuItem\n                                                key={action.label}\n                                                onClick={() => insertMarkdown(action.prefix, action.suffix)}\n                                            >\n                                                <span className=\"mr-2\">{action.icon}</span>\n                                                <span>{action.label}</span>\n                                                {action.shortcut && (\n                                                    <span className=\"ml-auto text-xs text-muted-foreground\">\n                            {action.shortcut}\n                          </span>\n                                                )}\n                                            </DropdownMenuItem>\n                                        ))}\n                                    </DropdownMenuContent>\n                                </DropdownMenu>\n                            </div>\n                        </TabsContent>\n\n                        <TabsContent value=\"preview\" className=\"m-0 absolute inset-0 h-full overflow-auto\">\n                            <motion.div\n                                initial={{ opacity: 0 }}\n                                animate={{ opacity: 1 }}\n                                transition={{ duration: 0.3 }}\n                                className=\"p-4\"\n                            >\n                                <RenderMarkdown features={features} className=\"h-full\">\n                                    {content}\n                                </RenderMarkdown>\n                            </motion.div>\n                        </TabsContent>\n\n                        <TabsContent value=\"split\" className=\"m-0 absolute inset-0 h-full overflow-hidden\">\n                            <div className=\"flex h-full\">\n                                <div className=\"w-1/2 h-full border-r relative\">\n                                    <Textarea\n                                        ref={textareaRef}\n                                        value={content}\n                                        onChange={(e) => updateContent(e.target.value)}\n                                        onKeyDown={handleKeyDown}\n                                        onMouseUp={handleSelection}\n                                        onKeyUp={handleSelection}\n                                        placeholder={placeholder}\n                                        className=\"h-full w-full resize-none font-mono text-base border-0 rounded-none focus-visible:ring-0 p-4\"\n                                        style={{\n                                            minHeight: '100%',\n                                            maxHeight: '100%',\n                                            whiteSpace: 'pre-wrap',\n                                            lineHeight: 1.5\n                                        }}\n                                    />\n\n                                    {/* Selection Toolbar for Split View */}\n                                    <AnimatePresence>\n                                        {selectionState.isSelecting && activeTab === 'split' && (\n                                            <motion.div\n                                                initial={{ opacity: 0, y: -10 }}\n                                                animate={{ opacity: 1, y: 0 }}\n                                                exit={{ opacity: 0, y: -10 }}\n                                                className=\"absolute bg-popover shadow-md rounded-md p-1 flex items-center gap-1 z-50\"\n                                                style={{\n                                                    left: selectionState.position.x / 2, // Adjust for split view\n                                                    top: selectionState.position.y - 40,\n                                                }}\n                                            >\n                                                {SELECTION_ACTIONS.map((action) => (\n                                                    <TooltipProvider key={action.label}>\n                                                        <Tooltip>\n                                                            <TooltipTrigger asChild>\n                                                                <Button\n                                                                    variant=\"ghost\"\n                                                                    size=\"sm\"\n                                                                    className=\"h-8 w-8 p-0\"\n                                                                    onClick={() => handleSelectionAction(action)}\n                                                                >\n                                                                    {action.icon}\n                                                                </Button>\n                                                            </TooltipTrigger>\n                                                            <TooltipContent side=\"top\">\n                                                                <p>{action.label} {action.shortcut && `(${action.shortcut})`}</p>\n                                                            </TooltipContent>\n                                                        </Tooltip>\n                                                    </TooltipProvider>\n                                                ))}\n                                            </motion.div>\n                                        )}\n                                    </AnimatePresence>\n                                </div>\n                                <div className=\"w-1/2 h-full overflow-auto\">\n                                    <div className=\"p-4\">\n                                        <RenderMarkdown features={features} className=\"h-full\">\n                                            {content}\n                                        </RenderMarkdown>\n                                    </div>\n                                </div>\n                            </div>\n                        </TabsContent>\n                    </div>\n                </Tabs>\n            </CardContent>\n        </Card>\n    );\n};\n\nexport default MarkdownEditor;"
  },
  {
    "path": "components/UpdateIndicatorBadge.tsx",
    "content": "'use client'\n\nimport React, { useState, useEffect } from 'react'\nimport { AlertCircle } from 'lucide-react'\nimport { UpdateStatus } from '@/lib/types/easypanel'\n\n/**\n * Small badge indicator that shows when an update is available\n * Only shows for admins who have access to the update API\n * Displays on sidebar About tab\n */\nexport const UpdateIndicatorBadge: React.FC = () => {\n    const [updateAvailable, setUpdateAvailable] = useState(false)\n    const [loading, setLoading] = useState(true)\n\n    useEffect(() => {\n        const checkForUpdates = async () => {\n            try {\n                const response = await fetch('/api/system/update-status', {\n                    cache: 'no-store'\n                })\n\n                if (!response.ok) {\n                    // If not admin or error, silently fail\n                    setLoading(false)\n                    return\n                }\n\n                const data: UpdateStatus = await response.json()\n                setUpdateAvailable(data.available)\n            } catch {\n                // Silently fail on error\n            } finally {\n                setLoading(false)\n            }\n        }\n\n        checkForUpdates()\n\n        // Refresh every hour\n        const interval = setInterval(checkForUpdates, 60 * 60 * 1000)\n\n        return () => clearInterval(interval)\n    }, [])\n\n    // Only show if update is available\n    if (!updateAvailable || loading) {\n        return null\n    }\n\n    return (\n        <div className=\"flex items-center gap-1.5 px-2 py-1 bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 rounded-full text-xs font-medium\">\n            <AlertCircle className=\"h-3 w-3 flex-shrink-0\" />\n            <span>Update available</span>\n        </div>\n    )\n}"
  },
  {
    "path": "components/UpdateStatus.tsx",
    "content": "// components/UpdateStatus.tsx (Updated without redundant Easypanel info)\n\n'use client'\n\nimport React, { useState, useEffect } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { Alert, AlertDescription } from '@/components/ui/alert'\nimport { CheckCircle2, AlertCircle, RefreshCw, Download, Zap, AlertTriangle } from 'lucide-react'\nimport { compareVersions } from 'compare-versions'\nimport { appInfo } from \"@/lib/app-info\"\nimport { UpdateStatus as UpdateStatusType } from '@/lib/types/easypanel'\n\ninterface UpdateStatusProps {\n    currentVersion: string\n    onCheckUpdate?: () => Promise<string>\n    onUpdate?: () => Promise<void>\n    checkOnMount?: boolean\n    autoCheckInterval?: number\n    showEasypanelInfo?: boolean\n}\n\ntype UpdateState = 'idle' | 'checking' | 'available' | 'no-update' | 'updating' | 'error'\n\nconst UpdateStatus: React.FC<UpdateStatusProps> = ({\n                                                       currentVersion,\n                                                       onCheckUpdate,\n                                                       onUpdate,\n                                                       checkOnMount = false,\n                                                       autoCheckInterval = 0,\n                                                       showEasypanelInfo = false,\n                                                   }) => {\n    const [state, setState] = useState<UpdateState>('idle')\n    const [latestVersion, setLatestVersion] = useState<string>(currentVersion)\n    const [updateStatus, setUpdateStatus] = useState<UpdateStatusType | null>(null)\n    const [error, setError] = useState<string | null>(null)\n\n    const fetchUpdateStatus = async (): Promise<UpdateStatusType> => {\n        const response = await fetch('/api/system/update-status')\n        if (!response.ok) {\n            throw new Error(`Failed to fetch update status: ${response.statusText}`)\n        }\n        return response.json()\n    }\n\n    const performEasypanelUpdate = async (targetVersion: string): Promise<void> => {\n        const response = await fetch('/api/system/perform-update', {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/json',\n            },\n            body: JSON.stringify({ targetVersion }),\n        })\n\n        if (!response.ok) {\n            const errorData = await response.json()\n            throw new Error(errorData.details || errorData.error || 'Update failed')\n        }\n\n        const result = await response.json()\n        console.log('Update result:', result)\n    }\n\n    const handleCheckUpdate = async () => {\n        setState('checking')\n        setError(null)\n\n        try {\n            let newVersion = currentVersion\n            let status: UpdateStatusType | null = null\n\n            // If we have a custom check function, use it\n            if (onCheckUpdate) {\n                newVersion = await onCheckUpdate()\n            } else {\n                // Use our enhanced API\n                status = await fetchUpdateStatus()\n                newVersion = status.latestVersion\n                setUpdateStatus(status)\n            }\n\n            setLatestVersion(newVersion)\n\n            if (compareVersions(newVersion, currentVersion) > 0) {\n                setState('available')\n            } else {\n                setState('no-update')\n            }\n        } catch (error) {\n            console.error('Failed to check for updates:', error)\n            setError(error instanceof Error ? error.message : 'Failed to check for updates')\n            setState('error')\n        }\n    }\n\n    const handleUpdate = async () => {\n        setState('updating')\n        setError(null)\n\n        try {\n            if (updateStatus?.canAutoUpdate && updateStatus.latestVersion) {\n                // Use Easypanel auto-update\n                await performEasypanelUpdate(updateStatus.latestVersion)\n                // After successful update, the app will restart\n                setTimeout(() => {\n                    window.location.reload()\n                }, 3000)\n            } else if (onUpdate) {\n                // Use custom update function\n                await onUpdate()\n                setState('no-update')\n            } else {\n                throw new Error('No update method available')\n            }\n        } catch (error) {\n            console.error('Update failed:', error)\n            setError(error instanceof Error ? error.message : 'Update failed')\n            setState('error')\n        }\n    }\n\n    useEffect(() => {\n        if (checkOnMount) {\n            handleCheckUpdate()\n        }\n    }, [checkOnMount])\n\n    useEffect(() => {\n        if (autoCheckInterval > 0) {\n            const interval = setInterval(handleCheckUpdate, autoCheckInterval)\n            return () => clearInterval(interval)\n        }\n    }, [autoCheckInterval])\n\n    return (\n        <div className=\"flex flex-col items-center space-y-4\">\n            {/* Error Alert */}\n            {error && (\n                <Alert variant=\"destructive\" className=\"w-full max-w-md\">\n                    <AlertTriangle className=\"h-4 w-4\" />\n                    <AlertDescription>{error}</AlertDescription>\n                </Alert>\n            )}\n\n            {/* Easypanel Configuration Status - Only if showEasypanelInfo is true */}\n            {showEasypanelInfo && updateStatus && (\n                <Alert variant={updateStatus.easypanelConfigured ? \"success\" : \"warning\"} className=\"w-full max-w-md\">\n                    <Zap className=\"h-4 w-4\" />\n                    <AlertDescription>\n                        {updateStatus.easypanelConfigured ? (\n                            <span>\n                                ✅ Easypanel configured - Automatic updates available\n                            </span>\n                        ) : (\n                            <span>\n                                ⚠️ Easypanel not configured - Manual updates only\n                            </span>\n                        )}\n                    </AlertDescription>\n                </Alert>\n            )}\n\n            {/* Update Status Display */}\n            <div className=\"flex flex-col items-center space-y-2\">\n                {state === 'idle' && (\n                    <Button\n                        size=\"sm\"\n                        variant=\"outline\"\n                        onClick={handleCheckUpdate}\n                        className=\"flex items-center gap-1.5\"\n                    >\n                        <RefreshCw className=\"h-3.5 w-3.5\" />\n                        Check for Updates\n                    </Button>\n                )}\n\n                {state === 'checking' && (\n                    <Button\n                        size=\"sm\"\n                        variant=\"outline\"\n                        disabled\n                        className=\"flex items-center gap-1.5\"\n                    >\n                        <RefreshCw className=\"h-3.5 w-3.5 animate-spin\" />\n                        Checking...\n                    </Button>\n                )}\n\n                {state === 'updating' && (\n                    <div className=\"flex flex-col items-center gap-2\">\n                        <Button\n                            size=\"sm\"\n                            variant=\"outline\"\n                            disabled\n                            className=\"flex items-center gap-1.5\"\n                        >\n                            <RefreshCw className=\"h-3.5 w-3.5 animate-spin\" />\n                            Updating...\n                        </Button>\n                        <div className=\"text-xs text-muted-foreground text-center\">\n                            Please wait while the application is being updated.\n                            <br />\n                            The page will reload automatically.\n                        </div>\n                    </div>\n                )}\n\n                {state === 'no-update' && (\n                    <div className=\"flex flex-col items-center gap-1 text-sm\">\n                        <div className=\"flex items-center text-green-500 gap-1\">\n                            <CheckCircle2 className=\"h-4 w-4\" />\n                            <span>Up to date!</span>\n                        </div>\n                    </div>\n                )}\n\n                {state === 'available' && (\n                    <div className=\"flex flex-col items-center gap-2\">\n                        <div className=\"flex items-center text-amber-500 gap-1 text-sm\">\n                            <AlertCircle className=\"h-4 w-4\" />\n                            <span>Update available: v{latestVersion}</span>\n                        </div>\n\n                        {updateStatus?.canAutoUpdate ? (\n                            <Button\n                                size=\"sm\"\n                                variant=\"default\"\n                                onClick={handleUpdate}\n                                className=\"flex items-center gap-1.5 w-full\"\n                            >\n                                <Zap className=\"h-3.5 w-3.5\" />\n                                Auto-Update\n                            </Button>\n                        ) : (\n                            <Button\n                                size=\"sm\"\n                                variant=\"default\"\n                                onClick={() => window.open(`${appInfo['repository']}`, '_blank')}\n                                className=\"flex items-center gap-1.5 w-full\"\n                            >\n                                <Download className=\"h-3.5 w-3.5\" />\n                                Download from GitHub\n                            </Button>\n                        )}\n\n                        <div className=\"text-xs text-muted-foreground mt-1 text-center\">\n                            {updateStatus?.canAutoUpdate ? (\n                                'Automatic update will restart the application'\n                            ) : (\n                                'Manual update required - follow upgrade instructions'\n                            )}\n                        </div>\n                    </div>\n                )}\n\n                {state === 'error' && (\n                    <Button\n                        size=\"sm\"\n                        variant=\"outline\"\n                        onClick={handleCheckUpdate}\n                        className=\"flex items-center gap-1.5\"\n                    >\n                        <RefreshCw className=\"h-3.5 w-3.5\" />\n                        Retry Check\n                    </Button>\n                )}\n            </div>\n        </div>\n    )\n}\n\nexport default UpdateStatus"
  },
  {
    "path": "components/admin/api/PermissionsModal.tsx",
    "content": "'use client'\n\nimport React, { useState } from 'react';\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogFooter,\n    DialogHeader,\n    DialogTitle,\n} from '@/components/ui/dialog';\nimport { Button } from '@/components/ui/button';\nimport { Checkbox } from '@/components/ui/checkbox';\nimport { Label } from '@/components/ui/label';\nimport { Badge } from '@/components/ui/badge';\nimport { API_PERMISSIONS, PERMISSION_GROUPS } from '@/lib/api/permissions';\nimport { ScrollArea } from '@/components/ui/scroll-area';\nimport { Separator } from '@/components/ui/separator';\n\ninterface PermissionsModalProps {\n    open: boolean;\n    onOpenChange: (open: boolean) => void;\n    selectedPermissions: string[];\n    onSave: (permissions: string[]) => void;\n}\n\nexport function PermissionsModal({\n    open,\n    onOpenChange,\n    selectedPermissions,\n    onSave,\n}: PermissionsModalProps) {\n    const [permissions, setPermissions] = useState<string[]>(selectedPermissions);\n\n    // Sync local state when modal opens or selectedPermissions changes\n    React.useEffect(() => {\n        if (open) {\n            setPermissions(selectedPermissions);\n        }\n    }, [open, selectedPermissions]);\n\n    const handleToggle = (permission: string) => {\n        setPermissions(prev =>\n            prev.includes(permission)\n                ? prev.filter(p => p !== permission)\n                : [...prev, permission]\n        );\n    };\n\n    const handleSelectGroup = (group: string[]) => {\n        setPermissions(group);\n    };\n\n    const handleSave = () => {\n        onSave(permissions);\n        onOpenChange(false);\n    };\n\n    const permissionCategories = {\n        'Changelog Operations': [\n            { key: 'CHANGELOG_READ', label: 'Read Entries', description: 'Fetch changelog entries and metadata' },\n            { key: 'CHANGELOG_WRITE', label: 'Write Entries', description: 'Create and update changelog entries' },\n            { key: 'CHANGELOG_PUBLISH', label: 'Publish Entries', description: 'Publish or unpublish entries' },\n            { key: 'CHANGELOG_DELETE', label: 'Delete Entries', description: 'Delete changelog entries' },\n        ],\n        'Project Management': [\n            { key: 'PROJECT_READ', label: 'Read Project', description: 'Read project settings and configuration' },\n            { key: 'PROJECT_WRITE', label: 'Write Project', description: 'Modify project settings' },\n        ],\n        'Analytics': [\n            { key: 'ANALYTICS_READ', label: 'Read Analytics', description: 'Access analytics data and metrics' },\n        ],\n        'Subscribers': [\n            { key: 'SUBSCRIBERS_READ', label: 'Read Subscribers', description: 'List and view subscribers' },\n            { key: 'SUBSCRIBERS_WRITE', label: 'Write Subscribers', description: 'Add, update, or remove subscribers' },\n        ],\n        'Email': [\n            { key: 'EMAIL_SEND', label: 'Send Emails', description: 'Trigger email notifications for entries' },\n        ],\n        'GitHub Integration': [\n            { key: 'GITHUB_READ', label: 'Read GitHub', description: 'Read GitHub integration settings' },\n            { key: 'GITHUB_WRITE', label: 'Write GitHub', description: 'Modify GitHub integration and generate entries' },\n        ],\n        'Tags': [\n            { key: 'TAGS_READ', label: 'Read Tags', description: 'List and view tags' },\n            { key: 'TAGS_WRITE', label: 'Write Tags', description: 'Create, update, or delete tags' },\n        ],\n        'Webhooks': [\n            { key: 'WEBHOOKS_READ', label: 'Read Webhooks', description: 'List webhook configurations (future)' },\n            { key: 'WEBHOOKS_WRITE', label: 'Write Webhooks', description: 'Create, update, or delete webhooks (future)' },\n        ],\n    };\n\n    return (\n        <Dialog open={open} onOpenChange={onOpenChange}>\n            <DialogContent className=\"max-w-2xl max-h-[80vh]\">\n                <DialogHeader>\n                    <DialogTitle>Configure Permissions</DialogTitle>\n                    <DialogDescription>\n                        Select which API operations this key can perform.\n                    </DialogDescription>\n                </DialogHeader>\n\n                <div className=\"space-y-4\">\n                    {/* Quick Select Groups */}\n                    <div>\n                        <Label className=\"text-sm font-medium mb-2 block\">Quick Select</Label>\n                        <div className=\"flex gap-2 flex-wrap\">\n                            <Badge\n                                variant=\"outline\"\n                                className=\"cursor-pointer hover:bg-primary hover:text-primary-foreground\"\n                                onClick={() => handleSelectGroup([...PERMISSION_GROUPS.READ_ONLY])}\n                            >\n                                Read Only\n                            </Badge>\n                            <Badge\n                                variant=\"outline\"\n                                className=\"cursor-pointer hover:bg-primary hover:text-primary-foreground\"\n                                onClick={() => handleSelectGroup([...PERMISSION_GROUPS.CHANGELOG_MANAGER])}\n                            >\n                                Changelog Manager\n                            </Badge>\n                            <Badge\n                                variant=\"outline\"\n                                className=\"cursor-pointer hover:bg-primary hover:text-primary-foreground\"\n                                onClick={() => handleSelectGroup([...PERMISSION_GROUPS.FULL_ACCESS])}\n                            >\n                                Full Access\n                            </Badge>\n                            <Badge\n                                variant=\"outline\"\n                                className=\"cursor-pointer hover:bg-destructive hover:text-destructive-foreground\"\n                                onClick={() => setPermissions([])}\n                            >\n                                Clear All\n                            </Badge>\n                        </div>\n                    </div>\n\n                    <Separator />\n\n                    {/* Permission Categories */}\n                    <ScrollArea className=\"h-[400px] pr-4\">\n                        <div className=\"space-y-6\">\n                            {Object.entries(permissionCategories).map(([category, perms]) => (\n                                <div key={category}>\n                                    <h4 className=\"text-sm font-semibold mb-3\">{category}</h4>\n                                    <div className=\"space-y-3 ml-2\">\n                                        {perms.map(perm => {\n                                            const permValue = API_PERMISSIONS[perm.key as keyof typeof API_PERMISSIONS];\n                                            return (\n                                                <div key={perm.key} className=\"flex items-start space-x-3\">\n                                                    <Checkbox\n                                                        id={perm.key}\n                                                        checked={permissions.includes(permValue)}\n                                                        onCheckedChange={() => handleToggle(permValue)}\n                                                    />\n                                                    <div className=\"flex-1\">\n                                                        <Label\n                                                            htmlFor={perm.key}\n                                                            className=\"text-sm font-medium cursor-pointer\"\n                                                        >\n                                                            {perm.label}\n                                                        </Label>\n                                                        <p className=\"text-xs text-muted-foreground mt-0.5\">\n                                                            {perm.description}\n                                                        </p>\n                                                    </div>\n                                                </div>\n                                            );\n                                        })}\n                                    </div>\n                                </div>\n                            ))}\n                        </div>\n                    </ScrollArea>\n                </div>\n\n                <DialogFooter>\n                    <div className=\"flex items-center justify-between w-full\">\n                        <span className=\"text-sm text-muted-foreground\">\n                            {permissions.length} permission{permissions.length !== 1 ? 's' : ''} selected\n                        </span>\n                        <div className=\"flex gap-2\">\n                            <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n                                Cancel\n                            </Button>\n                            <Button onClick={handleSave}>\n                                Save Permissions\n                            </Button>\n                        </div>\n                    </div>\n                </DialogFooter>\n            </DialogContent>\n        </Dialog>\n    );\n}\n"
  },
  {
    "path": "components/admin/api/Rename.tsx",
    "content": "import React, { useState } from 'react';\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogFooter,\n    DialogHeader,\n    DialogTitle,\n} from '@/components/ui/dialog';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\n\ninterface RenameDialogProps {\n    open: boolean;\n    onOpenChange: (open: boolean) => void;\n    onRename: (newName: string) => Promise<void>;\n    currentName: string;\n}\n\nexport function RenameDialog({\n                                 open,\n                                 onOpenChange,\n                                 onRename,\n                                 currentName,\n                             }: RenameDialogProps) {\n    const [newName, setNewName] = useState(currentName);\n    const [isSubmitting, setIsSubmitting] = useState(false);\n\n    const handleSubmit = async (e: React.FormEvent) => {\n        e.preventDefault();\n        if (!newName.trim() || newName === currentName) return;\n\n        setIsSubmitting(true);\n        try {\n            await onRename(newName);\n            onOpenChange(false);\n        } catch (error) {\n            console.error('Failed to rename API key:', error);\n        } finally {\n            setIsSubmitting(false);\n        }\n    };\n\n    return (\n        <Dialog open={open} onOpenChange={onOpenChange}>\n            <DialogContent>\n                <DialogHeader>\n                    <DialogTitle>Rename API Key</DialogTitle>\n                    <DialogDescription>\n                        Enter a new name for your API key.\n                    </DialogDescription>\n                </DialogHeader>\n                <form onSubmit={handleSubmit}>\n                    <div className=\"grid gap-4 py-4\">\n                        <div className=\"grid gap-2\">\n                            <Label htmlFor=\"new-name\">New Name</Label>\n                            <Input\n                                id=\"new-name\"\n                                value={newName}\n                                onChange={(e) => setNewName(e.target.value)}\n                                placeholder=\"e.g., Production API Key\"\n                            />\n                        </div>\n                    </div>\n                    <DialogFooter>\n                        <Button\n                            type=\"button\"\n                            variant=\"outline\"\n                            onClick={() => onOpenChange(false)}\n                            disabled={isSubmitting}\n                        >\n                            Cancel\n                        </Button>\n                        <Button\n                            type=\"submit\"\n                            disabled={!newName.trim() || newName === currentName || isSubmitting}\n                        >\n                            {isSubmitting ? 'Renaming...' : 'Rename'}\n                        </Button>\n                    </DialogFooter>\n                </form>\n            </DialogContent>\n        </Dialog>\n    );\n}"
  },
  {
    "path": "components/admin/api/sdk-showcase-compact.tsx",
    "content": "import React, { useState } from 'react';\nimport {\n    Tabs,\n    TabsList,\n    TabsTrigger,\n    TabsContent\n} from \"@/components/ui/tabs\";\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle\n} from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport { Copy, Code, CheckCircle } from 'lucide-react';\nimport { toast } from '@/hooks/use-toast';\nimport { cn } from '@/lib/utils';\n\nconst SDKShowcaseCompact = ({ className }: { className?: string }) => {\n    const [copied, setCopied] = useState({\n        react: false,\n        php: false\n    });\n\n    const handleCopy = (text: string, sdk: string) => {\n        navigator.clipboard.writeText(text);\n        setCopied({ ...copied, [sdk]: true });\n\n        toast({\n            title: 'Command Copied',\n            description: 'The installation command has been copied to your clipboard.',\n        });\n\n        setTimeout(() => {\n            setCopied({ ...copied, [sdk]: false });\n        }, 2000);\n    };\n\n    return (\n        <Card className={cn(\"overflow-hidden\", className)}>\n            <CardHeader className=\"pb-3\">\n                <CardTitle className=\"text-base font-medium flex items-center\">\n                    <Code className=\"h-4 w-4 mr-2\" />\n                    SDKs & Tools\n                </CardTitle>\n                <CardDescription className=\"text-xs\">\n                    Official client libraries\n                </CardDescription>\n            </CardHeader>\n\n            <Tabs defaultValue=\"react\" className=\"w-full\">\n                <TabsList className=\"mx-6 grid grid-cols-2 mb-1\">\n                    <TabsTrigger value=\"react\" className=\"text-xs\">React</TabsTrigger>\n                    <TabsTrigger value=\"php\" className=\"text-xs\">PHP</TabsTrigger>\n                </TabsList>\n\n                <TabsContent value=\"react\" className=\"mt-0\">\n                    <CardContent className=\"p-4 pt-4 space-y-3\">\n                        <div className=\"space-y-1\">\n                            <p className=\"text-xs font-medium\">Installation</p>\n                            <div className=\"rounded-md overflow-hidden border\">\n                                <div className=\"flex items-center justify-between px-2 py-1.5 bg-muted/50 border-b\">\n                                    <span className=\"text-xs text-muted-foreground\">npm</span>\n                                    <Button\n                                        variant=\"ghost\"\n                                        size=\"sm\"\n                                        className=\"h-5 w-5 p-0\"\n                                        onClick={() => handleCopy(\"npm install @changerawr/react\", \"react\")}\n                                    >\n                                        {copied.react ? (\n                                            <CheckCircle className=\"h-3 w-3 text-green-500\" />\n                                        ) : (\n                                            <Copy className=\"h-3 w-3\" />\n                                        )}\n                                    </Button>\n                                </div>\n                                <div className=\"p-2 font-mono text-xs\">\n                                    <code>npm install @changerawr/react</code>\n                                </div>\n                            </div>\n                        </div>\n\n                        <div className=\"space-y-1.5\">\n                            <p className=\"text-xs font-medium\">Features</p>\n                            <ul className=\"space-y-1 text-xs text-muted-foreground\">\n                                <li className=\"flex items-start gap-1.5\">\n                                    <CheckCircle className=\"h-3 w-3 text-primary mt-0.5 flex-shrink-0\" />\n                                    <span>TypeScript support</span>\n                                </li>\n                                <li className=\"flex items-start gap-1.5\">\n                                    <CheckCircle className=\"h-3 w-3 text-primary mt-0.5 flex-shrink-0\" />\n                                    <span>React hooks</span>\n                                </li>\n                            </ul>\n                        </div>\n                    </CardContent>\n                </TabsContent>\n\n                <TabsContent value=\"php\" className=\"mt-0\">\n                    <CardContent className=\"p-4 pt-4 space-y-3\">\n                        <div className=\"space-y-1\">\n                            <p className=\"text-xs font-medium\">Installation</p>\n                            <div className=\"rounded-md overflow-hidden border\">\n                                <div className=\"flex items-center justify-between px-2 py-1.5 bg-muted/50 border-b\">\n                                    <span className=\"text-xs text-muted-foreground\">composer</span>\n                                    <Button\n                                        variant=\"ghost\"\n                                        size=\"sm\"\n                                        className=\"h-5 w-5 p-0\"\n                                        onClick={() => handleCopy(\"composer require changerawr/php\", \"php\")}\n                                    >\n                                        {copied.php ? (\n                                            <CheckCircle className=\"h-3 w-3 text-green-500\" />\n                                        ) : (\n                                            <Copy className=\"h-3 w-3\" />\n                                        )}\n                                    </Button>\n                                </div>\n                                <div className=\"p-2 font-mono text-xs\">\n                                    <code>composer require changerawr/php</code>\n                                </div>\n                            </div>\n                        </div>\n\n                        <div className=\"space-y-1.5\">\n                            <p className=\"text-xs font-medium\">Features</p>\n                            <ul className=\"space-y-1 text-xs text-muted-foreground\">\n                                <li className=\"flex items-start gap-1.5\">\n                                    <CheckCircle className=\"h-3 w-3 text-primary mt-0.5 flex-shrink-0\" />\n                                    <span>Lightweight client</span>\n                                </li>\n                                <li className=\"flex items-start gap-1.5\">\n                                    <CheckCircle className=\"h-3 w-3 text-primary mt-0.5 flex-shrink-0\" />\n                                    <span>PHP 7.4+ compatible</span>\n                                </li>\n                            </ul>\n                        </div>\n                    </CardContent>\n                </TabsContent>\n            </Tabs>\n        </Card>\n    );\n}\n\nexport default SDKShowcaseCompact;"
  },
  {
    "path": "components/admin/api/sdk-showcase.tsx",
    "content": "import React, { useState } from 'react';\nimport {\n    Tabs,\n    TabsList,\n    TabsTrigger,\n    TabsContent\n} from \"@/components/ui/tabs\";\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardFooter,\n    CardHeader,\n    CardTitle\n} from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport { Copy, ChevronRight, Code, CheckCircle, FileCode, Braces } from 'lucide-react';\nimport { toast } from '@/hooks/use-toast';\nimport { cn } from '@/lib/utils';\n\nconst SDKShowcase = ({ className }: { className: string }) => {\n    const [copied, setCopied] = useState({\n        react: false,\n        php: false\n    });\n\n    const handleCopy = (text: string, sdk: string) => {\n        navigator.clipboard.writeText(text);\n        setCopied({ ...copied, [sdk]: true });\n\n        toast({\n            title: 'Command Copied',\n            description: 'The installation command has been copied to your clipboard.',\n        });\n\n        setTimeout(() => {\n            setCopied({ ...copied, [sdk]: false });\n        }, 2000);\n    };\n\n    return (\n        <Card className={cn(\"overflow-hidden\", className)}>\n            <CardHeader className=\"pb-2\">\n                <CardTitle className=\"text-lg font-medium flex items-center\">\n                    <Code className=\"h-5 w-5 mr-2\" />\n                    Integrations\n                </CardTitle>\n                <CardDescription>\n                    Official tools to integrate with Changerawr\n                </CardDescription>\n            </CardHeader>\n\n            <Tabs defaultValue=\"react\" className=\"w-full\">\n                <TabsList className=\"mx-6 grid grid-cols-2 mb-1\">\n                    <TabsTrigger value=\"react\">React SDK</TabsTrigger>\n                    <TabsTrigger value=\"php\">PHP SDK</TabsTrigger>\n                </TabsList>\n\n                <TabsContent value=\"react\" className=\"space-y-4 mt-0\">\n                    <CardContent className=\"p-3 pt-6\">\n                        <div className=\"space-y-4\">\n                            <div className=\"flex items-start\">\n                                <div className=\"h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0 mr-3\">\n                                    <Braces className=\"h-5 w-5 text-primary\" />\n                                </div>\n                                <div>\n                                    <h3 className=\"font-medium mb-1\">React Client Library</h3>\n                                    <p className=\"text-sm text-muted-foreground mb-3\">\n                                        Easily integrate Changerawr into your React applications with our official client library.\n                                    </p>\n                                </div>\n                            </div>\n\n                            <div className=\"space-y-1\">\n                                <p className=\"text-xs uppercase font-medium text-muted-foreground\">INSTALLATION</p>\n                                <div className=\"rounded-md overflow-hidden border\">\n                                    <div className=\"flex items-center px-3 py-1.5 bg-muted/50 border-b\">\n                                        <div className=\"flex space-x-1.5\">\n                                            <div className=\"w-2.5 h-2.5 rounded-full bg-muted-foreground/30\"></div>\n                                            <div className=\"w-2.5 h-2.5 rounded-full bg-muted-foreground/30\"></div>\n                                            <div className=\"w-2.5 h-2.5 rounded-full bg-muted-foreground/30\"></div>\n                                        </div>\n                                        <div className=\"ml-auto\">\n                                            <Button\n                                                variant=\"ghost\"\n                                                size=\"sm\"\n                                                className=\"h-6\"\n                                                onClick={() => handleCopy(\"npm install @changerawr/react\", \"react\")}\n                                            >\n                                                {copied.react ? (\n                                                    <CheckCircle className=\"h-3.5 w-3.5 text-green-500\" />\n                                                ) : (\n                                                    <Copy className=\"h-3.5 w-3.5\" />\n                                                )}\n                                            </Button>\n                                        </div>\n                                    </div>\n                                    <div className=\"p-3 font-mono text-xs\">\n                                        <code>npm install @changerawr/react</code>\n                                    </div>\n                                </div>\n                            </div>\n\n                            <div className=\"space-y-2\">\n                                <p className=\"text-xs uppercase font-medium text-muted-foreground\">FEATURES</p>\n                                <ul className=\"space-y-2 text-sm\">\n                                    <li className=\"flex items-start gap-2\">\n                                        <CheckCircle className=\"h-4 w-4 text-primary mt-0.5 flex-shrink-0\" />\n                                        <span>TypeScript support with full type definitions</span>\n                                    </li>\n                                    <li className=\"flex items-start gap-2\">\n                                        <CheckCircle className=\"h-4 w-4 text-primary mt-0.5 flex-shrink-0\" />\n                                        <span>React hooks for all API endpoints</span>\n                                    </li>\n                                    <li className=\"flex items-start gap-2\">\n                                        <CheckCircle className=\"h-4 w-4 text-primary mt-0.5 flex-shrink-0\" />\n                                        <span>Fully tested and maintained API client</span>\n                                    </li>\n                                </ul>\n                            </div>\n                        </div>\n                    </CardContent>\n\n                    <CardFooter className=\"pt-0\">\n                        <Button\n                            className=\"w-full\"\n                            variant=\"outline\"\n                            asChild\n                        >\n                            <a\n                                href=\"https://github.com/changerawr/react\"\n                                target=\"_blank\"\n                                rel=\"noopener noreferrer\"\n                                className=\"flex items-center justify-center\"\n                            >\n                                View Documentation\n                                <ChevronRight className=\"h-4 w-4 ml-1\" />\n                            </a>\n                        </Button>\n                    </CardFooter>\n                </TabsContent>\n\n                <TabsContent value=\"php\" className=\"space-y-4 mt-0\">\n                    <CardContent className=\"p-3 pt-6\">\n                        <div className=\"space-y-4\">\n                            <div className=\"flex items-start\">\n                                <div className=\"h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0 mr-3\">\n                                    <FileCode className=\"h-5 w-5 text-primary\" />\n                                </div>\n                                <div>\n                                    <h3 className=\"font-medium mb-1\">PHP Client Library</h3>\n                                    <p className=\"text-sm text-muted-foreground mb-3\">\n                                        Simple and lightweight PHP SDK for integrating with the Changerawr API.\n                                    </p>\n                                </div>\n                            </div>\n\n                            <div className=\"space-y-1\">\n                                <p className=\"text-xs uppercase font-medium text-muted-foreground\">INSTALLATION</p>\n                                <div className=\"rounded-md overflow-hidden border\">\n                                    <div className=\"flex items-center px-3 py-1.5 bg-muted/50 border-b\">\n                                        <div className=\"flex space-x-1.5\">\n                                            <div className=\"w-2.5 h-2.5 rounded-full bg-muted-foreground/30\"></div>\n                                            <div className=\"w-2.5 h-2.5 rounded-full bg-muted-foreground/30\"></div>\n                                            <div className=\"w-2.5 h-2.5 rounded-full bg-muted-foreground/30\"></div>\n                                        </div>\n                                        <div className=\"ml-auto\">\n                                            <Button\n                                                variant=\"ghost\"\n                                                size=\"sm\"\n                                                className=\"h-6\"\n                                                onClick={() => handleCopy(\"composer require changerawr/php\", \"php\")}\n                                            >\n                                                {copied.php ? (\n                                                    <CheckCircle className=\"h-3.5 w-3.5 text-green-500\" />\n                                                ) : (\n                                                    <Copy className=\"h-3.5 w-3.5\" />\n                                                )}\n                                            </Button>\n                                        </div>\n                                    </div>\n                                    <div className=\"p-3 font-mono text-xs\">\n                                        <code>composer require changerawr/php</code>\n                                    </div>\n                                </div>\n                            </div>\n\n                            <div className=\"space-y-2\">\n                                <p className=\"text-xs uppercase font-medium text-muted-foreground\">FEATURES</p>\n                                <ul className=\"space-y-2 text-sm\">\n                                    <li className=\"flex items-start gap-2\">\n                                        <CheckCircle className=\"h-4 w-4 text-primary mt-0.5 flex-shrink-0\" />\n                                        <span>Simple, lightweight API client</span>\n                                    </li>\n                                    <li className=\"flex items-start gap-2\">\n                                        <CheckCircle className=\"h-4 w-4 text-primary mt-0.5 flex-shrink-0\" />\n                                        <span>PHP 7.4+ compatible</span>\n                                    </li>\n                                    <li className=\"flex items-start gap-2\">\n                                        <CheckCircle className=\"h-4 w-4 text-primary mt-0.5 flex-shrink-0\" />\n                                        <span>Minimal dependencies</span>\n                                    </li>\n                                </ul>\n                            </div>\n                        </div>\n                    </CardContent>\n\n                    <CardFooter className=\"pt-0\">\n                        <Button\n                            className=\"w-full\"\n                            variant=\"outline\"\n                            asChild\n                        >\n                            <a\n                                href=\"https://github.com/changerawr/php\"\n                                target=\"_blank\"\n                                rel=\"noopener noreferrer\"\n                                className=\"flex items-center justify-center\"\n                            >\n                                View Documentation\n                                <ChevronRight className=\"h-4 w-4 ml-1\" />\n                            </a>\n                        </Button>\n                    </CardFooter>\n                </TabsContent>\n            </Tabs>\n        </Card>\n    );\n}\n\nexport default SDKShowcase;"
  },
  {
    "path": "components/admin/audit-logs/VirtualizedList.tsx",
    "content": "'use client'\n\nimport React, { useCallback, useRef } from 'react'\nimport { motion, AnimatePresence } from 'framer-motion'\nimport { format, parseISO } from 'date-fns'\nimport { useVirtualizer } from '@tanstack/react-virtual'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Info, Loader2 } from 'lucide-react'\n\n// Types\ninterface AuditLog {\n    id: string\n    action: string\n    userId: string\n    targetUserId?: string | null\n    details?: Record<string, unknown> | string\n    createdAt: string\n    user: {\n        name?: string | null\n        email: string\n    }\n    targetUser?: {\n        name?: string | null\n        email?: string\n    } | null\n}\n\ninterface VirtualizedListProps {\n    items: AuditLog[]\n    height?: number\n    loadMore?: () => void\n    hasMore?: boolean\n    isLoadingMore?: boolean\n    onLogSelect: (log: AuditLog) => void\n    getActionVariant: (action: string) => string\n}\n\nconst VirtualizedList: React.FC<VirtualizedListProps> = ({\n                                                             items,\n                                                             height = 500,\n                                                             loadMore = () => {},\n                                                             hasMore = false,\n                                                             isLoadingMore = false,\n                                                             onLogSelect,\n                                                             getActionVariant\n                                                         }) => {\n    const parentRef = useRef<HTMLDivElement>(null)\n\n    // Virtual list implementation\n    const virtualizer = useVirtualizer({\n        count: items.length,\n        getScrollElement: () => parentRef.current,\n        estimateSize: () => 46, // Row height\n        overscan: 10,\n    })\n\n    // Handle scroll to load more data\n    const handleScroll = useCallback(() => {\n        if (hasMore && !isLoadingMore && parentRef.current) {\n            const { scrollHeight, scrollTop, clientHeight } = parentRef.current\n\n            // If scrolled to 90% of the way down, load more\n            if (scrollTop + clientHeight >= scrollHeight * 0.9) {\n                loadMore()\n            }\n        }\n    }, [hasMore, isLoadingMore, loadMore])\n\n    // Animation variants for row animations\n    const rowVariants = {\n        hidden: { opacity: 0, y: 5 },\n        visible: (i: number) => ({\n            opacity: 1,\n            y: 0,\n            transition: {\n                delay: i * 0.01, // Staggered delay based on index\n                duration: 0.2,\n            }\n        })\n    }\n\n    return (\n        <div\n            ref={parentRef}\n            className=\"relative overflow-auto w-full\"\n            style={{ height }}\n            onScroll={handleScroll}\n        >\n            <table className=\"w-full border-collapse table-fixed\">\n                <thead className=\"sticky top-0 z-10 bg-background\">\n                <tr className=\"border-b text-xs text-muted-foreground\">\n                    <th className=\"text-left font-medium p-2 pl-3 w-40\">Timestamp</th>\n                    <th className=\"text-left font-medium p-2 w-40\">Action</th>\n                    <th className=\"text-left font-medium p-2 w-48\">Performer</th>\n                    <th className=\"text-left font-medium p-2\">Target</th>\n                    <th className=\"text-right font-medium p-2 w-16\">Details</th>\n                </tr>\n                </thead>\n            </table>\n\n            {/* Virtualized rows container */}\n            <div\n                style={{\n                    height: virtualizer.getTotalSize(),\n                    width: '100%',\n                    position: 'relative'\n                }}\n            >\n                <AnimatePresence>\n                    {virtualizer.getVirtualItems().map((virtualRow) => {\n                        const item = items[virtualRow.index]\n                        return (\n                            <motion.div\n                                key={item.id}\n                                data-index={virtualRow.index}\n                                className={`${\n                                    virtualRow.index % 2 === 0 ? 'bg-background' : 'bg-muted/5'\n                                } hover:bg-muted/20 transition-colors border-b cursor-pointer absolute top-0 left-0 w-full`}\n                                style={{\n                                    height: `${virtualRow.size}px`,\n                                    transform: `translateY(${virtualRow.start}px)`,\n                                }}\n                                onClick={() => onLogSelect(item)}\n                                initial=\"hidden\"\n                                animate=\"visible\"\n                                custom={virtualRow.index}\n                                variants={rowVariants}\n                            >\n                                <div className=\"flex items-center w-full h-full\">\n                                    <div className=\"text-xs whitespace-nowrap p-2 pl-3 w-40\">\n                                        {format(parseISO(item.createdAt), 'yyyy-MM-dd HH:mm:ss')}\n                                    </div>\n                                    <div className=\"p-2 w-40\">\n                                        <Badge\n                                            variant={getActionVariant(item.action) as \"info\" | \"default\" | \"outline\" | \"secondary\" | \"destructive\" | \"ghost\" | \"success\" | \"warning\" | null | undefined}\n                                            className=\"text-xs font-medium\"\n                                        >\n                                            {item.action}\n                                        </Badge>\n                                    </div>\n                                    <div className=\"p-2 text-xs w-48 truncate\">\n                                        {item.user.name || item.user.email}\n                                    </div>\n                                    <div className=\"p-2 text-xs flex-1 truncate\">\n                                        {item.targetUser\n                                            ? (item.targetUser.name || item.targetUser.email)\n                                            : \"—\"}\n                                    </div>\n                                    <div className=\"p-2 w-16 text-right\">\n                                        <Button\n                                            variant=\"ghost\"\n                                            size=\"icon\"\n                                            className=\"h-6 w-6 ml-auto\"\n                                            onClick={(e) => {\n                                                e.stopPropagation()\n                                                onLogSelect(item)\n                                            }}\n                                        >\n                                            <Info className=\"h-3.5 w-3.5\" />\n                                        </Button>\n                                    </div>\n                                </div>\n                            </motion.div>\n                        )\n                    })}\n                </AnimatePresence>\n            </div>\n\n            {/* Bottom message */}\n            {isLoadingMore && (\n                <div className=\"text-center py-2 text-xs text-muted-foreground sticky bottom-0 bg-background bg-opacity-90 backdrop-blur-sm\">\n                    <div className=\"flex items-center justify-center gap-2\">\n                        <Loader2 className=\"h-3 w-3 animate-spin\" />\n                        <span>Loading more logs...</span>\n                    </div>\n                </div>\n            )}\n\n            {!hasMore && items.length > 0 && (\n                <div className=\"text-center py-2 text-xs text-muted-foreground sticky bottom-0 bg-background bg-opacity-90 backdrop-blur-sm\">\n                    <span>You&apos;ve reached the end of the logs ({items.length} records)</span>\n                </div>\n            )}\n        </div>\n    )\n}\n\nexport default VirtualizedList"
  },
  {
    "path": "components/admin/requests/Management.tsx",
    "content": "'use client'\n\nimport {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'\nimport React, {useState} from 'react'\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card'\nimport {\n    AlertDialog,\n    AlertDialogAction,\n    AlertDialogCancel,\n    AlertDialogContent,\n    AlertDialogDescription,\n    AlertDialogFooter,\n    AlertDialogHeader,\n    AlertDialogTitle,\n} from '@/components/ui/alert-dialog'\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogHeader,\n    DialogTitle,\n} from '@/components/ui/dialog'\nimport {\n    Collapsible,\n    CollapsibleContent,\n    CollapsibleTrigger,\n} from '@/components/ui/collapsible'\nimport {Button} from '@/components/ui/button'\nimport {Badge} from '@/components/ui/badge'\nimport {Separator} from '@/components/ui/separator'\nimport {useToast} from '@/hooks/use-toast'\nimport {formatDistanceToNow, format} from 'date-fns'\nimport {\n    Check,\n    X,\n    Loader2,\n    AlertTriangle,\n    Calendar,\n    Clock,\n    ChevronDown,\n    ChevronRight,\n    Eye,\n    User,\n    Building,\n    FileText,\n    Tag,\n    Trash2,\n} from 'lucide-react'\nimport {motion, AnimatePresence} from 'framer-motion'\n\n// Updated type definitions\nexport type RequestType = 'DELETE_PROJECT' | 'DELETE_TAG' | 'DELETE_ENTRY' | 'ALLOW_PUBLISH' | 'ALLOW_SCHEDULE';\n\nexport const REQUEST_TYPES: Record<RequestType, {\n    label: string;\n    description: string;\n    targetDisplay: (request: ChangelogRequest) => string;\n    icon: React.ReactNode;\n    variant: 'default' | 'secondary' | 'destructive' | 'outline';\n    severity: 'low' | 'medium' | 'high' | 'critical';\n    getDetails: (request: ChangelogRequest) => React.ReactNode;\n}> = {\n    DELETE_PROJECT: {\n        label: 'Delete Project',\n        description: 'Permanently delete an entire project and all associated data',\n        targetDisplay: () => 'Entire Project',\n        icon: <AlertTriangle className=\"h-3 w-3\"/>,\n        variant: 'destructive',\n        severity: 'critical',\n        getDetails: (request) => (\n            <div className=\"space-y-3\">\n                <div className=\"p-3 bg-red-50 border border-red-200 rounded-lg\">\n                    <div className=\"flex items-center gap-2 text-red-800 font-medium\">\n                        <AlertTriangle className=\"h-4 w-4\"/>\n                        Critical Action - Irreversible\n                    </div>\n                    <p className=\"text-sm text-red-700 mt-1\">\n                        This will permanently delete the entire project, including all changelog entries, tags, and\n                        settings.\n                    </p>\n                </div>\n                <div className=\"grid grid-cols-2 gap-4 text-sm\">\n                    <div>\n                        <span className=\"text-muted-foreground\">Project:</span>\n                        <p className=\"font-medium\">{request.project.name}</p>\n                    </div>\n                    <div>\n                        <span className=\"text-muted-foreground\">Impact:</span>\n                        <p className=\"font-medium text-red-600\">All data will be lost</p>\n                    </div>\n                </div>\n            </div>\n        )\n    },\n    DELETE_TAG: {\n        label: 'Delete Tag',\n        description: 'Remove a specific changelog tag',\n        targetDisplay: (request) => request.ChangelogTag?.name || 'Unknown Tag',\n        icon: <Tag className=\"h-3 w-3\"/>,\n        variant: 'destructive',\n        severity: 'medium',\n        getDetails: (request) => (\n            <div className=\"space-y-3\">\n                <div className=\"p-3 bg-amber-50 border border-amber-200 rounded-lg\">\n                    <div className=\"flex items-center gap-2 text-amber-800 font-medium\">\n                        <Tag className=\"h-4 w-4\"/>\n                        Tag Removal\n                    </div>\n                    <p className=\"text-sm text-amber-700 mt-1\">\n                        This tag will be removed from all changelog entries and the project defaults.\n                    </p>\n                </div>\n                <div className=\"grid grid-cols-2 gap-4 text-sm\">\n                    <div>\n                        <span className=\"text-muted-foreground\">Tag Name:</span>\n                        <p className=\"font-medium\">{request.ChangelogTag?.name || request.targetId || 'Unknown'}</p>\n                    </div>\n                    <div>\n                        <span className=\"text-muted-foreground\">Project:</span>\n                        <p className=\"font-medium\">{request.project.name}</p>\n                    </div>\n                </div>\n            </div>\n        )\n    },\n    DELETE_ENTRY: {\n        label: 'Delete Entry',\n        description: 'Remove a specific changelog entry',\n        targetDisplay: (request) => request.ChangelogEntry?.title || 'Unknown Entry',\n        icon: <Trash2 className=\"h-3 w-3\"/>,\n        variant: 'destructive',\n        severity: 'high',\n        getDetails: (request) => (\n            <div className=\"space-y-3\">\n                <div className=\"p-3 bg-red-50 border border-red-200 rounded-lg\">\n                    <div className=\"flex items-center gap-2 text-red-800 font-medium\">\n                        <FileText className=\"h-4 w-4\"/>\n                        Entry Deletion\n                    </div>\n                    <p className=\"text-sm text-red-700 mt-1\">\n                        This changelog entry will be permanently removed and cannot be recovered.\n                    </p>\n                </div>\n                <div className=\"space-y-2 text-sm\">\n                    <div>\n                        <span className=\"text-muted-foreground\">Entry Title:</span>\n                        <p className=\"font-medium\">{request.ChangelogEntry?.title || 'Unknown Entry'}</p>\n                    </div>\n                    <div>\n                        <span className=\"text-muted-foreground\">Project:</span>\n                        <p className=\"font-medium\">{request.project.name}</p>\n                    </div>\n                </div>\n            </div>\n        )\n    },\n    ALLOW_PUBLISH: {\n        label: 'Publish Entry',\n        description: 'Request approval to publish a changelog entry',\n        targetDisplay: (request) => request.ChangelogEntry?.title || 'Unknown Entry',\n        icon: <Check className=\"h-3 w-3\"/>,\n        variant: 'default',\n        severity: 'low',\n        getDetails: (request) => {\n            const metadata = request.metadata as {customPublishedAt?: string} | null;\n            const customPublishedAt = metadata?.customPublishedAt;\n            const publishDate = customPublishedAt ? new Date(customPublishedAt) : null;\n\n            return (\n                <div className=\"space-y-3\">\n                    <div className={`p-3 rounded-lg border ${\n                        customPublishedAt\n                            ? 'bg-amber-50 border-amber-200'\n                            : 'bg-blue-50 border-blue-200'\n                    }`}>\n                        <div className={`flex items-center gap-2 font-medium ${\n                            customPublishedAt\n                                ? 'text-amber-800'\n                                : 'text-blue-800'\n                        }`}>\n                            <FileText className=\"h-4 w-4\"/>\n                            {customPublishedAt ? 'Publish with Custom Date' : 'Publish Request'}\n                        </div>\n                        <p className={`text-sm mt-1 ${\n                            customPublishedAt\n                                ? 'text-amber-700'\n                                : 'text-blue-700'\n                        }`}>\n                            {customPublishedAt && publishDate\n                                ? `Staff member wants to publish this entry with a backdated publish date of ${format(publishDate, 'PPP p')}.`\n                                : 'Staff member wants to publish this changelog entry immediately.'\n                            }\n                        </p>\n                    </div>\n                    <div className=\"space-y-2 text-sm\">\n                        <div>\n                            <span className=\"text-muted-foreground\">Entry Title:</span>\n                            <p className=\"font-medium\">{request.ChangelogEntry?.title || 'Unknown Entry'}</p>\n                        </div>\n                        <div>\n                            <span className=\"text-muted-foreground\">Project:</span>\n                            <p className=\"font-medium\">{request.project.name}</p>\n                        </div>\n                        {customPublishedAt && publishDate && (\n                            <>\n                                <div>\n                                    <span className=\"text-muted-foreground\">Requested Publish Date:</span>\n                                    <p className=\"font-medium text-amber-700\">{format(publishDate, 'PPP p')}</p>\n                                </div>\n                                <div className=\"p-2 bg-amber-50 border border-amber-200 rounded text-xs text-amber-800\">\n                                    ⚠️ This entry will show the custom date instead of the current date. Ensure this is intentional.\n                                </div>\n                            </>\n                        )}\n                    </div>\n                </div>\n            );\n        }\n    },\n    ALLOW_SCHEDULE: {\n        label: 'Schedule Entry',\n        description: 'Request approval to schedule a changelog entry for automatic publishing',\n        targetDisplay: (request) => {\n            const title = request.ChangelogEntry?.title || 'Unknown Entry';\n            const scheduledTime = request.targetId ? format(new Date(request.targetId), 'MMM d, h:mm a') : 'Unknown time';\n            return `${title} (${scheduledTime})`;\n        },\n        icon: <Clock className=\"h-3 w-3\"/>,\n        variant: 'secondary',\n        severity: 'low',\n        getDetails: (request) => (\n            <div className=\"space-y-3\">\n                <div className=\"p-3 bg-purple-50 border border-purple-200 rounded-lg\">\n                    <div className=\"flex items-center gap-2 text-purple-800 font-medium\">\n                        <Calendar className=\"h-4 w-4\"/>\n                        Schedule Request\n                    </div>\n                    <p className=\"text-sm text-purple-700 mt-1\">\n                        Staff member wants to schedule this entry for automatic publishing.\n                    </p>\n                </div>\n                <div className=\"grid grid-cols-2 gap-4 text-sm\">\n                    <div>\n                        <span className=\"text-muted-foreground\">Entry Title:</span>\n                        <p className=\"font-medium\">{request.ChangelogEntry?.title || 'Unknown Entry'}</p>\n                    </div>\n                    <div>\n                        <span className=\"text-muted-foreground\">Scheduled For:</span>\n                        <p className=\"font-medium text-purple-600\">\n                            {request.targetId\n                                ? format(new Date(request.targetId), 'PPP p')\n                                : 'Unknown time'\n                            }\n                        </p>\n                    </div>\n                </div>\n                <div className=\"text-sm\">\n                    <span className=\"text-muted-foreground\">Project:</span>\n                    <p className=\"font-medium\">{request.project.name}</p>\n                </div>\n                {request.targetId && (\n                    <div className=\"text-sm\">\n                        <span className=\"text-muted-foreground\">Time until publish:</span>\n                        <p className=\"font-medium\">\n                            {formatDistanceToNow(new Date(request.targetId), {addSuffix: true})}\n                        </p>\n                    </div>\n                )}\n            </div>\n        )\n    }\n};\n\n// Updated types\nexport interface ChangelogRequest {\n    id: string;\n    type: RequestType;\n    targetId?: string | null;\n    metadata?: {customPublishedAt?: string} | null;\n    project: {\n        name: string;\n    };\n    staff: {\n        name: string | null;\n        email: string;\n    };\n    ChangelogTag?: {\n        name: string;\n    };\n    ChangelogEntry?: {\n        title: string;\n    };\n    createdAt: string;\n}\n\nexport type RequestStatus = 'APPROVED' | 'REJECTED';\n\ntype ProcessingRequest = {\n    id: string;\n    status: RequestStatus;\n} | null;\n\nexport function RequestManagement() {\n    const {toast} = useToast()\n    const queryClient = useQueryClient()\n    const [processingRequest, setProcessingRequest] = useState<ProcessingRequest>(null)\n    const [isDialogOpen, setIsDialogOpen] = useState(false)\n    const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set())\n    const [selectedRequest, setSelectedRequest] = useState<ChangelogRequest | null>(null)\n    const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false)\n\n    const {data: requests, isLoading, error} = useQuery<ChangelogRequest[]>({\n        queryKey: ['changelog-requests'],\n        queryFn: async () => {\n            const response = await fetch('/api/changelog/requests')\n            if (!response.ok) {\n                const error = await response.json()\n                throw new Error(error.error || 'Failed to fetch requests')\n            }\n            return response.json()\n        }\n    })\n\n    const processRequest = useMutation({\n        mutationFn: async ({\n                               requestId,\n                               status\n                           }: {\n            requestId: string\n            status: RequestStatus\n        }) => {\n            const response = await fetch(`/api/changelog/requests/${requestId}`, {\n                method: 'PATCH',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({status})\n            })\n\n            if (!response.ok) {\n                const error = await response.json()\n                throw new Error(error.error || 'Failed to process request')\n            }\n\n            return response.json()\n        },\n        onSuccess: () => {\n            queryClient.invalidateQueries({queryKey: ['changelog-requests']})\n            toast({\n                title: 'Success',\n                description: `Request ${processingRequest?.status?.toLowerCase() || ''} successfully`\n            })\n            setIsDialogOpen(false)\n            setProcessingRequest(null)\n        },\n        onError: (error: Error) => {\n            toast({\n                title: 'Error',\n                description: error.message,\n                variant: 'destructive'\n            })\n            setIsDialogOpen(false)\n            setProcessingRequest(null)\n        }\n    })\n\n    const handleProcessRequest = (requestId: string, status: RequestStatus) => {\n        setProcessingRequest({id: requestId, status})\n        setIsDialogOpen(true)\n    }\n\n    const confirmProcessRequest = () => {\n        if (!processingRequest) return\n\n        processRequest.mutate({\n            requestId: processingRequest.id,\n            status: processingRequest.status\n        })\n    }\n\n    const toggleRowExpansion = (requestId: string) => {\n        const newExpanded = new Set(expandedRows)\n        if (newExpanded.has(requestId)) {\n            newExpanded.delete(requestId)\n        } else {\n            newExpanded.add(requestId)\n        }\n        setExpandedRows(newExpanded)\n    }\n\n    const openDetailDialog = (request: ChangelogRequest) => {\n        setSelectedRequest(request)\n        setIsDetailDialogOpen(true)\n    }\n\n    const getRequestTypeInfo = (type: RequestType) => {\n        return REQUEST_TYPES[type] || {\n            label: type,\n            description: 'Unknown request type',\n            targetDisplay: () => 'Unknown',\n            icon: <AlertTriangle className=\"h-3 w-3\"/>,\n            variant: 'outline' as const,\n            severity: 'medium' as const,\n            getDetails: () => <div>No details available</div>\n        };\n    }\n\n    const getSeverityColor = (severity: string) => {\n        switch (severity) {\n            case 'critical':\n                return 'bg-red-100 text-red-800 border-red-200'\n            case 'high':\n                return 'bg-orange-100 text-orange-800 border-orange-200'\n            case 'medium':\n                return 'bg-yellow-100 text-yellow-800 border-yellow-200'\n            case 'low':\n                return 'bg-green-100 text-green-800 border-green-200'\n            default:\n                return 'bg-gray-100 text-gray-800 border-gray-200'\n        }\n    }\n\n    if (isLoading) {\n        return (\n            <div className=\"flex items-center justify-center h-96\">\n                <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\"/>\n            </div>\n        )\n    }\n\n    if (error) {\n        return (\n            <div className=\"flex flex-col items-center justify-center h-96 gap-4\">\n                <AlertTriangle className=\"h-8 w-8 text-destructive\"/>\n                <p className=\"text-sm text-muted-foreground\">Failed to load requests</p>\n            </div>\n        )\n    }\n\n    return (\n        <>\n            <Card>\n                <CardHeader>\n                    <CardTitle>Pending Requests</CardTitle>\n                    <CardDescription>\n                        Review and manage action requests from staff members. Click on any row for detailed information.\n                    </CardDescription>\n                </CardHeader>\n                <CardContent>\n                    {!requests?.length ? (\n                        <div className=\"text-center py-12\">\n                            <div className=\"flex flex-col items-center gap-3\">\n                                <div className=\"p-3 bg-muted rounded-full\">\n                                    <Calendar className=\"h-8 w-8 text-muted-foreground\"/>\n                                </div>\n                                <div className=\"space-y-1\">\n                                    <p className=\"text-sm font-medium\">No pending requests</p>\n                                    <p className=\"text-xs text-muted-foreground\">All requests have been processed</p>\n                                </div>\n                            </div>\n                        </div>\n                    ) : (\n                        <div className=\"space-y-4\">\n                            {requests.map((request) => {\n                                const typeInfo = getRequestTypeInfo(request.type);\n                                const isExpanded = expandedRows.has(request.id);\n\n                                return (\n                                    <motion.div\n                                        key={request.id}\n                                        initial={{opacity: 0, y: 20}}\n                                        animate={{opacity: 1, y: 0}}\n                                        className=\"border rounded-lg overflow-hidden hover:shadow-md transition-all duration-200\"\n                                    >\n                                        <Collapsible\n                                            open={isExpanded}\n                                            onOpenChange={() => toggleRowExpansion(request.id)}\n                                        >\n                                            <CollapsibleTrigger asChild>\n                                                <div className=\"p-4 hover:bg-muted/50 cursor-pointer transition-colors\">\n                                                    <div className=\"flex items-center justify-between\">\n                                                        <div className=\"flex items-center gap-4 flex-1\">\n                                                            <div className=\"flex items-center gap-2\">\n                                                                {isExpanded ? (\n                                                                    <ChevronDown\n                                                                        className=\"h-4 w-4 text-muted-foreground\"/>\n                                                                ) : (\n                                                                    <ChevronRight\n                                                                        className=\"h-4 w-4 text-muted-foreground\"/>\n                                                                )}\n                                                                <Badge\n                                                                    variant={typeInfo.variant}\n                                                                    className={`gap-1 ${getSeverityColor(typeInfo.severity)}`}\n                                                                >\n                                                                    {typeInfo.icon}\n                                                                    {typeInfo.label}\n                                                                </Badge>\n                                                            </div>\n\n                                                            <div className=\"flex-1 min-w-0\">\n                                                                <div className=\"flex items-center gap-2\">\n                                                                    <Building\n                                                                        className=\"h-4 w-4 text-muted-foreground flex-shrink-0\"/>\n                                                                    <span className=\"font-medium text-sm truncate\">\n                                                                        {request.project.name}\n                                                                    </span>\n                                                                </div>\n                                                                <div className=\"flex items-center gap-2 mt-1\">\n                                                                    <User\n                                                                        className=\"h-4 w-4 text-muted-foreground flex-shrink-0\"/>\n                                                                    <span\n                                                                        className=\"text-sm text-muted-foreground truncate\">\n                                                                        {request.staff.name || request.staff.email}\n                                                                    </span>\n                                                                </div>\n                                                            </div>\n\n                                                            <div className=\"text-right\">\n                                                                <div className=\"text-sm font-medium\">\n                                                                    {typeInfo.targetDisplay(request)}\n                                                                </div>\n                                                                <div className=\"text-xs text-muted-foreground mt-1\">\n                                                                    {formatDistanceToNow(new Date(request.createdAt), {\n                                                                        addSuffix: true\n                                                                    })}\n                                                                </div>\n                                                            </div>\n                                                        </div>\n\n                                                        <div className=\"flex items-center gap-2 ml-4\">\n                                                            <Button\n                                                                size=\"sm\"\n                                                                variant=\"outline\"\n                                                                onClick={(e) => {\n                                                                    e.stopPropagation();\n                                                                    openDetailDialog(request);\n                                                                }}\n                                                                className=\"h-8 w-8 p-0\"\n                                                            >\n                                                                <Eye className=\"h-4 w-4\"/>\n                                                            </Button>\n                                                            <Button\n                                                                size=\"sm\"\n                                                                variant=\"success\"\n                                                                onClick={(e) => {\n                                                                    e.stopPropagation();\n                                                                    handleProcessRequest(request.id, 'APPROVED');\n                                                                }}\n                                                                disabled={processRequest.isPending}\n                                                            >\n                                                                <Check className=\"h-4 w-4\"/>\n                                                            </Button>\n                                                            <Button\n                                                                size=\"sm\"\n                                                                variant=\"destructive\"\n                                                                onClick={(e) => {\n                                                                    e.stopPropagation();\n                                                                    handleProcessRequest(request.id, 'REJECTED');\n                                                                }}\n                                                                disabled={processRequest.isPending}\n                                                                className=\"h-8 w-8 p-0\"\n                                                            >\n                                                                <X className=\"h-4 w-4\"/>\n                                                            </Button>\n                                                        </div>\n                                                    </div>\n                                                </div>\n                                            </CollapsibleTrigger>\n\n                                            <CollapsibleContent>\n                                                <AnimatePresence>\n                                                    {isExpanded && (\n                                                        <motion.div\n                                                            initial={{opacity: 0, height: 0}}\n                                                            animate={{opacity: 1, height: 'auto'}}\n                                                            exit={{opacity: 0, height: 0}}\n                                                            transition={{duration: 0.2}}\n                                                        >\n                                                            <Separator/>\n                                                            <div className=\"p-4 bg-muted/25\">\n                                                                {typeInfo.getDetails(request)}\n                                                            </div>\n                                                        </motion.div>\n                                                    )}\n                                                </AnimatePresence>\n                                            </CollapsibleContent>\n                                        </Collapsible>\n                                    </motion.div>\n                                );\n                            })}\n                        </div>\n                    )}\n                </CardContent>\n            </Card>\n\n            {/* Detail Dialog */}\n            <Dialog open={isDetailDialogOpen} onOpenChange={setIsDetailDialogOpen}>\n                <DialogContent className=\"max-w-2xl\">\n                    {selectedRequest && (\n                        <>\n                            <DialogHeader>\n                                <div className=\"flex items-center gap-2\">\n                                    {REQUEST_TYPES[selectedRequest.type].icon}\n                                    <DialogTitle>\n                                        {REQUEST_TYPES[selectedRequest.type].label} Request\n                                    </DialogTitle>\n                                </div>\n                                <DialogDescription>\n                                    Detailed information about this request\n                                </DialogDescription>\n                            </DialogHeader>\n\n                            <div className=\"space-y-6\">\n                                {REQUEST_TYPES[selectedRequest.type].getDetails(selectedRequest)}\n\n                                <Separator/>\n\n                                <div className=\"grid grid-cols-2 gap-4 text-sm\">\n                                    <div>\n                                        <span className=\"text-muted-foreground\">Requested by:</span>\n                                        <div className=\"font-medium\">\n                                            {selectedRequest.staff.name || 'Unnamed User'}\n                                        </div>\n                                        <div className=\"text-xs text-muted-foreground\">\n                                            {selectedRequest.staff.email}\n                                        </div>\n                                    </div>\n                                    <div>\n                                        <span className=\"text-muted-foreground\">Requested:</span>\n                                        <p className=\"font-medium\">\n                                            {formatDistanceToNow(new Date(selectedRequest.createdAt), {\n                                                addSuffix: true\n                                            })}\n                                        </p>\n                                        <div className=\"text-xs text-muted-foreground\">\n                                            {format(new Date(selectedRequest.createdAt), 'PPP p')}\n                                        </div>\n                                    </div>\n                                </div>\n\n                                <div className=\"flex gap-2 pt-4\">\n                                    <Button\n                                        onClick={() => {\n                                            setIsDetailDialogOpen(false);\n                                            handleProcessRequest(selectedRequest.id, 'APPROVED');\n                                        }}\n                                        variant=\"success\"\n                                    >\n                                        <Check className=\"h-4 w-4 mr-2\"/>\n                                        Approve Request\n                                    </Button>\n                                    <Button\n                                        variant=\"destructive\"\n                                        onClick={() => {\n                                            setIsDetailDialogOpen(false);\n                                            handleProcessRequest(selectedRequest.id, 'REJECTED');\n                                        }}\n                                        className=\"flex-1\"\n                                    >\n                                        <X className=\"h-4 w-4 mr-2\"/>\n                                        Reject Request\n                                    </Button>\n                                </div>\n                            </div>\n                        </>\n                    )}\n                </DialogContent>\n            </Dialog>\n\n            {/* Confirmation Dialog */}\n            <AlertDialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>\n                <AlertDialogContent>\n                    <AlertDialogHeader>\n                        <AlertDialogTitle>\n                            {processingRequest?.status === 'APPROVED' ? 'Approve Request?' : 'Reject Request?'}\n                        </AlertDialogTitle>\n                        <AlertDialogDescription>\n                            {processingRequest?.status === 'APPROVED'\n                                ? 'This will approve the request and execute the requested action immediately.'\n                                : 'This will reject the request. The requested action will not be performed and the staff member will be notified.'}\n                        </AlertDialogDescription>\n                    </AlertDialogHeader>\n                    <AlertDialogFooter>\n                        <AlertDialogCancel onClick={() => {\n                            setProcessingRequest(null)\n                            setIsDialogOpen(false)\n                        }}>\n                            Cancel\n                        </AlertDialogCancel>\n                        <AlertDialogAction\n                            onClick={confirmProcessRequest}\n                        >\n                            {processRequest.isPending ? (\n                                <Loader2 className=\"h-4 w-4 animate-spin\"/>\n                            ) : processingRequest?.status === 'APPROVED' ? (\n                                'Approve'\n                            ) : (\n                                'Reject'\n                            )}\n                        </AlertDialogAction>\n                    </AlertDialogFooter>\n                </AlertDialogContent>\n            </AlertDialog>\n        </>\n    )\n}"
  },
  {
    "path": "components/analytics/analytics-chart.tsx",
    "content": "'use client';\n\nimport {\n    LineChart,\n    Line,\n    XAxis,\n    YAxis,\n    CartesianGrid,\n    Tooltip,\n    ResponsiveContainer,\n    AreaChart,\n    Area,\n    Legend\n} from 'recharts';\nimport { useTheme } from 'next-themes';\nimport { useTimezone } from '@/hooks/use-timezone';\nimport type { DailyAnalytics } from '@/lib/types/analytics';\n\ninterface AnalyticsChartProps {\n    data: DailyAnalytics[];\n    type?: 'line' | 'area';\n    height?: number;\n}\n\ninterface TooltipPayload {\n    value: number;\n    payload: {\n        date: string;\n        fullDate: string;\n        views: number;\n        uniqueVisitors: number;\n    };\n    color: string;\n    name: string;\n}\n\ninterface CustomTooltipProps {\n    active?: boolean | undefined;\n    payload?: TooltipPayload[] | null;\n}\n\nexport function AnalyticsChart({\n                                   data,\n                                   type = 'area',\n                                   height = 350\n                               }: AnalyticsChartProps) {\n    const { theme } = useTheme();\n    const timezone = useTimezone();\n\n    // Transform and prepare data for the chart\n    const chartData = data\n        .map(item => ({\n            date: item.date,\n            displayDate: new Date(item.date).toLocaleDateString('en-US', {\n                month: 'short',\n                day: 'numeric',\n                timeZone: timezone,\n            }),\n            fullDate: new Date(item.date).toLocaleDateString('en-US', {\n                weekday: 'long',\n                year: 'numeric',\n                month: 'long',\n                day: 'numeric',\n                timeZone: timezone,\n            }),\n            views: item.views || 0,\n            uniqueVisitors: item.uniqueVisitors || 0,\n        }))\n        .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) // Sort chronologically\n        .slice(-30); // Show last 30 days max for readability\n\n    // Theme-aware colors\n    const isDark = theme === 'dark';\n    const colors = {\n        primary: isDark ? '#3b82f6' : '#2563eb',\n        secondary: isDark ? '#64748b' : '#475569',\n        grid: isDark ? '#374151' : '#e5e7eb',\n        text: isDark ? '#f1f5f9' : '#1e293b',\n        background: isDark ? '#1e293b' : '#ffffff',\n    };\n\n    // Custom tooltip\n    const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {\n        if (active && payload && payload.length) {\n            const data = payload[0].payload;\n            return (\n                <div className=\"bg-card border border-border rounded-lg shadow-lg p-3 min-w-[200px]\">\n                    <p className=\"font-medium text-card-foreground mb-2\">\n                        {data.fullDate}\n                    </p>\n                    <div className=\"space-y-1\">\n                        {payload.map((entry: TooltipPayload, index: number) => (\n                            <div key={index} className=\"flex items-center justify-between gap-4 text-sm\">\n                                <div className=\"flex items-center gap-2\">\n                                    <div\n                                        className=\"w-3 h-3 rounded-full\"\n                                        style={{ backgroundColor: entry.color }}\n                                    />\n                                    <span className=\"text-muted-foreground\">{entry.name}</span>\n                                </div>\n                                <span className=\"font-medium text-card-foreground\">\n                  {entry.value.toLocaleString()}\n                </span>\n                            </div>\n                        ))}\n                    </div>\n                </div>\n            );\n        }\n        return null;\n    };\n\n    // Handle empty data\n    if (!data || data.length === 0) {\n        return (\n            <div className=\"flex items-center justify-center h-80 text-muted-foreground\">\n                <div className=\"text-center space-y-2\">\n                    <div className=\"text-4xl mb-2\">📊</div>\n                    <p className=\"text-lg font-medium\">No data available</p>\n                    <p className=\"text-sm\">Analytics data will appear here once you have visitors</p>\n                </div>\n            </div>\n        );\n    }\n\n    return (\n        <div className=\"w-full\" style={{ height }}>\n            <ResponsiveContainer width=\"100%\" height=\"100%\">\n                {type === 'area' ? (\n                    <AreaChart\n                        data={chartData}\n                        margin={{ top: 20, right: 30, left: 20, bottom: 20 }}\n                    >\n                        <defs>\n                            <linearGradient id=\"viewsGradient\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n                                <stop offset=\"5%\" stopColor={colors.primary} stopOpacity={0.4} />\n                                <stop offset=\"95%\" stopColor={colors.primary} stopOpacity={0.1} />\n                            </linearGradient>\n                            <linearGradient id=\"visitorsGradient\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n                                <stop offset=\"5%\" stopColor={colors.secondary} stopOpacity={0.4} />\n                                <stop offset=\"95%\" stopColor={colors.secondary} stopOpacity={0.1} />\n                            </linearGradient>\n                        </defs>\n\n                        <CartesianGrid\n                            strokeDasharray=\"3 3\"\n                            stroke={colors.grid}\n                            opacity={0.5}\n                            vertical={false}\n                        />\n\n                        <XAxis\n                            dataKey=\"displayDate\"\n                            stroke={colors.text}\n                            fontSize={12}\n                            tickLine={false}\n                            axisLine={false}\n                            tick={{ fill: colors.text }}\n                            interval=\"preserveStartEnd\"\n                        />\n\n                        <YAxis\n                            stroke={colors.text}\n                            fontSize={12}\n                            tickLine={false}\n                            axisLine={false}\n                            tick={{ fill: colors.text }}\n                            tickFormatter={(value) => value.toLocaleString()}\n                            width={60}\n                        />\n\n                        <Tooltip content={<CustomTooltip />} />\n\n                        <Legend\n                            wrapperStyle={{\n                                paddingTop: '20px',\n                                color: colors.text\n                            }}\n                        />\n\n                        <Area\n                            type=\"monotone\"\n                            dataKey=\"views\"\n                            stackId=\"1\"\n                            stroke={colors.primary}\n                            fill=\"url(#viewsGradient)\"\n                            strokeWidth={2}\n                            name=\"Views\"\n                            dot={false}\n                            activeDot={{\n                                r: 4,\n                                stroke: colors.primary,\n                                strokeWidth: 2,\n                                fill: colors.background\n                            }}\n                        />\n\n                        <Area\n                            type=\"monotone\"\n                            dataKey=\"uniqueVisitors\"\n                            stackId=\"2\"\n                            stroke={colors.secondary}\n                            fill=\"url(#visitorsGradient)\"\n                            strokeWidth={2}\n                            name=\"Unique Visitors\"\n                            dot={false}\n                            activeDot={{\n                                r: 4,\n                                stroke: colors.secondary,\n                                strokeWidth: 2,\n                                fill: colors.background\n                            }}\n                        />\n                    </AreaChart>\n                ) : (\n                    <LineChart\n                        data={chartData}\n                        margin={{ top: 20, right: 30, left: 20, bottom: 20 }}\n                    >\n                        <CartesianGrid\n                            strokeDasharray=\"3 3\"\n                            stroke={colors.grid}\n                            opacity={0.5}\n                            vertical={false}\n                        />\n\n                        <XAxis\n                            dataKey=\"displayDate\"\n                            stroke={colors.text}\n                            fontSize={12}\n                            tickLine={false}\n                            axisLine={false}\n                            tick={{ fill: colors.text }}\n                            interval=\"preserveStartEnd\"\n                        />\n\n                        <YAxis\n                            stroke={colors.text}\n                            fontSize={12}\n                            tickLine={false}\n                            axisLine={false}\n                            tick={{ fill: colors.text }}\n                            tickFormatter={(value) => value.toLocaleString()}\n                            width={60}\n                        />\n\n                        <Tooltip content={<CustomTooltip />} />\n\n                        <Legend\n                            wrapperStyle={{\n                                paddingTop: '20px',\n                                color: colors.text\n                            }}\n                        />\n\n                        <Line\n                            type=\"monotone\"\n                            dataKey=\"views\"\n                            stroke={colors.primary}\n                            strokeWidth={3}\n                            name=\"Views\"\n                            dot={{ fill: colors.primary, strokeWidth: 2, r: 4 }}\n                            activeDot={{\n                                r: 6,\n                                stroke: colors.primary,\n                                strokeWidth: 2,\n                                fill: colors.background\n                            }}\n                        />\n\n                        <Line\n                            type=\"monotone\"\n                            dataKey=\"uniqueVisitors\"\n                            stroke={colors.secondary}\n                            strokeWidth={3}\n                            name=\"Unique Visitors\"\n                            dot={{ fill: colors.secondary, strokeWidth: 2, r: 4 }}\n                            activeDot={{\n                                r: 6,\n                                stroke: colors.secondary,\n                                strokeWidth: 2,\n                                fill: colors.background\n                            }}\n                        />\n                    </LineChart>\n                )}\n            </ResponsiveContainer>\n        </div>\n    );\n}"
  },
  {
    "path": "components/analytics/analytics-metric-card.tsx",
    "content": "'use client';\n\nimport {motion} from 'framer-motion';\nimport {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card';\nimport {Badge} from '@/components/ui/badge';\nimport {TrendingUp, TrendingDown, Minus, type LucideIcon} from 'lucide-react';\nimport {cn} from '@/lib/utils';\n\ninterface AnalyticsMetricCardProps {\n    title: string;\n    value: number;\n    icon: LucideIcon;\n    description?: string;\n    change?: number;\n    changeType?: 'increase' | 'decrease' | 'neutral';\n    format?: 'number' | 'percentage' | 'currency';\n    loading?: boolean;\n}\n\nexport function AnalyticsMetricCard({\n                                        title,\n                                        value,\n                                        icon: Icon,\n                                        description,\n                                        change,\n                                        changeType,\n                                        format = 'number',\n                                        loading = false\n                                    }: AnalyticsMetricCardProps) {\n    const formatValue = (val: number) => {\n        switch (format) {\n            case 'percentage':\n                return `${val.toFixed(1)}%`;\n            case 'currency':\n                return new Intl.NumberFormat('en-US', {\n                    style: 'currency',\n                    currency: 'USD'\n                }).format(val);\n            default:\n                return val.toLocaleString();\n        }\n    };\n\n    const getChangeIcon = () => {\n        switch (changeType) {\n            case 'increase':\n                return TrendingUp;\n            case 'decrease':\n                return TrendingDown;\n            default:\n                return Minus;\n        }\n    };\n\n    const getChangeColor = () => {\n        switch (changeType) {\n            case 'increase':\n                return 'text-green-600 dark:text-green-400';\n            case 'decrease':\n                return 'text-red-600 dark:text-red-400';\n            default:\n                return 'text-muted-foreground';\n        }\n    };\n\n    const ChangeIcon = getChangeIcon();\n\n    const cardVariants = {\n        initial: {opacity: 0, y: 20},\n        animate: {opacity: 1, y: 0},\n        transition: {duration: 0.5}\n    };\n\n    const valueVariants = {\n        initial: {scale: 0.8, opacity: 0},\n        animate: {\n            scale: 1,\n            opacity: 1,\n            transition: {delay: 0.2, duration: 0.3}\n        }\n    };\n\n    if (loading) {\n        return (\n            <Card>\n                <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n                    <CardTitle className=\"text-sm font-medium\">{title}</CardTitle>\n                    <div className=\"h-4 w-4 bg-muted animate-pulse rounded\"/>\n                </CardHeader>\n                <CardContent>\n                    <div className=\"h-8 w-20 bg-muted animate-pulse rounded mb-1\"/>\n                    <div className=\"h-3 w-32 bg-muted animate-pulse rounded\"/>\n                </CardContent>\n            </Card>\n        );\n    }\n\n    return (\n        <motion.div\n            variants={cardVariants}\n            initial=\"initial\"\n            animate=\"animate\"\n            whileHover={{\n                scale: 1.02,\n                transition: {duration: 0.2}\n            }}\n        >\n            <Card className={cn(\n                \"relative overflow-hidden\",\n                \"hover:shadow-lg hover:shadow-primary/5 transition-all duration-300\",\n                \"border-l-4 border-l-primary/20 hover:border-l-primary\"\n            )}>\n                <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n                    <CardTitle className=\"text-sm font-medium text-muted-foreground\">\n                        {title}\n                    </CardTitle>\n                    <Icon className=\"h-4 w-4 text-primary\"/>\n                </CardHeader>\n                <CardContent>\n                    <motion.div\n                        variants={valueVariants}\n                        initial=\"initial\"\n                        animate=\"animate\"\n                        className=\"space-y-1\"\n                    >\n                        <div className=\"text-2xl font-bold tracking-tight\">\n                            {formatValue(value)}\n                        </div>\n\n                        <div className=\"flex items-center gap-2 text-xs\">\n                            {description && (\n                                <p className=\"text-muted-foreground\">{description}</p>\n                            )}\n\n                            {change !== undefined && (\n                                <Badge\n                                    variant=\"secondary\"\n                                    className={cn(\n                                        \"gap-1 text-xs font-normal\",\n                                        getChangeColor()\n                                    )}\n                                >\n                                    <ChangeIcon className=\"h-3 w-3\"/>\n                                    {Math.abs(change).toFixed(1)}%\n                                </Badge>\n                            )}\n                        </div>\n                    </motion.div>\n                </CardContent>\n\n                {/* Subtle background gradient */}\n                <div\n                    className=\"absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-transparent pointer-events-none\"/>\n            </Card>\n        </motion.div>\n    );\n}"
  },
  {
    "path": "components/analytics/country-analytics-table.tsx",
    "content": "'use client';\n\nimport {motion} from 'framer-motion';\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card';\nimport {\n    Table,\n    TableBody,\n    TableCell,\n    TableHead,\n    TableHeader,\n    TableRow,\n} from '@/components/ui/table';\nimport {Badge} from '@/components/ui/badge';\nimport {Progress} from '@/components/ui/progress';\nimport {Globe} from 'lucide-react';\nimport type {CountryAnalytics} from '@/lib/types/analytics';\n\ninterface CountryAnalyticsTableProps {\n    countries: CountryAnalytics[];\n}\n\n// Country code to flag emoji mapping\nconst getCountryFlag = (countryName: string): string => {\n    const countryFlags: Record<string, string> = {\n        'United States': '🇺🇸',\n        'United Kingdom': '🇬🇧',\n        'Canada': '🇨🇦',\n        'Germany': '🇩🇪',\n        'France': '🇫🇷',\n        'Japan': '🇯🇵',\n        'Australia': '🇦🇺',\n        'Netherlands': '🇳🇱',\n        'Brazil': '🇧🇷',\n        'India': '🇮🇳',\n        'China': '🇨🇳',\n        'Russia': '🇷🇺',\n        'Spain': '🇪🇸',\n        'Italy': '🇮🇹',\n        'South Korea': '🇰🇷',\n        'Mexico': '🇲🇽',\n        'Poland': '🇵🇱',\n        'Sweden': '🇸🇪',\n        'Norway': '🇳🇴',\n        'Denmark': '🇩🇰',\n        'Finland': '🇫🇮',\n        'Switzerland': '🇨🇭',\n        'Austria': '🇦🇹',\n        'Belgium': '🇧🇪',\n        'Ireland': '🇮🇪',\n        'Portugal': '🇵🇹',\n        'Czech Republic': '🇨🇿',\n        'Hungary': '🇭🇺',\n        'Romania': '🇷🇴',\n        'Bulgaria': '🇧🇬',\n        'Croatia': '🇭🇷',\n        'Slovakia': '🇸🇰',\n        'Slovenia': '🇸🇮',\n        'Estonia': '🇪🇪',\n        'Latvia': '🇱🇻',\n        'Lithuania': '🇱🇹',\n        'Local': '🏠',\n        'Unknown': '🌍',\n    };\n\n    return countryFlags[countryName] || '🌍';\n};\n\nexport function CountryAnalyticsTable({countries}: CountryAnalyticsTableProps) {\n    // Calculate percentages\n    const totalViews = countries.reduce((sum, country) => sum + country.count, 0);\n    const countriesWithPercentage = countries.map(country => ({\n        ...country,\n        percentage: totalViews > 0 ? (country.count / totalViews) * 100 : 0\n    }));\n\n    const containerVariants = {\n        initial: {opacity: 0, y: 20},\n        animate: {\n            opacity: 1,\n            y: 0,\n            transition: {\n                duration: 0.5,\n                staggerChildren: 0.05\n            }\n        }\n    };\n\n    const rowVariants = {\n        initial: {opacity: 0, x: -20},\n        animate: {opacity: 1, x: 0}\n    };\n\n    return (\n        <motion.div\n            variants={containerVariants}\n            initial=\"initial\"\n            animate=\"animate\"\n        >\n            <Card>\n                <CardHeader>\n                    <CardTitle className=\"flex items-center gap-2\">\n                        <Globe className=\"h-5 w-5\"/>\n                        Top Countries\n                    </CardTitle>\n                    <CardDescription>\n                        Geographic distribution of your audience\n                    </CardDescription>\n                </CardHeader>\n                <CardContent>\n                    {countriesWithPercentage.length === 0 ? (\n                        <div className=\"text-center py-8 text-muted-foreground\">\n                            <Globe className=\"h-12 w-12 mx-auto mb-4 opacity-50\"/>\n                            <p>No geographic data available</p>\n                        </div>\n                    ) : (\n                        <div className=\"space-y-4\">\n                            <Table>\n                                <TableHeader>\n                                    <TableRow>\n                                        <TableHead>Country</TableHead>\n                                        <TableHead className=\"text-right\">Views</TableHead>\n                                        <TableHead className=\"text-right\">Share</TableHead>\n                                        <TableHead className=\"w-24\">Distribution</TableHead>\n                                    </TableRow>\n                                </TableHeader>\n                                <TableBody>\n                                    {countriesWithPercentage.map((country, index) => (\n                                        <motion.tr\n                                            key={country.country}\n                                            variants={rowVariants}\n                                            className=\"group hover:bg-muted/50 transition-colors\"\n                                        >\n                                            <TableCell className=\"flex items-center gap-3\">\n                                                <span className=\"text-lg\">{getCountryFlag(country.country)}</span>\n                                                <div>\n                                                    <div className=\"font-medium\">{country.country}</div>\n                                                    {index === 0 && (\n                                                        <Badge variant=\"secondary\" className=\"text-xs mt-1\">\n                                                            Top Market\n                                                        </Badge>\n                                                    )}\n                                                </div>\n                                            </TableCell>\n                                            <TableCell className=\"text-right font-mono\">\n                                                {country.count.toLocaleString()}\n                                            </TableCell>\n                                            <TableCell className=\"text-right\">\n                                                <Badge variant=\"outline\" className=\"font-mono\">\n                                                    {country.percentage.toFixed(1)}%\n                                                </Badge>\n                                            </TableCell>\n                                            <TableCell>\n                                                <div className=\"space-y-1\">\n                                                    <Progress\n                                                        value={country.percentage}\n                                                        className=\"h-2\"\n                                                    />\n                                                </div>\n                                            </TableCell>\n                                        </motion.tr>\n                                    ))}\n                                </TableBody>\n                            </Table>\n\n                            {totalViews > 0 && (\n                                <div\n                                    className=\"flex items-center justify-between text-sm text-muted-foreground pt-2 border-t\">\n                                    <span>Total Views</span>\n                                    <span className=\"font-mono font-medium\">\n                    {totalViews.toLocaleString()}\n                  </span>\n                                </div>\n                            )}\n                        </div>\n                    )}\n                </CardContent>\n            </Card>\n        </motion.div>\n    );\n}"
  },
  {
    "path": "components/analytics/entry-analytics-table.tsx",
    "content": "'use client';\n\nimport {motion} from 'framer-motion';\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card';\nimport {\n    Table,\n    TableBody,\n    TableCell,\n    TableHead,\n    TableHeader,\n    TableRow,\n} from '@/components/ui/table';\nimport {Badge} from '@/components/ui/badge';\nimport {Progress} from '@/components/ui/progress';\nimport {Button} from '@/components/ui/button';\nimport {FileText, ExternalLink, Eye, Users} from 'lucide-react';\nimport Link from 'next/link';\nimport type {EntryAnalytics} from '@/lib/types/analytics';\n\ninterface EntryAnalyticsTableProps {\n    entries: EntryAnalytics[];\n    projectId: string;\n}\n\nexport function EntryAnalyticsTable({entries, projectId}: EntryAnalyticsTableProps) {\n    // Calculate percentages based on total views\n    const totalViews = entries.reduce((sum, entry) => sum + entry.views, 0);\n    const entriesWithPercentage = entries.map(entry => ({\n        ...entry,\n        percentage: totalViews > 0 ? (entry.views / totalViews) * 100 : 0\n    }));\n\n    const containerVariants = {\n        initial: {opacity: 0, y: 20},\n        animate: {\n            opacity: 1,\n            y: 0,\n            transition: {\n                duration: 0.5,\n                staggerChildren: 0.05\n            }\n        }\n    };\n\n    const rowVariants = {\n        initial: {opacity: 0, x: -20},\n        animate: {opacity: 1, x: 0}\n    };\n\n    return (\n        <motion.div\n            variants={containerVariants}\n            initial=\"initial\"\n            animate=\"animate\"\n        >\n            <Card>\n                <CardHeader>\n                    <CardTitle className=\"flex items-center gap-2\">\n                        <FileText className=\"h-5 w-5\"/>\n                        Top Changelog Entries\n                    </CardTitle>\n                    <CardDescription>\n                        Most viewed changelog entries in the selected period\n                    </CardDescription>\n                </CardHeader>\n                <CardContent>\n                    {entriesWithPercentage.length === 0 ? (\n                        <div className=\"text-center py-8 text-muted-foreground\">\n                            <FileText className=\"h-12 w-12 mx-auto mb-4 opacity-50\"/>\n                            <p>No entry data available</p>\n                            <p className=\"text-sm mt-2\">Entry views will appear here once tracked</p>\n                        </div>\n                    ) : (\n                        <div className=\"space-y-4\">\n                            <Table>\n                                <TableHeader>\n                                    <TableRow>\n                                        <TableHead>Entry</TableHead>\n                                        <TableHead className=\"text-center\">Views</TableHead>\n                                        <TableHead className=\"text-center\">Visitors</TableHead>\n                                        <TableHead className=\"text-center\">Share</TableHead>\n                                        <TableHead className=\"w-24\">Performance</TableHead>\n                                        <TableHead className=\"w-20\">Action</TableHead>\n                                    </TableRow>\n                                </TableHeader>\n                                <TableBody>\n                                    {entriesWithPercentage.map((entry, index) => (\n                                        <motion.tr\n                                            key={entry.entryId}\n                                            variants={rowVariants}\n                                            className=\"group hover:bg-muted/50 transition-colors\"\n                                        >\n                                            <TableCell className=\"max-w-md\">\n                                                <div className=\"space-y-1\">\n                                                    <div className=\"font-medium truncate pr-4\" title={entry.title}>\n                                                        {entry.title}\n                                                    </div>\n                                                    <div className=\"flex items-center gap-2\">\n                                                        {index === 0 && (\n                                                            <Badge variant=\"default\" className=\"text-xs\">\n                                                                🔥 Most Popular\n                                                            </Badge>\n                                                        )}\n                                                        {index < 3 && index > 0 && (\n                                                            <Badge variant=\"secondary\" className=\"text-xs\">\n                                                                Top {index + 1}\n                                                            </Badge>\n                                                        )}\n                                                    </div>\n                                                </div>\n                                            </TableCell>\n                                            <TableCell className=\"text-center\">\n                                                <div className=\"flex items-center justify-center gap-1\">\n                                                    <Eye className=\"h-3 w-3 text-muted-foreground\"/>\n                                                    <span className=\"font-mono font-medium\">\n                            {entry.views.toLocaleString()}\n                          </span>\n                                                </div>\n                                            </TableCell>\n                                            <TableCell className=\"text-center\">\n                                                <div className=\"flex items-center justify-center gap-1\">\n                                                    <Users className=\"h-3 w-3 text-muted-foreground\"/>\n                                                    <span className=\"font-mono font-medium\">\n                            {entry.uniqueVisitors.toLocaleString()}\n                          </span>\n                                                </div>\n                                            </TableCell>\n                                            <TableCell className=\"text-center\">\n                                                <Badge variant=\"outline\" className=\"font-mono\">\n                                                    {entry.percentage.toFixed(1)}%\n                                                </Badge>\n                                            </TableCell>\n                                            <TableCell>\n                                                <div className=\"space-y-1\">\n                                                    <Progress\n                                                        value={entry.percentage}\n                                                        className=\"h-2\"\n                                                    />\n                                                    <div className=\"text-xs text-muted-foreground\">\n                                                        {((entry.uniqueVisitors / entry.views) * 100).toFixed(0)}%\n                                                        unique\n                                                    </div>\n                                                </div>\n                                            </TableCell>\n                                            <TableCell>\n                                                <Button variant=\"ghost\" size=\"sm\" asChild>\n                                                    <Link\n                                                        href={`/changelog/${projectId}#${entry.entryId}`}\n                                                        target=\"_blank\"\n                                                        className=\"h-8 w-8 p-0\"\n                                                    >\n                                                        <ExternalLink className=\"h-3 w-3\"/>\n                                                        <span className=\"sr-only\">View entry</span>\n                                                    </Link>\n                                                </Button>\n                                            </TableCell>\n                                        </motion.tr>\n                                    ))}\n                                </TableBody>\n                            </Table>\n\n                            {totalViews > 0 && (\n                                <div\n                                    className=\"flex items-center justify-between text-sm text-muted-foreground pt-2 border-t\">\n                                    <span>Total Entry Views</span>\n                                    <span className=\"font-mono font-medium\">\n                    {totalViews.toLocaleString()}\n                  </span>\n                                </div>\n                            )}\n                        </div>\n                    )}\n                </CardContent>\n            </Card>\n        </motion.div>\n    );\n}"
  },
  {
    "path": "components/analytics/project-analytics-table.tsx",
    "content": "'use client';\n\nimport { motion } from 'framer-motion';\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card';\nimport {\n    Table,\n    TableBody,\n    TableCell,\n    TableHead,\n    TableHeader,\n    TableRow,\n} from '@/components/ui/table';\nimport { Badge } from '@/components/ui/badge';\nimport { Progress } from '@/components/ui/progress';\nimport { Button } from '@/components/ui/button';\nimport { Avatar, AvatarFallback } from '@/components/ui/avatar';\nimport { Building2, ExternalLink, Eye, Users, BarChart3 } from 'lucide-react';\nimport Link from 'next/link';\nimport type { ProjectAnalyticsSummary } from '@/lib/types/analytics';\n\ninterface ProjectAnalyticsTableProps {\n    projects: ProjectAnalyticsSummary[];\n}\n\nexport function ProjectAnalyticsTable({ projects }: ProjectAnalyticsTableProps) {\n    // Calculate percentages based on total views\n    const totalViews = projects.reduce((sum, project) => sum + project.views, 0);\n    const projectsWithPercentage = projects.map(project => ({\n        ...project,\n        percentage: totalViews > 0 ? (project.views / totalViews) * 100 : 0\n    }));\n\n    const containerVariants = {\n        initial: { opacity: 0, y: 20 },\n        animate: {\n            opacity: 1,\n            y: 0,\n            transition: {\n                duration: 0.5,\n                staggerChildren: 0.05\n            }\n        }\n    };\n\n    const rowVariants = {\n        initial: { opacity: 0, x: -20 },\n        animate: { opacity: 1, x: 0 }\n    };\n\n    return (\n        <motion.div\n            variants={containerVariants}\n            initial=\"initial\"\n            animate=\"animate\"\n        >\n            <Card>\n                <CardHeader>\n                    <CardTitle className=\"flex items-center gap-2\">\n                        <Building2 className=\"h-5 w-5\" />\n                        Top Projects\n                    </CardTitle>\n                    <CardDescription>\n                        Most popular projects by views and engagement\n                    </CardDescription>\n                </CardHeader>\n                <CardContent>\n                    {projectsWithPercentage.length === 0 ? (\n                        <div className=\"text-center py-8 text-muted-foreground\">\n                            <Building2 className=\"h-12 w-12 mx-auto mb-4 opacity-50\" />\n                            <p>No project data available</p>\n                            <p className=\"text-sm mt-2\">Project analytics will appear here once tracked</p>\n                        </div>\n                    ) : (\n                        <div className=\"space-y-4\">\n                            <Table>\n                                <TableHeader>\n                                    <TableRow>\n                                        <TableHead>Project</TableHead>\n                                        <TableHead className=\"text-center\">Views</TableHead>\n                                        <TableHead className=\"text-center\">Visitors</TableHead>\n                                        <TableHead className=\"text-center\">Share</TableHead>\n                                        <TableHead className=\"w-24\">Performance</TableHead>\n                                        <TableHead className=\"w-20\">Actions</TableHead>\n                                    </TableRow>\n                                </TableHeader>\n                                <TableBody>\n                                    {projectsWithPercentage.map((project, index) => (\n                                        <motion.tr\n                                            key={project.projectId}\n                                            variants={rowVariants}\n                                            className=\"group hover:bg-muted/50 transition-colors\"\n                                        >\n                                            <TableCell>\n                                                <div className=\"flex items-center gap-3\">\n                                                    <Avatar className=\"h-8 w-8 rounded-lg bg-primary/10 flex items-center justify-center text-primary font-medium border-2 border-primary/20\">\n                                                        <AvatarFallback className=\"text-xs bg-transparent\">\n                                                            {project.projectName.substring(0, 2).toUpperCase()}\n                                                        </AvatarFallback>\n                                                    </Avatar>\n                                                    <div>\n                                                        <div className=\"font-medium\">{project.projectName}</div>\n                                                        <div className=\"flex items-center gap-2 mt-1\">\n                                                            {index === 0 && (\n                                                                <Badge variant=\"default\" className=\"text-xs\">\n                                                                    🏆 Top Performer\n                                                                </Badge>\n                                                            )}\n                                                            {index < 3 && index > 0 && (\n                                                                <Badge variant=\"secondary\" className=\"text-xs\">\n                                                                    Top {index + 1}\n                                                                </Badge>\n                                                            )}\n                                                        </div>\n                                                    </div>\n                                                </div>\n                                            </TableCell>\n                                            <TableCell className=\"text-center\">\n                                                <div className=\"flex items-center justify-center gap-1\">\n                                                    <Eye className=\"h-3 w-3 text-muted-foreground\" />\n                                                    <span className=\"font-mono font-medium\">\n                            {project.views.toLocaleString()}\n                          </span>\n                                                </div>\n                                            </TableCell>\n                                            <TableCell className=\"text-center\">\n                                                <div className=\"flex items-center justify-center gap-1\">\n                                                    <Users className=\"h-3 w-3 text-muted-foreground\" />\n                                                    <span className=\"font-mono font-medium\">\n                            {project.uniqueVisitors.toLocaleString()}\n                          </span>\n                                                </div>\n                                            </TableCell>\n                                            <TableCell className=\"text-center\">\n                                                <Badge variant=\"outline\" className=\"font-mono\">\n                                                    {project.percentage.toFixed(1)}%\n                                                </Badge>\n                                            </TableCell>\n                                            <TableCell>\n                                                <div className=\"space-y-1\">\n                                                    <Progress\n                                                        value={project.percentage}\n                                                        className=\"h-2\"\n                                                    />\n                                                    <div className=\"text-xs text-muted-foreground\">\n                                                        {project.views > 0 ? ((project.uniqueVisitors / project.views) * 100).toFixed(0) : 0}% unique\n                                                    </div>\n                                                </div>\n                                            </TableCell>\n                                            <TableCell>\n                                                <div className=\"flex items-center gap-1\">\n                                                    <Button variant=\"ghost\" size=\"sm\" asChild>\n                                                        <Link\n                                                            href={`/dashboard/projects/${project.projectId}/analytics`}\n                                                            className=\"h-8 w-8 p-0\"\n                                                        >\n                                                            <BarChart3 className=\"h-3 w-3\" />\n                                                            <span className=\"sr-only\">View analytics</span>\n                                                        </Link>\n                                                    </Button>\n                                                    <Button variant=\"ghost\" size=\"sm\" asChild>\n                                                        <Link\n                                                            href={`/changelog/${project.projectId}`}\n                                                            target=\"_blank\"\n                                                            className=\"h-8 w-8 p-0\"\n                                                        >\n                                                            <ExternalLink className=\"h-3 w-3\" />\n                                                            <span className=\"sr-only\">View public page</span>\n                                                        </Link>\n                                                    </Button>\n                                                </div>\n                                            </TableCell>\n                                        </motion.tr>\n                                    ))}\n                                </TableBody>\n                            </Table>\n\n                            {totalViews > 0 && (\n                                <div className=\"flex items-center justify-between text-sm text-muted-foreground pt-2 border-t\">\n                                    <span>Total System Views</span>\n                                    <span className=\"font-mono font-medium\">\n                    {totalViews.toLocaleString()}\n                  </span>\n                                </div>\n                            )}\n                        </div>\n                    )}\n                </CardContent>\n            </Card>\n        </motion.div>\n    );\n}"
  },
  {
    "path": "components/analytics/referrer-analytics-table.tsx",
    "content": "'use client';\n\nimport {motion} from 'framer-motion';\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card';\nimport {\n    Table,\n    TableBody,\n    TableCell,\n    TableHead,\n    TableHeader,\n    TableRow,\n} from '@/components/ui/table';\nimport {Badge} from '@/components/ui/badge';\nimport {Progress} from '@/components/ui/progress';\nimport {Button} from '@/components/ui/button';\nimport {ExternalLink, Link as LinkIcon} from 'lucide-react';\nimport Link from 'next/link';\nimport type {ReferrerAnalytics} from '@/lib/types/analytics';\n\ninterface ReferrerAnalyticsTableProps {\n    referrers: ReferrerAnalytics[];\n}\n\n// Get a nice icon/emoji for different referrer types\nconst getReferrerIcon = (referrer: string): string => {\n    if (referrer === 'Direct' || referrer === '(direct)') return '🔗';\n    if (referrer.includes('google')) return '🔍';\n    if (referrer.includes('twitter') || referrer.includes('x.com')) return '🐦';\n    if (referrer.includes('facebook')) return '📘';\n    if (referrer.includes('linkedin')) return '💼';\n    if (referrer.includes('reddit')) return '🤖';\n    if (referrer.includes('github')) return '⚡';\n    if (referrer.includes('discord')) return '💬';\n    if (referrer.includes('slack')) return '💌';\n    if (referrer.includes('email') || referrer.includes('mailto')) return '📧';\n    return '🌐';\n};\n\n// Clean up referrer display name\nconst cleanReferrerName = (referrer: string): string => {\n    if (referrer === 'Direct' || referrer === '(direct)') return 'Direct Traffic';\n\n    try {\n        const url = new URL(referrer);\n        const hostname = url.hostname.replace('www.', '');\n\n        // Special cases for better display\n        const displayNames: Record<string, string> = {\n            'google.com': 'Google Search',\n            'google.co.uk': 'Google Search (UK)',\n            'bing.com': 'Bing Search',\n            'duckduckgo.com': 'DuckDuckGo',\n            'twitter.com': 'Twitter',\n            'x.com': 'X (Twitter)',\n            'facebook.com': 'Facebook',\n            'linkedin.com': 'LinkedIn',\n            'reddit.com': 'Reddit',\n            'github.com': 'GitHub',\n            'stackoverflow.com': 'Stack Overflow',\n            'discord.com': 'Discord',\n            'slack.com': 'Slack',\n            'medium.com': 'Medium',\n            'dev.to': 'DEV Community',\n            'hashnode.com': 'Hashnode',\n            'youtube.com': 'YouTube',\n        };\n\n        return displayNames[hostname] || hostname;\n    } catch {\n        return referrer;\n    }\n};\n\n// Determine if referrer is external and can be linked to\nconst isExternalReferrer = (referrer: string): boolean => {\n    return referrer !== 'Direct' &&\n        referrer !== '(direct)' &&\n        referrer.startsWith('http');\n};\n\nexport function ReferrerAnalyticsTable({referrers}: ReferrerAnalyticsTableProps) {\n    // Calculate percentages\n    const totalViews = referrers.reduce((sum, referrer) => sum + referrer.count, 0);\n    const referrersWithPercentage = referrers.map(referrer => ({\n        ...referrer,\n        percentage: totalViews > 0 ? (referrer.count / totalViews) * 100 : 0,\n        displayName: cleanReferrerName(referrer.referrer),\n        icon: getReferrerIcon(referrer.referrer),\n        isExternal: isExternalReferrer(referrer.referrer)\n    }));\n\n    const containerVariants = {\n        initial: {opacity: 0, y: 20},\n        animate: {\n            opacity: 1,\n            y: 0,\n            transition: {\n                duration: 0.5,\n                staggerChildren: 0.05\n            }\n        }\n    };\n\n    const rowVariants = {\n        initial: {opacity: 0, x: -20},\n        animate: {opacity: 1, x: 0}\n    };\n\n    return (\n        <motion.div\n            variants={containerVariants}\n            initial=\"initial\"\n            animate=\"animate\"\n        >\n            <Card>\n                <CardHeader>\n                    <CardTitle className=\"flex items-center gap-2\">\n                        <LinkIcon className=\"h-5 w-5\"/>\n                        Top Referrers\n                    </CardTitle>\n                    <CardDescription>\n                        Where your visitors are coming from\n                    </CardDescription>\n                </CardHeader>\n                <CardContent>\n                    {referrersWithPercentage.length === 0 ? (\n                        <div className=\"text-center py-8 text-muted-foreground\">\n                            <LinkIcon className=\"h-12 w-12 mx-auto mb-4 opacity-50\"/>\n                            <p>No referrer data available</p>\n                            <p className=\"text-sm mt-2\">Traffic sources will appear here</p>\n                        </div>\n                    ) : (\n                        <div className=\"space-y-4\">\n                            <Table>\n                                <TableHeader>\n                                    <TableRow>\n                                        <TableHead>Source</TableHead>\n                                        <TableHead className=\"text-right\">Visits</TableHead>\n                                        <TableHead className=\"text-right\">Share</TableHead>\n                                        <TableHead className=\"w-24\">Distribution</TableHead>\n                                        <TableHead className=\"w-20\">Link</TableHead>\n                                    </TableRow>\n                                </TableHeader>\n                                <TableBody>\n                                    {referrersWithPercentage.map((referrer, index) => (\n                                        <motion.tr\n                                            key={referrer.referrer}\n                                            variants={rowVariants}\n                                            className=\"group hover:bg-muted/50 transition-colors\"\n                                        >\n                                            <TableCell>\n                                                <div className=\"flex items-center gap-3\">\n                                                    <span className=\"text-lg\">{referrer.icon}</span>\n                                                    <div>\n                                                        <div className=\"font-medium\">{referrer.displayName}</div>\n                                                        {index === 0 && (\n                                                            <Badge variant=\"secondary\" className=\"text-xs mt-1\">\n                                                                Top Source\n                                                            </Badge>\n                                                        )}\n                                                        {referrer.referrer === 'Direct' && (\n                                                            <div className=\"text-xs text-muted-foreground mt-1\">\n                                                                Bookmarks, URL bar, apps\n                                                            </div>\n                                                        )}\n                                                    </div>\n                                                </div>\n                                            </TableCell>\n                                            <TableCell className=\"text-right font-mono\">\n                                                {referrer.count.toLocaleString()}\n                                            </TableCell>\n                                            <TableCell className=\"text-right\">\n                                                <Badge variant=\"outline\" className=\"font-mono\">\n                                                    {referrer.percentage.toFixed(1)}%\n                                                </Badge>\n                                            </TableCell>\n                                            <TableCell>\n                                                <div className=\"space-y-1\">\n                                                    <Progress\n                                                        value={referrer.percentage}\n                                                        className=\"h-2\"\n                                                    />\n                                                </div>\n                                            </TableCell>\n                                            <TableCell>\n                                                {referrer.isExternal ? (\n                                                    <Button variant=\"ghost\" size=\"sm\" asChild>\n                                                        <Link\n                                                            href={referrer.referrer}\n                                                            target=\"_blank\"\n                                                            rel=\"noopener noreferrer\"\n                                                            className=\"h-8 w-8 p-0\"\n                                                        >\n                                                            <ExternalLink className=\"h-3 w-3\"/>\n                                                            <span className=\"sr-only\">Visit source</span>\n                                                        </Link>\n                                                    </Button>\n                                                ) : (\n                                                    <div className=\"h-8 w-8 flex items-center justify-center\">\n                                                        <span className=\"text-muted-foreground text-xs\">—</span>\n                                                    </div>\n                                                )}\n                                            </TableCell>\n                                        </motion.tr>\n                                    ))}\n                                </TableBody>\n                            </Table>\n\n                            {totalViews > 0 && (\n                                <div\n                                    className=\"flex items-center justify-between text-sm text-muted-foreground pt-2 border-t\">\n                                    <span>Total Referral Traffic</span>\n                                    <span className=\"font-mono font-medium\">\n                    {totalViews.toLocaleString()}\n                  </span>\n                                </div>\n                            )}\n                        </div>\n                    )}\n                </CardContent>\n            </Card>\n        </motion.div>\n    );\n}"
  },
  {
    "path": "components/changelog/ButtonGroup.tsx",
    "content": "'use client'\n\nimport React from 'react'\nimport ScrollToTopButton from './ScrollToTopButton'\nimport { ThemeToggle } from './ThemeToggle'\nimport { useScrollVisibility } from './ScrollToTopButton'\n\ninterface ButtonGroupProps {\n    projectId: string\n}\n\nexport default function ButtonGroup({ projectId }: ButtonGroupProps) {\n    const scrollVisible = useScrollVisibility()\n\n    return (\n        <div className={`fixed flex flex-col gap-2 right-6 md:right-8 z-50 transition-all duration-300 ${\n            scrollVisible\n                ? 'bottom-6 md:bottom-8'\n                : 'bottom-6 md:bottom-8'\n        }`}>\n            {scrollVisible && <ScrollToTopButton />}\n            <ThemeToggle projectId={projectId} />\n        </div>\n    )\n}"
  },
  {
    "path": "components/changelog/ChangelogActionRequest.tsx",
    "content": "import {useState} from 'react'\nimport {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogFooter,\n    DialogHeader,\n    DialogTitle,\n    DialogTrigger,\n} from '@/components/ui/dialog'\nimport {Button} from '@/components/ui/button'\nimport {RadioGroup, RadioGroupItem} from '@/components/ui/radio-group'\nimport {Checkbox} from '@/components/ui/checkbox'\nimport {Label} from '@/components/ui/label'\nimport {Separator} from '@/components/ui/separator'\nimport {Badge} from '@/components/ui/badge'\nimport {Card, CardContent, CardHeader} from '@/components/ui/card'\nimport {Input} from '@/components/ui/input'\nimport {useToast} from '@/hooks/use-toast'\nimport {\n    Globe,\n    Loader2,\n    PackageOpen,\n    Trash2,\n    Calendar,\n    Mail,\n    Users,\n    Clock,\n    AlertTriangle,\n    CheckCircle,\n    Ban,\n    AlertCircle\n} from 'lucide-react'\nimport {useAuth} from '@/context/auth'\nimport {cn} from '@/lib/utils'\nimport {truncateText} from '@/lib/utils/text'\n\ntype ActionType = 'PUBLISH' | 'UNPUBLISH' | 'DELETE' | 'ALLOW_SCHEDULE';\ntype RequestType = 'ALLOW_PUBLISH' | 'DELETE_ENTRY' | 'ALLOW_SCHEDULE';\ntype ButtonVariant = 'default' | 'destructive' | 'outline' | 'ghost' | 'secondary';\ntype ButtonSize = 'default' | 'sm' | 'lg' | 'icon';\ntype RecipientType = 'SUBSCRIBERS' | 'MANUAL' | 'BOTH';\ntype SubscriptionType = 'ALL_UPDATES' | 'MAJOR_ONLY' | 'DIGEST_ONLY';\n\ninterface PendingRequest {\n    id: string;\n    type: RequestType;\n    status: string;\n    createdAt: string;\n    staff: {\n        name: string;\n        email: string;\n    };\n}\n\ninterface ChangelogActionRequestProps {\n    projectId: string;\n    entryId: string;\n    action: ActionType;\n    title: string;\n    isPublished?: boolean;\n    onSuccess?: () => void;\n    className?: string;\n    variant?: ButtonVariant;\n    disabled?: boolean;\n    size?: ButtonSize;\n}\n\nexport function ChangelogActionRequest({\n                                           projectId,\n                                           entryId,\n                                           action,\n                                           title,\n                                           isPublished = false,\n                                           onSuccess,\n                                           className,\n                                           variant = 'default',\n                                           disabled = false,\n                                           size = 'default'\n                                       }: ChangelogActionRequestProps) {\n    const {user} = useAuth();\n    const {toast} = useToast();\n    const queryClient = useQueryClient();\n    const [isOpen, setIsOpen] = useState(false);\n    const [isSubmitting, setIsSubmitting] = useState(false);\n\n    // Email notification state\n    const [sendEmails, setSendEmails] = useState(false);\n    const [recipientType, setRecipientType] = useState<RecipientType>('SUBSCRIBERS');\n    const [subscriptionTypes, setSubscriptionTypes] = useState<SubscriptionType[]>(['ALL_UPDATES']);\n\n    // Custom publish date state\n    const [useCustomPublishedAt, setUseCustomPublishedAt] = useState(false);\n    const [customPublishedAt, setCustomPublishedAt] = useState<string>('');\n\n    const isAdmin = user?.role === 'ADMIN';\n    const isStaff = user?.role === 'STAFF';\n    const canPerformAction = isAdmin || isStaff;\n\n    // Map action to request type for checking pending requests\n    const getRequestType = (actionType: ActionType): RequestType | null => {\n        const mapping: Record<ActionType, RequestType | null> = {\n            'PUBLISH': 'ALLOW_PUBLISH',\n            'DELETE': 'DELETE_ENTRY',\n            'ALLOW_SCHEDULE': 'ALLOW_SCHEDULE',\n            'UNPUBLISH': null // Unpublish doesn't create requests\n        };\n        return mapping[actionType];\n    };\n\n    // Fetch pending requests for this entry\n    const {data: pendingRequests = []} = useQuery<PendingRequest[]>({\n        queryKey: ['pending-requests', projectId, entryId],\n        queryFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}/changelog/${entryId}/requests`);\n            if (!response.ok) return [];\n            return response.json();\n        },\n        enabled: canPerformAction && !!entryId\n    });\n\n    // Check project settings for approval requirement\n    const {data: projectSettings} = useQuery({\n        queryKey: ['project-settings', projectId],\n        queryFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}/settings`);\n            if (!response.ok) return null;\n            return response.json();\n        },\n        enabled: action === 'PUBLISH' && isStaff\n    });\n\n    // Check if email notifications are enabled\n    const {data: emailConfig} = useQuery({\n        queryKey: ['email-config', projectId],\n        queryFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}/integrations/email`);\n            if (!response.ok) return null;\n            return response.json();\n        },\n        enabled: action === 'PUBLISH'\n    });\n\n    // Check if there's a pending request for this specific action\n    const requestType = getRequestType(action);\n    const pendingRequest = requestType\n        ? pendingRequests.find(req => req.type === requestType)\n        : null;\n\n    const requiresApproval = isStaff && projectSettings?.requireApproval && !projectSettings?.allowAutoPublish;\n    const showEmailOptions = action === 'PUBLISH' && emailConfig?.enabled && !requiresApproval;\n\n    // Handle subscription type changes\n    const handleSubscriptionTypeChange = (type: SubscriptionType, checked: boolean) => {\n        if (checked) {\n            setSubscriptionTypes(prev => [...prev, type]);\n        } else {\n            setSubscriptionTypes(prev => prev.filter(t => t !== type));\n        }\n    };\n\n    // Update entry mutation\n    const updateEntry = useMutation({\n        mutationFn: async () => {\n            setIsSubmitting(true);\n            try {\n                const payload: {action: string; publishedAt?: string} = {\n                    action: action.toLowerCase()\n                };\n\n                if (action === 'PUBLISH' && useCustomPublishedAt && customPublishedAt) {\n                    payload.publishedAt = new Date(customPublishedAt).toISOString();\n                }\n\n                const response = await fetch(`/api/projects/${projectId}/changelog/${entryId}`, {\n                    method: 'PATCH',\n                    headers: {'Content-Type': 'application/json'},\n                    body: JSON.stringify(payload)\n                });\n\n                if (!response.ok) {\n                    const error = await response.json();\n                    throw new Error(error.error || `Failed to ${action.toLowerCase()} entry`);\n                }\n\n                const publishResult = await response.json();\n\n                // Send emails if it's a direct publish (not requiring approval)\n                if (action === 'PUBLISH' && sendEmails && showEmailOptions && !publishResult.requiresApproval) {\n                    try {\n                        const emailResponse = await fetch(`/api/projects/${projectId}/integrations/email/send`, {\n                            method: 'POST',\n                            headers: {'Content-Type': 'application/json'},\n                            body: JSON.stringify({\n                                subject: `New Update - ${title}`,\n                                changelogEntryId: entryId,\n                                recipientType,\n                                subscriptionTypes\n                            })\n                        });\n\n                        if (!emailResponse.ok) {\n                            console.warn('Failed to send email notifications, but publish succeeded');\n                        }\n                    } catch (emailError) {\n                        console.warn('Email sending failed:', emailError);\n                    }\n                }\n\n                return publishResult;\n            } finally {\n                setIsSubmitting(false);\n            }\n        },\n        onSuccess: (data) => {\n            queryClient.invalidateQueries({queryKey: ['changelog-entry', entryId]});\n            queryClient.invalidateQueries({queryKey: ['changelog-entries', projectId]});\n            queryClient.invalidateQueries({queryKey: ['pending-requests', projectId, entryId]});\n            queryClient.setQueryData(['changelog-entry', entryId], data);\n\n            if (data.requiresApproval) {\n                toast({\n                    title: 'Request Submitted',\n                    description: 'Your request has been sent to an administrator for approval.',\n                    duration: 4000\n                });\n            } else {\n                const actionMessages = {\n                    'PUBLISH': {\n                        title: 'Entry Published',\n                        description: sendEmails && showEmailOptions\n                            ? 'Entry published and email notifications sent.'\n                            : 'Entry published successfully.'\n                    },\n                    'UNPUBLISH': {\n                        title: 'Entry Unpublished',\n                        description: 'Entry has been unpublished.'\n                    },\n                    'ALLOW_SCHEDULE': {\n                        title: 'Schedule Allowed',\n                        description: 'Entry approved for scheduling.'\n                    }\n                };\n\n                const message = actionMessages[action as keyof typeof actionMessages];\n                toast({\n                    title: message.title,\n                    description: message.description\n                });\n            }\n\n            setIsOpen(false);\n            onSuccess?.();\n        },\n        onError: (error: Error) => {\n            toast({\n                title: `Failed to ${action.toLowerCase().replace('_', ' ')}`,\n                description: error.message,\n                variant: 'destructive'\n            });\n            setIsOpen(false);\n        }\n    });\n\n    // Delete entry mutation\n    const deleteEntry = useMutation({\n        mutationFn: async () => {\n            setIsSubmitting(true);\n            try {\n                const response = await fetch(`/api/projects/${projectId}/changelog/${entryId}`, {\n                    method: 'DELETE'\n                });\n\n                if (!response.ok) {\n                    const error = await response.json();\n                    throw new Error(error.error || 'Failed to process deletion');\n                }\n\n                const data = await response.json();\n                return {data, status: response.status};\n            } finally {\n                setIsSubmitting(false);\n            }\n        },\n        onSuccess: (result) => {\n            queryClient.invalidateQueries({queryKey: ['pending-requests', projectId, entryId]});\n\n            if (isStaff && result.status === 202) {\n                toast({\n                    title: 'Deletion Request Submitted',\n                    description: 'Your request has been sent for approval.',\n                    duration: 4000\n                });\n            } else {\n                queryClient.invalidateQueries({queryKey: ['changelog-entries', projectId]});\n                queryClient.removeQueries({queryKey: ['changelog-entry', entryId]});\n                toast({\n                    title: 'Entry Deleted',\n                    description: 'Entry has been deleted successfully.'\n                });\n            }\n            setIsOpen(false);\n            onSuccess?.();\n        },\n        onError: (error: Error) => {\n            toast({\n                title: 'Error',\n                description: error.message || 'Failed to process request',\n                variant: 'destructive'\n            });\n            setIsOpen(false);\n        }\n    });\n\n    if (!canPerformAction) return null;\n\n    const handleAction = () => {\n        if (action === 'DELETE') {\n            deleteEntry.mutate();\n        } else {\n            updateEntry.mutate();\n        }\n    };\n\n    const getButtonConfig = () => {\n        const isPending = !!pendingRequest;\n\n        const configs = {\n            'PUBLISH': {\n                icon: isPending ? Clock : Globe,\n                label: isPending ? 'Publish Pending' : 'Publish',\n                loadingLabel: 'Publishing...',\n                variant: (isPending ? 'secondary' : (variant || 'default')) as ButtonVariant,\n                disabled: isPending\n            },\n            'UNPUBLISH': {\n                icon: PackageOpen,\n                label: 'Unpublish',\n                loadingLabel: 'Unpublishing...',\n                variant: (variant || 'outline') as ButtonVariant,\n                disabled: false\n            },\n            'DELETE': {\n                icon: isPending ? Clock : Trash2,\n                label: isPending ? 'Delete Pending' : 'Delete',\n                loadingLabel: 'Processing...',\n                variant: (isPending ? 'secondary' : 'destructive') as ButtonVariant,\n                disabled: isPending\n            },\n            'ALLOW_SCHEDULE': {\n                icon: isPending ? Clock : Calendar,\n                label: isPending ? 'Schedule Pending' : 'Allow Schedule',\n                loadingLabel: 'Processing...',\n                variant: (isPending ? 'secondary' : (variant || 'default')) as ButtonVariant,\n                disabled: isPending\n            }\n        };\n        return configs[action];\n    };\n\n    const config = getButtonConfig();\n    const IconComponent = config.icon;\n\n    // Don't render publish button if already published\n    if (action === 'PUBLISH' && isPublished) return null;\n\n    const renderButton = () => (\n        <Button\n            onClick={() => setIsOpen(true)}\n            disabled={disabled || isSubmitting || config.disabled}\n            variant={config.variant}\n            size={size}\n            className={cn(\"gap-2\", className, config.disabled && \"opacity-75\")}\n        >\n            {isSubmitting ? (\n                <>\n                    <Loader2 className=\"h-4 w-4 animate-spin\"/>\n                    {config.loadingLabel}\n                </>\n            ) : (\n                <>\n                    <IconComponent className=\"h-4 w-4\"/>\n                    {config.label}\n                </>\n            )}\n        </Button>\n    );\n\n    const getDialogContent = () => {\n        if (pendingRequest) {\n            const requestDate = new Date(pendingRequest.createdAt).toLocaleDateString();\n            return {\n                title: `${action.charAt(0) + action.slice(1).toLowerCase().replace('_', ' ')} Request Pending`,\n                description: `A ${action.toLowerCase()} request for this entry is already pending approval (submitted ${requestDate} by ${pendingRequest.staff.name}).`,\n                showForm: false,\n                isPending: true\n            };\n        }\n\n        const descriptions = {\n            'PUBLISH': requiresApproval\n                ? 'This will send a publish request to administrators for approval.'\n                : 'This entry will be visible to all users.',\n            'UNPUBLISH': 'This entry will no longer be visible to users.',\n            'DELETE': isStaff\n                ? 'This will send a deletion request to administrators for approval.'\n                : 'This action cannot be undone.',\n            'ALLOW_SCHEDULE': 'This will allow the entry to be scheduled for future publication.'\n        };\n\n        return {\n            title: `${action === 'DELETE' && isStaff ? 'Request' : ''} ${action.charAt(0) + action.slice(1).toLowerCase().replace('_', ' ')} Entry`,\n            description: descriptions[action],\n            showForm: true,\n            isPending: false\n        };\n    };\n\n    const dialogContent = getDialogContent();\n\n    return (\n        <Dialog open={isOpen} onOpenChange={setIsOpen}>\n            <DialogTrigger asChild>\n                {renderButton()}\n            </DialogTrigger>\n            <DialogContent className=\"max-w-lg\">\n                <DialogHeader>\n                    <DialogTitle className=\"flex items-center gap-2\">\n                        <IconComponent className={cn(\"w-5 h-5\", dialogContent.isPending && \"text-amber-500\")}/>\n                        {dialogContent.title}\n                    </DialogTitle>\n                    <DialogDescription>\n                        {dialogContent.description}\n                    </DialogDescription>\n                </DialogHeader>\n\n                {dialogContent.isPending && (\n                    <Card className=\"border-amber-200 bg-amber-50\">\n                        <CardContent className=\"pt-4\">\n                            <div className=\"flex items-center gap-3\">\n                                <div className=\"flex-shrink-0\">\n                                    <Clock className=\"w-5 h-5 text-amber-600\"/>\n                                </div>\n                                <div>\n                                    <p className=\"text-sm font-medium text-amber-800\">\n                                        Request awaiting approval\n                                    </p>\n                                    <p className=\"text-xs text-amber-700 mt-1\">\n                                        Submitted\n                                        by {pendingRequest?.staff.name} on {new Date(pendingRequest?.createdAt || '').toLocaleDateString()}\n                                    </p>\n                                </div>\n                            </div>\n                        </CardContent>\n                    </Card>\n                )}\n\n                {dialogContent.showForm && (\n                    <div className=\"space-y-4\">\n                        {/* Entry Details Card */}\n                        <Card className=\"border-dashed\">\n                            <CardHeader className=\"pb-3\">\n                                <div className=\"flex items-center gap-2\">\n                                    <div className=\"w-2 h-2 bg-blue-500 rounded-full\"/>\n                                    <span className=\"font-medium text-sm\">Entry Details</span>\n                                </div>\n                            </CardHeader>\n                            <CardContent className=\"pt-0\">\n                                <p className=\"text-sm font-medium\" title={title}>{truncateText(title, 50)}</p>\n                                <div className=\"flex items-center gap-2 mt-2\">\n                                    <Badge variant={isPublished ? 'default' : 'secondary'} className=\"text-xs\">\n                                        {isPublished ? (\n                                            <>\n                                                <CheckCircle className=\"w-3 h-3 mr-1\"/>\n                                                Published\n                                            </>\n                                        ) : (\n                                            <>\n                                                <Ban className=\"w-3 h-3 mr-1\"/>\n                                                Draft\n                                            </>\n                                        )}\n                                    </Badge>\n                                    {requiresApproval && (\n                                        <Badge variant=\"outline\" className=\"text-xs\">\n                                            <AlertTriangle className=\"w-3 h-3 mr-1\"/>\n                                            Requires Approval\n                                        </Badge>\n                                    )}\n                                </div>\n                            </CardContent>\n                        </Card>\n\n                        {/* Custom Publish Date */}\n                        {action === 'PUBLISH' && (\n                            <Card className=\"border border-amber-200 bg-gradient-to-br from-amber-50/40 to-orange-50/20\">\n                                <CardHeader>\n                                    <div className=\"flex items-center justify-between\">\n                                        <div className=\"flex items-center gap-3\">\n                                            <div className=\"p-2 bg-amber-100/60 rounded-lg\">\n                                                <Calendar className=\"w-4 h-4 text-amber-700\"/>\n                                            </div>\n                                            <div className=\"flex-1\">\n                                                <span className=\"font-semibold text-sm text-amber-950\">Custom Publish Date</span>\n                                                <p className=\"text-xs text-amber-700/70\">Set a specific date instead of\n                                                    now</p>\n                                            </div>\n                                        </div>\n                                        <Checkbox\n                                            checked={useCustomPublishedAt}\n                                            onCheckedChange={(checked) => setUseCustomPublishedAt(checked === true)}\n                                            className=\"border-amber-400\"\n                                        />\n                                    </div>\n                                </CardHeader>\n\n                                {useCustomPublishedAt && (\n                                    <CardContent className=\"pt-0 space-y-4\">\n                                        <div className=\"space-y-2.5\">\n                                            <Label htmlFor=\"publishedAt\"\n                                                   className=\"text-sm font-medium text-gray-900\">Publish Date & Time</Label>\n                                            <Input\n                                                id=\"publishedAt\"\n                                                type=\"datetime-local\"\n                                                value={customPublishedAt}\n                                                onChange={(e) => setCustomPublishedAt(e.target.value)}\n                                                className=\"text-sm border-amber-200 focus:border-amber-400 focus:ring-amber-100\"\n                                            />\n                                        </div>\n\n                                        <div className=\"p-3 bg-amber-50 border border-amber-200/60 rounded-lg flex gap-3\">\n                                            <AlertCircle className=\"w-4 h-4 text-amber-700 flex-shrink-0 mt-0.5\"/>\n                                            <div>\n                                                <p className=\"text-xs font-medium text-amber-900\">\n                                                    Backdating changes history\n                                                </p>\n                                                <p className=\"text-xs text-amber-700 mt-1\">\n                                                    Setting a custom date may not accurately reflect when this update was actually published. Use this responsibly.\n                                                </p>\n                                            </div>\n                                        </div>\n                                    </CardContent>\n                                )}\n                            </Card>\n                        )}\n\n                        {/* Email Options */}\n                        {showEmailOptions && (\n                            <Card className=\"border-dashed border-blue-200 bg-blue-50/30\">\n                                <CardHeader className=\"pb-3\">\n                                    <div className=\"flex items-center justify-between\">\n                                        <div className=\"flex items-center gap-2\">\n                                            <Mail className=\"w-4 h-4 text-blue-600\"/>\n                                            <span\n                                                className=\"font-medium text-sm text-blue-900\">Email Notifications</span>\n                                            <Badge variant=\"outline\" className=\"text-xs text-blue-700 border-blue-300\">\n                                                Optional\n                                            </Badge>\n                                        </div>\n                                        <Checkbox\n                                            checked={sendEmails}\n                                            onCheckedChange={(checked) => setSendEmails(checked === true)}\n                                            className=\"border-blue-400\"\n                                        />\n                                    </div>\n                                </CardHeader>\n\n                                {sendEmails && (\n                                    <CardContent className=\"pt-0 space-y-4\">\n                                        <div className=\"space-y-3\">\n                                            <div>\n                                                <Label\n                                                    className=\"text-xs font-medium text-gray-700 uppercase tracking-wide\">Recipients</Label>\n                                                <RadioGroup\n                                                    value={recipientType}\n                                                    onValueChange={(value: RecipientType) => setRecipientType(value)}\n                                                    className=\"mt-2 space-y-2\"\n                                                >\n                                                    <div className=\"flex items-center space-x-3\">\n                                                        <RadioGroupItem value=\"SUBSCRIBERS\" id=\"subscribers\"\n                                                                        className=\"w-4 h-4\"/>\n                                                        <Label htmlFor=\"subscribers\"\n                                                               className=\"text-sm flex items-center gap-2\">\n                                                            <Users className=\"w-4 h-4 text-gray-500\"/>\n                                                            Subscribers only\n                                                        </Label>\n                                                    </div>\n                                                    <div className=\"flex items-center space-x-3\">\n                                                        <RadioGroupItem value=\"MANUAL\" id=\"manual\" className=\"w-4 h-4\"/>\n                                                        <Label htmlFor=\"manual\"\n                                                               className=\"text-sm flex items-center gap-2\">\n                                                            <Mail className=\"w-4 h-4 text-gray-500\"/>\n                                                            Manual recipients\n                                                        </Label>\n                                                    </div>\n                                                    <div className=\"flex items-center space-x-3\">\n                                                        <RadioGroupItem value=\"BOTH\" id=\"both\" className=\"w-4 h-4\"/>\n                                                        <Label htmlFor=\"both\" className=\"text-sm\">Both subscribers &\n                                                            manual</Label>\n                                                    </div>\n                                                </RadioGroup>\n                                            </div>\n\n                                            <Separator className=\"opacity-30\"/>\n\n                                            <div>\n                                                <Label\n                                                    className=\"text-xs font-medium text-gray-700 uppercase tracking-wide\">Subscription\n                                                    Types</Label>\n                                                <div className=\"mt-2 space-y-2\">\n                                                    {[\n                                                        {\n                                                            id: 'ALL_UPDATES',\n                                                            label: 'All Updates',\n                                                            desc: 'Every changelog entry'\n                                                        },\n                                                        {\n                                                            id: 'MAJOR_ONLY',\n                                                            label: 'Major Updates',\n                                                            desc: 'Important releases only'\n                                                        },\n                                                        {\n                                                            id: 'DIGEST_ONLY',\n                                                            label: 'Digest Emails',\n                                                            desc: 'Weekly/monthly summaries'\n                                                        }\n                                                    ].map((type) => (\n                                                        <div key={type.id} className=\"flex items-start space-x-3\">\n                                                            <Checkbox\n                                                                id={type.id}\n                                                                className=\"w-4 h-4 mt-0.5\"\n                                                                checked={subscriptionTypes.includes(type.id as SubscriptionType)}\n                                                                onCheckedChange={(checked) =>\n                                                                    handleSubscriptionTypeChange(type.id as SubscriptionType, checked === true)\n                                                                }\n                                                            />\n                                                            <div className=\"flex-1 min-w-0\">\n                                                                <Label htmlFor={type.id}\n                                                                       className=\"text-sm font-medium\">\n                                                                    {type.label}\n                                                                </Label>\n                                                                <p className=\"text-xs text-gray-500 mt-0.5\">\n                                                                    {type.desc}\n                                                                </p>\n                                                            </div>\n                                                        </div>\n                                                    ))}\n                                                </div>\n                                            </div>\n                                        </div>\n                                    </CardContent>\n                                )}\n                            </Card>\n                        )}\n                    </div>\n                )}\n\n                <DialogFooter>\n                    <Button variant=\"outline\" onClick={() => setIsOpen(false)}>\n                        Cancel\n                    </Button>\n\n                    {dialogContent.showForm && (\n                        <Button\n                            onClick={handleAction}\n                            disabled={disabled || isSubmitting}\n                            variant={action === 'DELETE' ? 'destructive' : 'default'}\n                            className=\"gap-2\"\n                        >\n                            {isSubmitting ? (\n                                <>\n                                    <Loader2 className=\"w-4 h-4 animate-spin\"/>\n                                    {config.loadingLabel}\n                                </>\n                            ) : (\n                                <>\n                                    <CheckCircle className=\"w-4 h-4\"/>\n                                    Confirm {action.charAt(0) + action.slice(1).toLowerCase().replace('_', ' ')}\n                                </>\n                            )}\n                        </Button>\n                    )}\n                </DialogFooter>\n            </DialogContent>\n        </Dialog>\n    );\n}"
  },
  {
    "path": "components/changelog/ChangelogEditor.tsx",
    "content": "import React, {useEffect, useMemo, useState, useCallback, useRef} from 'react';\nimport {useRouter, useSearchParams} from 'next/navigation';\nimport {useMutation, useQuery, useInfiniteQuery, useQueryClient} from '@tanstack/react-query';\nimport {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card';\nimport {Input} from '@/components/ui/input';\nimport {Button} from '@/components/ui/button';\nimport {useDebounce} from 'use-debounce';\nimport {toast} from \"@/hooks/use-toast\";\nimport EditorHeader from '@/components/changelog/editor/EditorHeader';\nimport {Loader2, RefreshCw, Save, ExternalLink} from 'lucide-react';\nimport {Alert, AlertDescription, AlertActions, AlertTitle} from '@/components/ui/alert';\nimport {motion, AnimatePresence} from 'framer-motion';\nimport {cn} from '@/lib/utils';\nimport {MarkdownEditor} from \"@/components/markdown-editor\";\n\n// ===== Type Definitions =====\n\ninterface Tag {\n    id: string;\n    name: string;\n}\n\ninterface ProjectResponse {\n    id: string;\n    defaultTags: string[];\n    name: string;\n    requireApproval: boolean;\n    allowAutoPublish: boolean;\n}\n\ninterface EntryResponse {\n    id: string;\n    title: string;\n    content: string;\n    version: string;\n    tags: Tag[];\n    publishedAt?: string;\n    createdAt: string;\n    updatedAt: string;\n}\n\ninterface TagsResponse {\n    tags: Tag[];\n    pagination: {\n        page: number;\n        hasMore: boolean;\n        totalCount: number;\n    };\n}\n\ninterface AISystemSettings {\n    enableAIAssistant: boolean;\n    aiApiKey: string | null;\n    aiDefaultModel: string | null;\n}\n\ninterface SaveErrorDetails {\n    message: string;\n    code?: string;\n    details?: unknown;\n    timestamp: Date;\n    retryable: boolean;\n}\n\ninterface EditorState {\n    title: string;\n    content: string;\n    version: string;\n    tags: Tag[];\n    isPublished: boolean;\n    hasUnsavedChanges: boolean;\n    hasVersionConflict: boolean;\n}\n\ninterface EditorStatus {\n    isSaving: boolean;\n    isAutoSaving: boolean;\n    lastSaveError: SaveErrorDetails | null;\n    lastSavedTime: Date | null;\n    saveAttempts: number;\n    canRetry: boolean;\n}\n\ninterface SaveError extends Error {\n    status?: number;\n    details?: unknown;\n}\n\n// WWC Protocol types - for receiving data from external sources\ninterface WWCProtocolData {\n    serverUrl?: string;\n    instanceType?: 'changerawr';\n    title?: string;\n    content?: string;\n    version?: string;\n    tags?: string[];\n    projectId?: string;\n    entryId?: string;\n    action?: 'edit' | 'create' | 'view';\n}\n\ninterface WWCProtocolState {\n    data: WWCProtocolData | null;\n    isChangerawrInstance: boolean;\n    serverUrl: string | null;\n    appliedAt: number | null;\n}\n\n// ===== Constants =====\n\nconst ITEMS_PER_PAGE = 20;\nconst CACHE_TIME = 1000 * 60 * 5; // 5 minutes\nconst DEBOUNCE_TIME = 1000; // 1 second (for small content)\nconst DEBOUNCE_TIME_MEDIUM = 2000; // 2 seconds (for medium content 1000-3000 words)\nconst DEBOUNCE_TIME_LARGE = 5000; // 5 seconds (for large content 3000+ words)\nconst MAX_RETRY_ATTEMPTS = 3;\nconst RETRY_DELAY = 2000; // 2 seconds\n\n// Helper function to calculate dynamic debounce time based on content size\nconst getDynamicDebounceTime = (content: string): number => {\n    const wordCount = content.trim() ? content.trim().split(/\\s+/).length : 0;\n    if (wordCount > 3000) return DEBOUNCE_TIME_LARGE;\n    if (wordCount > 1000) return DEBOUNCE_TIME_MEDIUM;\n    return DEBOUNCE_TIME;\n};\n\n// ===== Enhanced EditorHeader Wrapper =====\n\ninterface EnhancedEditorHeaderProps extends React.ComponentProps<typeof EditorHeader> {\n    onLoadMoreTags?: () => Promise<void>;\n    onVersionConflict?: (hasConflict: boolean) => void;\n    hasVersionConflict?: boolean;\n}\n\nconst EnhancedEditorHeader: React.FC<EnhancedEditorHeaderProps> = ({\n                                                                       onLoadMoreTags,\n                                                                       onVersionConflict,\n                                                                       hasVersionConflict,\n                                                                       availableTags,\n                                                                       onTagsChange,\n                                                                       content,\n                                                                       ...otherProps\n                                                                   }) => {\n    const tagsContainerRef = useRef<HTMLDivElement>(null);\n\n    const handleTagsChange = useCallback((tags: Tag[]) => {\n        onTagsChange(tags);\n    }, [onTagsChange]);\n\n    // Setup intersection observer for infinite tag loading\n    useEffect(() => {\n        if (!onLoadMoreTags) return;\n\n        const containerElement = tagsContainerRef.current;\n        if (!containerElement) return;\n\n        const observer = new IntersectionObserver(\n            (entries) => {\n                if (entries[0]?.isIntersecting) {\n                    onLoadMoreTags();\n                }\n            },\n            {threshold: 0.5}\n        );\n\n        observer.observe(containerElement);\n\n        return () => {\n            observer.unobserve(containerElement);\n        };\n    }, [onLoadMoreTags]);\n\n    return (\n        <>\n            <EditorHeader\n                {...otherProps}\n                availableTags={availableTags}\n                onTagsChange={handleTagsChange}\n                content={content}\n                onVersionConflict={onVersionConflict}\n                hasVersionConflict={hasVersionConflict}\n            />\n            {/* Hidden container to trigger infinite loading */}\n            {onLoadMoreTags && <div ref={tagsContainerRef} style={{height: 1, opacity: 0}}/>}\n        </>\n    );\n};\n\n// ===== Main Component =====\n\ninterface ChangelogEditorProps {\n    projectId: string;\n    entryId?: string;\n    isNewChangelog?: boolean;\n    initialPublishedStatus?: boolean;\n    initialContent?: string;\n    initialVersion?: string;\n    initialTitle?: string;\n}\n\nexport function ChangelogEditor({\n                                    projectId,\n                                    entryId,\n                                    isNewChangelog = false,\n                                    initialPublishedStatus = false,\n                                    initialContent = '',\n                                    initialVersion = '',\n                                    initialTitle = '',\n                                }: ChangelogEditorProps) {\n    const router = useRouter();\n    const queryClient = useQueryClient();\n    const searchParams = useSearchParams();\n\n    // ===== Refs =====\n    const initialValuesApplied = useRef(false);\n    const lastSavedStateRef = useRef<Omit<EditorState, 'isPublished' | 'hasUnsavedChanges' | 'hasVersionConflict'> | null>(null);\n    const retryTimeoutRef = useRef<NodeJS.Timeout | null>(null);\n\n    // ===== WWC Protocol State =====\n    const [wwcState, setWwcState] = useState<WWCProtocolState>({\n        data: null,\n        isChangerawrInstance: false,\n        serverUrl: null,\n        appliedAt: null\n    });\n\n    // ===== State Management =====\n    const [editorState, setEditorState] = useState<EditorState>(() => ({\n        title: initialTitle,\n        content: initialContent,\n        version: initialVersion,\n        tags: [],\n        isPublished: initialPublishedStatus,\n        hasUnsavedChanges: !!(initialTitle || initialContent || initialVersion),\n        hasVersionConflict: false,\n    }));\n\n    const [status, setStatus] = useState<EditorStatus>({\n        isSaving: false,\n        isAutoSaving: false,\n        lastSaveError: null,\n        lastSavedTime: null,\n        saveAttempts: 0,\n        canRetry: false,\n    });\n\n    // Debounced state for autosave (dynamic delay based on content size)\n    const dynamicDebounceTime = getDynamicDebounceTime(editorState.content);\n    const [debouncedState] = useDebounce(editorState, dynamicDebounceTime);\n\n    // ===== WWC Protocol Data Loading =====\n    useEffect(() => {\n        const loadWWCData = () => {\n            // Check URL parameters for WWC protocol data\n            const urlTitle = searchParams?.get('title');\n            const urlContent = searchParams?.get('content');\n            const urlVersion = searchParams?.get('version');\n            const urlTags = searchParams?.get('tags');\n            const urlServerUrl = searchParams?.get('serverUrl');\n            const urlInstanceType = searchParams?.get('instanceType');\n\n            const hasUrlData = !!(urlTitle || urlContent || urlVersion || urlTags);\n\n            if (hasUrlData) {\n                const mergedData: WWCProtocolData = {\n                    title: urlTitle || '',\n                    content: urlContent || '',\n                    version: urlVersion || '',\n                    tags: urlTags ? urlTags.split(',') : [],\n                    serverUrl: urlServerUrl || undefined,\n                    instanceType: (urlInstanceType || 'changerawr') as 'changerawr',\n                    projectId: projectId,\n                    entryId: entryId,\n                    action: 'create'\n                };\n\n                setWwcState({\n                    data: mergedData,\n                    isChangerawrInstance: mergedData.instanceType === 'changerawr',\n                    serverUrl: mergedData.serverUrl || null,\n                    appliedAt: Date.now()\n                });\n\n                // Apply to editor state\n                setEditorState(prev => ({\n                    ...prev,\n                    title: mergedData.title || prev.title,\n                    content: mergedData.content || prev.content,\n                    version: mergedData.version || prev.version,\n                    hasUnsavedChanges: true\n                }));\n\n                // Show notification\n                const sourceText = mergedData.serverUrl\n                    ? `from ${mergedData.serverUrl}`\n                    : 'from external source';\n\n                toast({\n                    title: \"External content loaded\",\n                    description: `Content has been loaded ${sourceText}.`,\n                    duration: 4000\n                });\n            }\n        };\n\n        // Only run once when component mounts\n        if (!wwcState.appliedAt) {\n            loadWWCData();\n        }\n    }, [searchParams, projectId, entryId, wwcState.appliedAt]);\n\n    // ===== Data Fetching =====\n\n    // Fetch AI system settings\n    const { data: aiSystemSettings, isLoading: isAISettingsLoading } = useQuery<AISystemSettings>({\n        queryKey: ['ai-system-settings'],\n        queryFn: async (): Promise<AISystemSettings> => {\n            try {\n                // Fetch encrypted settings\n                const response = await fetch('/api/ai/settings');\n                if (!response.ok) {\n                    console.warn('Failed to fetch AI settings:', response.statusText);\n                    return { enableAIAssistant: false, aiApiKey: null, aiDefaultModel: null };\n                }\n\n                const encryptedData = await response.json();\n\n                // If no API key or AI disabled, return as-is\n                if (!encryptedData.enableAIAssistant || !encryptedData.aiApiKey) {\n                    return {\n                        enableAIAssistant: encryptedData.enableAIAssistant || false,\n                        aiApiKey: null,\n                        aiDefaultModel: encryptedData.aiDefaultModel || null\n                    };\n                }\n\n                // Decrypt the API key\n                const decryptResponse = await fetch('/api/ai/decrypt', {\n                    method: 'POST',\n                    headers: {\n                        'Content-Type': 'application/json',\n                    },\n                    body: JSON.stringify({ encryptedToken: encryptedData.aiApiKey }),\n                });\n\n                let decryptedApiKey: string | null = null;\n\n                if (decryptResponse.ok) {\n                    const decryptData = await decryptResponse.json();\n                    decryptedApiKey = decryptData.decryptedKey;\n                } else {\n                    console.error('Failed to decrypt API key:', decryptResponse.statusText);\n                }\n\n                return {\n                    enableAIAssistant: encryptedData.enableAIAssistant || false,\n                    aiApiKey: decryptedApiKey,\n                    aiDefaultModel: encryptedData.aiDefaultModel || null\n                };\n\n            } catch (error) {\n                console.error('Error in AI settings query:', error);\n                return { enableAIAssistant: false, aiApiKey: null, aiDefaultModel: null };\n            }\n        },\n        staleTime: CACHE_TIME,\n        retry: 1,\n    });\n\n    // Fetch initial data (project and entry)\n    const {data: initialData, isLoading: isInitialDataLoading, error: initialDataError} = useQuery({\n        queryKey: ['changelog-init', projectId, entryId],\n        queryFn: async () => {\n            const [projectResponse, entryResponse] = await Promise.all([\n                fetch(`/api/projects/${projectId}`),\n                entryId ? fetch(`/api/projects/${projectId}/changelog/${entryId}`) : Promise.resolve(null)\n            ]);\n\n            if (!projectResponse.ok) {\n                throw new Error(`Failed to fetch project: ${projectResponse.statusText}`);\n            }\n\n            const project: ProjectResponse = await projectResponse.json();\n            let entry: EntryResponse | null = null;\n\n            if (entryResponse) {\n                if (!entryResponse.ok) {\n                    throw new Error(`Failed to fetch entry: ${entryResponse.statusText}`);\n                }\n                entry = await entryResponse.json();\n            }\n\n            return {project, entry};\n        },\n        staleTime: CACHE_TIME,\n        retry: 2,\n    });\n\n    // Fetch tags with pagination\n    const {\n        data: tagsData,\n        fetchNextPage,\n        hasNextPage,\n        isLoading: isTagsLoading,\n        error: tagsError\n    } = useInfiniteQuery<TagsResponse>({\n        queryKey: ['changelog-tags', projectId],\n        queryFn: async ({pageParam = 1}) => {\n            const response = await fetch(\n                `/api/projects/${projectId}/changelog/tags?page=${pageParam}&limit=${ITEMS_PER_PAGE}`\n            );\n            if (!response.ok) {\n                throw new Error(`Failed to fetch tags: ${response.statusText}`);\n            }\n            return response.json();\n        },\n        getNextPageParam: (lastPage: TagsResponse) => {\n            return lastPage.pagination.hasMore ? lastPage.pagination.page + 1 : undefined;\n        },\n        staleTime: CACHE_TIME,\n        initialPageParam: 1,\n        retry: 2,\n    });\n\n    // ===== Computed Values =====\n    const aiEnabled = aiSystemSettings?.enableAIAssistant || false;\n    const sectonApiKey = aiSystemSettings?.aiApiKey || '';\n\n    const {availableTags, mappedDefaultTags} = useMemo(() => {\n        if (!initialData || !tagsData?.pages) {\n            return {availableTags: [], mappedDefaultTags: []};\n        }\n\n        const allTags = tagsData.pages.flatMap(page => page.tags);\n        const {project} = initialData;\n        const defaultTags = project.defaultTags || [];\n\n        const tagMap = new Map<string, Tag>();\n        const defaultTagObjects: Tag[] = [];\n\n        // Process existing tags\n        allTags.forEach(tag => {\n            const lowercaseName = tag.name.toLowerCase();\n            if (!tagMap.has(lowercaseName)) {\n                tagMap.set(lowercaseName, tag);\n            }\n        });\n\n        // Process default tags\n        defaultTags.forEach(name => {\n            const lowercaseName = name.toLowerCase();\n            if (!tagMap.has(lowercaseName)) {\n                const tag = {\n                    id: `default-${lowercaseName}`,\n                    name: name\n                };\n                tagMap.set(lowercaseName, tag);\n                defaultTagObjects.push(tag);\n            } else {\n                const existingTag = tagMap.get(lowercaseName);\n                if (existingTag) {\n                    defaultTagObjects.push(existingTag);\n                }\n            }\n        });\n\n        return {\n            availableTags: Array.from(tagMap.values()),\n            mappedDefaultTags: defaultTagObjects\n        };\n    }, [initialData, tagsData?.pages]);\n\n    // ===== Process WWC Tags =====\n    const processWWCTags = useCallback((wwcTags: string[], availableTags: Tag[]): Tag[] => {\n        if (!wwcTags || wwcTags.length === 0) return [];\n\n        const tagMap = new Map<string, Tag>();\n        availableTags.forEach(tag => {\n            tagMap.set(tag.name.toLowerCase(), tag);\n        });\n\n        return wwcTags.map(tagName => {\n            const normalizedName = tagName.trim();\n            const lowerName = normalizedName.toLowerCase();\n\n            // Check if tag exists\n            if (tagMap.has(lowerName)) {\n                return tagMap.get(lowerName)!;\n            }\n\n            // Create new tag\n            return {\n                id: `external-${lowerName}`,\n                name: normalizedName\n            };\n        });\n    }, []);\n\n    // Apply WWC Tags when available tags are loaded\n    useEffect(() => {\n        if (wwcState.data?.tags && availableTags.length > 0 && !editorState.tags.length) {\n            const processedTags = processWWCTags(wwcState.data.tags, availableTags);\n            if (processedTags.length > 0) {\n                setEditorState(prev => ({\n                    ...prev,\n                    tags: processedTags,\n                    hasUnsavedChanges: true\n                }));\n            }\n        }\n    }, [wwcState.data?.tags, availableTags, editorState.tags.length, processWWCTags]);\n\n    // ===== Save Mutation =====\n    const saveEntry = useMutation({\n        mutationFn: async (data: Omit<EditorState, 'isPublished' | 'hasUnsavedChanges' | 'hasVersionConflict'>) => {\n            const url = entryId\n                ? `/api/projects/${projectId}/changelog/${entryId}`\n                : `/api/projects/${projectId}/changelog`;\n\n            const tagData = data.tags.map(tag =>\n                tag.id.startsWith('default-') || tag.id.startsWith('external-')\n                    ? {name: tag.name}\n                    : {id: tag.id, name: tag.name}\n            );\n\n            const response = await fetch(url, {\n                method: entryId ? 'PUT' : 'POST',\n                headers: {\n                    'Content-Type': 'application/json',\n                    'Cache-Control': 'no-cache'\n                },\n                body: JSON.stringify({\n                    title: data.title,\n                    content: data.content,\n                    version: data.version,\n                    tags: tagData\n                })\n            });\n\n            if (!response.ok) {\n                const errorData = await response.json().catch(() => ({}));\n                const error: SaveError = new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n                error.status = response.status;\n                error.details = errorData;\n                throw error;\n            }\n\n            return response.json();\n        }\n    });\n\n    // ===== Event Handlers =====\n    const handleContentChange = useCallback((newContent: string) => {\n        setEditorState(prev => ({\n            ...prev,\n            content: newContent,\n            hasUnsavedChanges: true\n        }));\n    }, []);\n\n    const handleTitleChange = useCallback((newTitle: string) => {\n        setEditorState(prev => ({\n            ...prev,\n            title: newTitle,\n            hasUnsavedChanges: true\n        }));\n    }, []);\n\n    const handleVersionChange = useCallback((newVersion: string) => {\n        setEditorState(prev => ({\n            ...prev,\n            version: newVersion,\n            hasUnsavedChanges: true\n        }));\n    }, []);\n\n    const handleTagsChange = useCallback((newTags: Tag[]) => {\n        setEditorState(prev => ({\n            ...prev,\n            tags: newTags,\n            hasUnsavedChanges: true\n        }));\n    }, []);\n\n    const handleVersionConflict = useCallback((hasConflict: boolean) => {\n        setEditorState(prev => ({\n            ...prev,\n            hasVersionConflict: hasConflict\n        }));\n    }, []);\n\n    // ===== WWC URL Generation =====\n    const generateWWCUrl = useCallback(() => {\n        const baseUrl = 'wwc://open';\n        const url = new URL(baseUrl);\n\n        // Add path segments\n        if (projectId) {\n            url.pathname = `/${projectId}`;\n            if (entryId) {\n                url.pathname += `/${entryId}`;\n            }\n        }\n\n        // Add query parameters\n        url.searchParams.set('serverUrl', window.location.origin);\n        url.searchParams.set('instanceType', 'changerawr');\n\n        if (editorState.title) {\n            url.searchParams.set('title', encodeURIComponent(editorState.title));\n        }\n        if (editorState.content) {\n            url.searchParams.set('content', encodeURIComponent(editorState.content));\n        }\n        if (editorState.version) {\n            url.searchParams.set('version', encodeURIComponent(editorState.version));\n        }\n        if (editorState.tags && editorState.tags.length > 0) {\n            const tagsString = editorState.tags.map(tag => encodeURIComponent(tag.name)).join(',');\n            url.searchParams.set('tags', tagsString);\n        }\n\n        const wwcUrl = url.toString();\n\n        // Copy to clipboard\n        navigator.clipboard.writeText(wwcUrl).then(() => {\n            toast({\n                title: \"WWC URL generated\",\n                description: \"The protocol URL has been copied to your clipboard.\",\n            });\n        }).catch(() => {\n            toast({\n                title: \"Copy failed\",\n                description: \"Could not copy to clipboard. Please copy manually: \" + wwcUrl,\n                variant: \"destructive\"\n            });\n        });\n    }, [editorState, projectId, entryId]);\n\n    // ===== Save Logic =====\n    const performSave = useCallback(async (isManual = false) => {\n        const currentState = {\n            title: editorState.title,\n            content: editorState.content,\n            version: editorState.version,\n            tags: editorState.tags\n        };\n\n        // Validation checks\n        if (!currentState.title.trim()) {\n            const error: SaveErrorDetails = {\n                message: 'Title is required',\n                timestamp: new Date(),\n                retryable: false\n            };\n            setStatus(prev => ({...prev, lastSaveError: error}));\n            return false;\n        }\n\n        if (!currentState.content.trim()) {\n            const error: SaveErrorDetails = {\n                message: 'Content is required',\n                timestamp: new Date(),\n                retryable: false\n            };\n            setStatus(prev => ({...prev, lastSaveError: error}));\n            return false;\n        }\n\n        if (!currentState.version.trim()) {\n            const error: SaveErrorDetails = {\n                message: 'Version is required',\n                timestamp: new Date(),\n                retryable: false\n            };\n            setStatus(prev => ({...prev, lastSaveError: error}));\n            return false;\n        }\n\n        if (editorState.hasVersionConflict) {\n            const error: SaveErrorDetails = {\n                message: 'Version conflict must be resolved',\n                timestamp: new Date(),\n                retryable: false\n            };\n            setStatus(prev => ({...prev, lastSaveError: error}));\n            return false;\n        }\n\n        // Check if state actually changed\n        const stateChanged = !lastSavedStateRef.current ||\n            JSON.stringify(currentState) !== JSON.stringify(lastSavedStateRef.current);\n\n        if (!stateChanged && !isManual) {\n            return true; // No changes to save\n        }\n\n        // Update status\n        setStatus(prev => ({\n            ...prev,\n            isSaving: true,\n            isAutoSaving: !isManual,\n            lastSaveError: null,\n            saveAttempts: prev.saveAttempts + 1\n        }));\n\n        try {\n            const result = await saveEntry.mutateAsync(currentState);\n\n            // Handle navigation for new entries\n            if (!entryId && result.id) {\n                router.replace(`/dashboard/projects/${projectId}/changelog/${result.id}`);\n            }\n\n            // Update success state\n            lastSavedStateRef.current = currentState;\n            setStatus(prev => ({\n                ...prev,\n                isSaving: false,\n                isAutoSaving: false,\n                lastSavedTime: new Date(),\n                lastSaveError: null,\n                saveAttempts: 0,\n                canRetry: false\n            }));\n\n            setEditorState(prev => ({\n                ...prev,\n                hasUnsavedChanges: false\n            }));\n\n            // Invalidate relevant queries\n            queryClient.invalidateQueries({queryKey: ['project-versions', projectId]});\n\n            if (isManual) {\n                toast({\n                    title: \"Changes saved\",\n                    description: \"Your changes have been saved successfully.\"\n                });\n            }\n\n            return true;\n\n        } catch (error: unknown) {\n            console.error('Save error:', error);\n\n            const saveError = error as SaveError;\n            const isRetryable = saveError.status !== 400 && saveError.status !== 409 && saveError.status !== 422;\n            const errorDetails: SaveErrorDetails = {\n                message: saveError.message || 'Failed to save changes',\n                code: saveError.status?.toString(),\n                details: saveError.details,\n                timestamp: new Date(),\n                retryable: isRetryable && status.saveAttempts < MAX_RETRY_ATTEMPTS\n            };\n\n            setStatus(prev => ({\n                ...prev,\n                isSaving: false,\n                isAutoSaving: false,\n                lastSaveError: errorDetails,\n                canRetry: errorDetails.retryable\n            }));\n\n            if (isManual) {\n                toast({\n                    title: \"Save failed\",\n                    description: errorDetails.message,\n                    variant: \"destructive\"\n                });\n            }\n\n            return false;\n        }\n    }, [editorState, entryId, projectId, router, saveEntry, status.saveAttempts, queryClient]);\n\n    const handleManualSave = useCallback(async () => {\n        await performSave(true);\n    }, [performSave]);\n\n    const handleRetryAutosave = useCallback(async () => {\n        if (status.canRetry) {\n            setStatus(prev => ({...prev, lastSaveError: null}));\n            await performSave(false);\n        }\n    }, [performSave, status.canRetry]);\n\n    // ===== Auto-save Logic =====\n    useEffect(() => {\n        if (!debouncedState.hasUnsavedChanges || status.isSaving || status.lastSaveError?.retryable === false) {\n            return;\n        }\n\n        const currentState = {\n            title: debouncedState.title,\n            content: debouncedState.content,\n            version: debouncedState.version,\n            tags: debouncedState.tags\n        };\n\n        const stateChanged = !lastSavedStateRef.current ||\n            JSON.stringify(currentState) !== JSON.stringify(lastSavedStateRef.current);\n\n        if (!stateChanged || !currentState.title || !currentState.content || !currentState.version) {\n            return;\n        }\n\n        // Auto-save with delay\n        const timeoutId = setTimeout(() => {\n            performSave(false);\n        }, 500);\n\n        return () => clearTimeout(timeoutId);\n    }, [debouncedState, status.isSaving, status.lastSaveError, performSave]);\n\n    // ===== Auto-retry Logic =====\n    useEffect(() => {\n        if (status.lastSaveError?.retryable && status.saveAttempts < MAX_RETRY_ATTEMPTS) {\n            retryTimeoutRef.current = setTimeout(() => {\n                handleRetryAutosave();\n            }, RETRY_DELAY * status.saveAttempts); // Exponential backoff\n\n            return () => {\n                if (retryTimeoutRef.current) {\n                    clearTimeout(retryTimeoutRef.current);\n                }\n            };\n        }\n    }, [status.lastSaveError, status.saveAttempts, handleRetryAutosave]);\n\n    // ===== Initialize Editor State =====\n    useEffect(() => {\n        if (!initialData) return;\n\n        const {entry} = initialData;\n\n        if (entry) {\n            // Load existing entry data\n            const entryTags = entry.tags || [];\n            const formattedTags = entryTags.map(tag => ({\n                ...tag,\n                name: tag.name.charAt(0).toUpperCase() + tag.name.slice(1).toLowerCase()\n            }));\n\n            setEditorState({\n                title: entry.title || '',\n                content: entry.content || '',\n                version: entry.version || '',\n                tags: formattedTags,\n                isPublished: !!entry.publishedAt,\n                hasUnsavedChanges: false,\n                hasVersionConflict: false\n            });\n            initialValuesApplied.current = true;\n            return;\n        }\n\n        // Initialize new entry\n        if (isNewChangelog && !initialValuesApplied.current) {\n            const hasInitialValues = !!(initialTitle || initialContent || initialVersion);\n\n            setEditorState(prev => ({\n                ...prev,\n                title: initialTitle || prev.title,\n                content: initialContent || prev.content,\n                version: initialVersion || prev.version,\n                tags: mappedDefaultTags.length > 0 ? mappedDefaultTags : prev.tags,\n                hasUnsavedChanges: hasInitialValues || mappedDefaultTags.length > 0\n            }));\n\n            initialValuesApplied.current = true;\n\n            if (hasInitialValues) {\n                toast({\n                    title: \"Content loaded\",\n                    description: \"Pre-filled content has been loaded into the editor.\",\n                });\n            }\n        }\n    }, [initialData, isNewChangelog, mappedDefaultTags, initialTitle, initialContent, initialVersion]);\n// ===== Export Handler =====\n    const handleExport = useCallback(() => {\n        if (!editorState.content) return;\n\n        const element = document.createElement('a');\n        let content = editorState.content;\n\n        // Add metadata comment if from external source\n        if (wwcState.data?.serverUrl) {\n            const metadata = `<!-- \nGenerated via WWC Protocol\nSource: ${wwcState.data.serverUrl}\nInstance Type: ${wwcState.data.instanceType}\nGenerated: ${new Date().toISOString()}\n-->\n\n`;\n            content = metadata + content;\n        }\n\n        const file = new Blob([content], { type: 'text/markdown' });\n        element.href = URL.createObjectURL(file);\n\n        const safeTitle = editorState.title\n            ? editorState.title.replace(/[^a-z0-9]/gi, '_').toLowerCase()\n            : 'changelog_entry';\n        const version = editorState.version ? `_${editorState.version}` : '';\n        const timestamp = new Date().toISOString().slice(0, 10);\n        const source = wwcState.data?.serverUrl ? '_external' : '';\n\n        element.download = `${safeTitle}${version}${source}_${timestamp}.md`;\n        document.body.appendChild(element);\n        element.click();\n        document.body.removeChild(element);\n    }, [editorState.content, editorState.title, editorState.version, wwcState.data]);\n\n    // ===== Loading States =====\n    const isLoading = isInitialDataLoading || isTagsLoading || isAISettingsLoading;\n\n    if (isLoading) {\n        return (\n            <div className=\"flex items-center justify-center min-h-screen\">\n                <div className=\"flex flex-col items-center gap-4\">\n                    <Loader2 className=\"h-8 w-8 animate-spin text-primary\"/>\n                    <p className=\"text-muted-foreground\">Loading editor...</p>\n                </div>\n            </div>\n        );\n    }\n\n    // ===== Error States =====\n    if (initialDataError || tagsError) {\n        return (\n            <div className=\"flex items-center justify-center min-h-screen p-6\">\n                <Alert variant=\"destructive\" className=\"max-w-md\">\n                    <AlertTitle>Failed to load editor</AlertTitle>\n                    <AlertDescription className=\"mt-2\">\n                        {(initialDataError as Error)?.message || (tagsError as Error)?.message || 'An unexpected error occurred'}\n                    </AlertDescription>\n                    <AlertActions>\n                        <Button\n                            onClick={() => window.location.reload()}\n                            size=\"sm\"\n                        >\n                            <RefreshCw className=\"h-4 w-4 mr-2\"/>\n                            Retry\n                        </Button>\n                        <Button\n                            variant=\"outline\"\n                            onClick={() => router.back()}\n                            size=\"sm\"\n                        >\n                            Go Back\n                        </Button>\n                    </AlertActions>\n                </Alert>\n            </div>\n        );\n    }\n\n    // ===== Main Render =====\n    return (\n        <div className=\"min-h-screen bg-background text-foreground overflow-x-hidden\">\n            <EnhancedEditorHeader\n                title={editorState.title}\n                isSaving={status.isSaving}\n                hasUnsavedChanges={editorState.hasUnsavedChanges}\n                lastSaveError={status.lastSaveError?.message || null}\n                onManualSave={handleManualSave}\n                onBack={() => router.back()}\n                isPublished={editorState.isPublished}\n                projectId={projectId}\n                entryId={entryId}\n                version={editorState.version}\n                onVersionChange={handleVersionChange}\n                onVersionConflict={handleVersionConflict}\n                hasVersionConflict={editorState.hasVersionConflict}\n                selectedTags={editorState.tags}\n                availableTags={availableTags}\n                onTagsChange={handleTagsChange}\n                onTitleChange={handleTitleChange}\n                content={editorState.content}\n                aiApiKey={sectonApiKey}\n                onLoadMoreTags={hasNextPage ? async () => {\n                    await fetchNextPage();\n                } : undefined}\n            />\n\n            <div className=\"container py-6 space-y-6\">\n                {/* External Content Alert */}\n                <AnimatePresence>\n                    {wwcState.data?.serverUrl && (\n                        <motion.div\n                            initial={{opacity: 0, y: -20}}\n                            animate={{opacity: 1, y: 0}}\n                            exit={{opacity: 0, y: -20}}\n                            transition={{duration: 0.3}}\n                        >\n                            <Alert className=\"border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950\">\n                                <ExternalLink className=\"h-4 w-4\" />\n                                <AlertTitle>External Content Loaded</AlertTitle>\n                                <AlertDescription>\n                                    Content received from <strong>{wwcState.data.serverUrl}</strong> via WWC protocol.\n                                    {wwcState.data.instanceType === 'changerawr' && (\n                                        <span className=\"block mt-1 text-sm\">\n                                            Source: Changerawr instance\n                                        </span>\n                                    )}\n                                </AlertDescription>\n                                <AlertActions>\n                                    <Button\n                                        onClick={generateWWCUrl}\n                                        size=\"sm\"\n                                        variant=\"outline\"\n                                    >\n                                        Generate Share URL\n                                    </Button>\n                                </AlertActions>\n                            </Alert>\n                        </motion.div>\n                    )}\n                </AnimatePresence>\n\n                {/* Save Error Alert */}\n                <AnimatePresence>\n                    {status.lastSaveError && !status.isSaving && (\n                        <motion.div\n                            initial={{opacity: 0, y: -20}}\n                            animate={{opacity: 1, y: 0}}\n                            exit={{opacity: 0, y: -20}}\n                            transition={{duration: 0.3}}\n                        >\n                            <Alert\n                                variant={status.lastSaveError.retryable ? \"warning\" : \"destructive\"}\n                            >\n                                <AlertTitle>\n                                    {status.lastSaveError.retryable ? 'Save Failed' : 'Validation Error'}\n                                </AlertTitle>\n                                <AlertDescription>\n                                    {status.lastSaveError.message}\n                                    {status.lastSaveError.retryable && status.saveAttempts < MAX_RETRY_ATTEMPTS && (\n                                        <span className=\"block mt-1 text-sm\">\n                                            Retrying automatically... (Attempt {status.saveAttempts}/{MAX_RETRY_ATTEMPTS})\n                                        </span>\n                                    )}\n                                </AlertDescription>\n                                {(status.canRetry || !status.lastSaveError.retryable) && (\n                                    <AlertActions>\n                                        {status.canRetry && (\n                                            <Button\n                                                onClick={handleRetryAutosave}\n                                                size=\"sm\"\n                                                disabled={status.isSaving}\n                                            >\n                                                {status.isSaving ? (\n                                                    <>\n                                                        <Loader2 className=\"h-3 w-3 mr-2 animate-spin\"/>\n                                                        Retrying...\n                                                    </>\n                                                ) : (\n                                                    <>\n                                                        <RefreshCw className=\"h-3 w-3 mr-2\"/>\n                                                        Retry Now\n                                                    </>\n                                                )}\n                                            </Button>\n                                        )}\n                                        <Button\n                                            variant=\"outline\"\n                                            onClick={() => setStatus(prev => ({...prev, lastSaveError: null}))}\n                                            size=\"sm\"\n                                        >\n                                            Dismiss\n                                        </Button>\n                                    </AlertActions>\n                                )}\n                            </Alert>\n                        </motion.div>\n                    )}\n                </AnimatePresence>\n\n                {/* Version Conflict Alert */}\n                <AnimatePresence>\n                    {editorState.hasVersionConflict && (\n                        <motion.div\n                            initial={{opacity: 0, y: -20}}\n                            animate={{opacity: 1, y: 0}}\n                            exit={{opacity: 0, y: -20}}\n                            transition={{duration: 0.3}}\n                        >\n                            <Alert variant=\"warning\">\n                                <AlertTitle>Version Conflict</AlertTitle>\n                                <AlertDescription>\n                                    The selected version already exists. Please choose a different version to continue.\n                                </AlertDescription>\n                            </Alert>\n                        </motion.div>\n                    )}\n                </AnimatePresence>\n\n                {/* Entry Details Card */}\n                <Card>\n                    <CardHeader>\n                        <CardTitle className=\"flex items-center justify-between\">\n                            Entry Details\n                            {status.isAutoSaving && (\n                                <div className=\"flex items-center text-sm text-muted-foreground\">\n                                    <Loader2 className=\"h-3 w-3 mr-1 animate-spin\"/>\n                                    Auto-saving...\n                                </div>\n                            )}\n                        </CardTitle>\n                    </CardHeader>\n                    <CardContent>\n                        <Input\n                            placeholder=\"Entry title\"\n                            value={editorState.title}\n                            onChange={(e) => handleTitleChange(e.target.value)}\n                            className={cn(\n                                \"text-lg font-medium\",\n                                !editorState.title.trim() && status.lastSaveError && \"border-red-500\"\n                            )}\n                        />\n                        {!editorState.title.trim() && status.lastSaveError && (\n                            <p className=\"text-sm text-red-600 mt-1\">Title is required</p>\n                        )}\n                    </CardContent>\n                </Card>\n\n                {/* Markdown Editor */}\n                {!isAISettingsLoading ? (\n                    <MarkdownEditor\n                        key={entryId || 'new'}\n                        initialValue={editorState.content}\n                        onChange={handleContentChange}\n                        onSave={handleManualSave}\n                        onExport={handleExport}\n                        placeholder=\"What's been changed today?\"\n                        className={cn(\n                            \"min-h-[500px]\",\n                            !editorState.content.trim() && status.lastSaveError && \"border-red-500\"\n                        )}\n                        enableAI={aiEnabled && !!sectonApiKey}\n                        aiApiKey={sectonApiKey}\n                        autoFocus={isNewChangelog && !initialContent}\n                    />\n                ) : (\n                    <div className=\"flex items-center justify-center p-12 border rounded-md bg-muted/10\">\n                        <Loader2 className=\"w-6 h-6 mr-2 animate-spin\"/>\n                        <span>Loading editor...</span>\n                    </div>\n                )}\n\n                {!editorState.content.trim() && status.lastSaveError && (\n                    <p className=\"text-sm text-red-600 -mt-4\">Content is required</p>\n                )}\n\n                {/* Save Status Footer */}\n                <AnimatePresence>\n                    {(status.lastSavedTime || status.isSaving) && (\n                        <motion.div\n                            initial={{opacity: 0}}\n                            animate={{opacity: 1}}\n                            exit={{opacity: 0}}\n                            className=\"flex items-center justify-between p-4 bg-muted/30 rounded-lg border\"\n                        >\n                            <div className=\"flex items-center space-x-3\">\n                                {status.isSaving ? (\n                                    <>\n                                        <Loader2 className=\"h-4 w-4 animate-spin text-blue-600\"/>\n                                        <span className=\"text-sm text-muted-foreground\">\n                                            {status.isAutoSaving ? 'Auto-saving...' : 'Saving...'}\n                                        </span>\n                                    </>\n                                ) : status.lastSavedTime ? (\n                                    <>\n                                        <div className=\"h-2 w-2 bg-green-500 rounded-full\"/>\n                                        <span className=\"text-sm text-muted-foreground\">\n                                            Last saved {status.lastSavedTime.toLocaleTimeString()}\n                                        </span>\n                                    </>\n                                ) : null}\n                            </div>\n\n                            {editorState.hasUnsavedChanges && !status.isSaving && (\n                                <Button\n                                    onClick={handleManualSave}\n                                    size=\"sm\"\n                                    disabled={editorState.hasVersionConflict || !editorState.title.trim() || !editorState.content.trim()}\n                                >\n                                    <Save className=\"h-3 w-3 mr-2\"/>\n                                    Save Now\n                                </Button>\n                            )}\n                        </motion.div>\n                    )}\n                </AnimatePresence>\n            </div>\n        </div>\n    );\n}\n\nexport default ChangelogEditor;"
  },
  {
    "path": "components/changelog/ChangelogEntries.tsx",
    "content": "// components/changelog/ChangelogEntries.tsx\n'use client'\n\nimport {useEffect, useRef, useState, useCallback} from 'react'\nimport {motion, useInView, useScroll, useSpring, AnimatePresence} from 'framer-motion'\nimport {format} from 'date-fns'\nimport {Badge} from '@/components/ui/badge'\nimport {Card, CardContent} from '@/components/ui/card'\nimport {Skeleton} from '@/components/ui/skeleton'\nimport {\n    ChevronRight,\n    Clock,\n    GitCommit,\n    Loader2,\n    Tag,\n    Search,\n    Filter,\n    SortDesc,\n    SortAsc,\n    X,\n    ArrowRight\n} from 'lucide-react'\nimport {useInfiniteQuery} from '@tanstack/react-query'\nimport type {ChangelogEntry} from '@/lib/types/changelog'\nimport {cn} from '@/lib/utils'\nimport {truncateMarkdown} from '@/lib/utils/text'\nimport {RenderMarkdown} from \"@/components/markdown-editor/RenderMarkdown\"\nimport {Input} from '@/components/ui/input'\nimport {Button} from '@/components/ui/button'\nimport Link from 'next/link'\nimport {usePathname} from 'next/navigation'\nimport {\n    Select,\n    SelectContent,\n    SelectItem,\n    SelectTrigger,\n    SelectValue,\n} from '@/components/ui/select'\nimport {\n    Popover,\n    PopoverContent,\n    PopoverTrigger,\n} from '@/components/ui/popover'\nimport {\n    Command,\n    CommandEmpty,\n    CommandGroup,\n    CommandInput,\n    CommandItem,\n} from '@/components/ui/command'\nimport {ColoredTag} from '@/components/changelog/editor/TagColorPicker'\n\ninterface ChangelogEntriesProps {\n    projectId: string\n}\n\n// Define the API response structure\ninterface ChangelogApiResponse {\n    items: ChangelogEntry[];\n    nextCursor: string | null;\n}\n\n// Animation variants\nconst container = {\n    hidden: {opacity: 0},\n    show: {\n        opacity: 1,\n        transition: {\n            staggerChildren: 0.1\n        }\n    }\n}\n\nconst item = {\n    hidden: {opacity: 0, y: 20},\n    show: {opacity: 1, y: 0, transition: {duration: 0.4}}\n}\n\nconst fadeIn = {\n    hidden: {opacity: 0},\n    show: {opacity: 1, transition: {duration: 0.3}}\n}\n\n// Enhanced tag interface with color support\ninterface TagWithColor {\n    id: string;\n    name: string;\n    color?: string | null;\n}\n\n// Skeleton components\nconst SkeletonEntry = () => (\n    <Card className=\"relative overflow-hidden border-primary/5\">\n        <CardContent className=\"p-6\">\n            <div className=\"space-y-6\">\n                <div className=\"flex flex-col md:flex-row md:items-start md:justify-between gap-4\">\n                    <div className=\"space-y-2\">\n                        <Skeleton className=\"h-8 w-64\"/>\n                        <div className=\"flex items-center gap-2\">\n                            <Skeleton className=\"h-5 w-24\"/>\n                        </div>\n                    </div>\n                    <Skeleton className=\"h-5 w-32\"/>\n                </div>\n                <div className=\"space-y-4\">\n                    <Skeleton className=\"h-4 w-full\"/>\n                    <Skeleton className=\"h-4 w-3/4\"/>\n                    <Skeleton className=\"h-4 w-5/6\"/>\n                </div>\n                <div className=\"pt-6 border-t border-border\">\n                    <div className=\"flex items-center gap-2\">\n                        <Skeleton className=\"h-6 w-16\"/>\n                        <Skeleton className=\"h-6 w-16\"/>\n                        <Skeleton className=\"h-6 w-16\"/>\n                    </div>\n                </div>\n            </div>\n        </CardContent>\n    </Card>\n)\n\nconst SkeletonSidebarItem = () => (\n    <div className=\"px-3 py-2\">\n        <Skeleton className=\"h-6 w-full\"/>\n    </div>\n)\n\n// Filter types\ntype SortOption = 'newest' | 'oldest';\ntype FilterState = {\n    search: string;\n    sort: SortOption;\n    tags: string[];\n}\n\nexport default function ChangelogEntries({projectId}: ChangelogEntriesProps) {\n    const loadMoreRef = useRef<HTMLDivElement>(null)\n    const containerRef = useRef<HTMLDivElement>(null)\n    const [activeEntry, setActiveEntry] = useState<string | null>(null)\n    const pathname = usePathname()\n\n    // Determine if we're on a custom domain\n    // On custom domains, pathname will be like \"/\" or \"/entryId\", NOT \"/changelog/custom-domain/...\"\n    // We're on a custom domain if pathname does NOT start with /changelog/ or /dashboard/\n    const isCustomDomain = pathname ? !pathname.startsWith('/changelog/') && !pathname.startsWith('/dashboard/') : false\n\n    // New state for filters and search\n    const [filters, setFilters] = useState<FilterState>({\n        search: '',\n        sort: 'newest',\n        tags: []\n    })\n    const [searchInput, setSearchInput] = useState('')\n    const [availableTags, setAvailableTags] = useState<TagWithColor[]>([])\n    const [isFilterApplied, setIsFilterApplied] = useState(false)\n\n    const isLoadMoreVisible = useInView(loadMoreRef, {\n        margin: \"200px 0px 0px 0px\",\n    })\n\n    // Query with filters\n    const {\n        data,\n        fetchNextPage,\n        hasNextPage,\n        isFetchingNextPage,\n        status,\n        error,\n        refetch\n    } = useInfiniteQuery({\n        queryKey: ['changelog-entries', projectId, filters],\n        queryFn: async ({pageParam}): Promise<ChangelogApiResponse> => {\n            const searchParams = new URLSearchParams()\n            if (pageParam !== undefined) {\n                searchParams.set('cursor', String(pageParam))\n            }\n            if (filters.search) {\n                searchParams.set('search', filters.search)\n            }\n            if (filters.sort) {\n                searchParams.set('sort', filters.sort)\n            }\n            if (filters.tags.length > 0) {\n                searchParams.set('tags', filters.tags.join(','))\n            }\n\n            const res = await fetch(\n                `/api/changelog/${projectId}/entries/all?${searchParams.toString()}`\n            )\n            if (!res.ok) {\n                const error = await res.json()\n                throw new Error(error.error || 'Failed to fetch entries')\n            }\n            return res.json()\n        },\n        getNextPageParam: (lastPage: ChangelogApiResponse) => lastPage.nextCursor ?? undefined,\n        initialPageParam: undefined,\n        refetchOnWindowFocus: false,\n    })\n\n    // Extract all tags for the filter with color support\n    useEffect(() => {\n        if (data?.pages) {\n            const tags = new Map<string, TagWithColor>();\n            data.pages.forEach((page) => {\n                (page as ChangelogApiResponse).items.forEach((entry: ChangelogEntry) => {\n                    entry.tags?.forEach(tag => {\n                        tags.set(tag.id, {\n                            id: tag.id,\n                            name: tag.name,\n                            color: tag.color || null\n                        });\n                    });\n                });\n            });\n            setAvailableTags(Array.from(tags.values()));\n        }\n    }, [data?.pages]);\n\n    // Infinite scroll\n    useEffect(() => {\n        if (isLoadMoreVisible && hasNextPage && !isFetchingNextPage) {\n            fetchNextPage()\n        }\n    }, [isLoadMoreVisible, hasNextPage, isFetchingNextPage, fetchNextPage])\n\n    // Scroll progress indicator\n    const {scrollYProgress} = useScroll({\n        target: containerRef,\n        offset: [\"start start\", \"end end\"]\n    })\n\n    const scaleX = useSpring(scrollYProgress, {\n        stiffness: 100,\n        damping: 30,\n        restDelta: 0.001\n    })\n\n    // Track active entry while scrolling\n    useEffect(() => {\n        const handleScroll = () => {\n            if (!containerRef.current) return\n\n            const entries = containerRef.current.querySelectorAll('[data-entry-id]')\n            const viewportMiddle = window.innerHeight / 2\n            let closestEntry = null\n            let closestDistance = Infinity\n\n            entries.forEach((entry) => {\n                const rect = entry.getBoundingClientRect()\n                const entryMiddle = rect.top + rect.height / 2\n                const distance = Math.abs(entryMiddle - viewportMiddle)\n\n                if (distance < closestDistance) {\n                    closestDistance = distance\n                    closestEntry = entry.getAttribute('data-entry-id')\n                }\n            })\n\n            setActiveEntry(closestEntry)\n        }\n\n        window.addEventListener('scroll', handleScroll, {passive: true})\n        handleScroll()\n\n        return () => window.removeEventListener('scroll', handleScroll)\n    }, [data?.pages])\n\n    // Handle filter application\n    const applyFilters = useCallback(() => {\n        setFilters(prev => ({\n            ...prev,\n            search: searchInput\n        }))\n        setIsFilterApplied(!!searchInput || filters.tags.length > 0 || filters.sort !== 'newest')\n    }, [searchInput, filters.tags, filters.sort])\n\n    // Handle tags selection\n    const toggleTag = useCallback((tagId: string) => {\n        setFilters(prev => {\n            const newTags = prev.tags.includes(tagId)\n                ? prev.tags.filter(id => id !== tagId)\n                : [...prev.tags, tagId]\n\n            return {\n                ...prev,\n                tags: newTags\n            }\n        })\n        setIsFilterApplied(true)\n    }, [])\n\n    // Reset filters\n    const resetFilters = useCallback(() => {\n        setSearchInput('')\n        setFilters({\n            search: '',\n            sort: 'newest',\n            tags: []\n        })\n        setIsFilterApplied(false)\n    }, [])\n\n    // Handle sort change\n    const handleSortChange = useCallback((value: string) => {\n        setFilters(prev => ({\n            ...prev,\n            sort: value as SortOption\n        }))\n        setIsFilterApplied(true)\n    }, [])\n\n    // Error handling\n    if (status === 'error') {\n        return (\n            <div className=\"text-center py-12\">\n                <p className=\"text-destructive\">\n                    Error loading changelog entries: {error instanceof Error ? error.message : 'Unknown error'}\n                </p>\n                <Button\n                    variant=\"outline\"\n                    className=\"mt-4\"\n                    onClick={() => refetch()}\n                >\n                    Try Again\n                </Button>\n            </div>\n        )\n    }\n\n    const isLoading = status === 'pending'\n    const allEntries = data?.pages.flatMap(page => (page as ChangelogApiResponse).items) || []\n\n    return (\n        <div ref={containerRef} className=\"relative min-h-[50vh]\">\n            {/* Progress bar */}\n            <motion.div\n                className=\"fixed top-0 left-0 right-0 h-1 bg-primary/10 z-50\"\n                initial={{opacity: 0}}\n                animate={{opacity: 1}}\n                transition={{delay: 0.5}}\n            >\n                <motion.div\n                    className=\"h-full bg-gradient-to-r from-primary/40 to-primary origin-left\"\n                    style={{scaleX}}\n                />\n            </motion.div>\n\n            {/* Filters Section */}\n            <motion.div\n                className=\"mb-8 p-4 bg-background/80 backdrop-blur-sm rounded-lg border border-border/40\"\n                variants={fadeIn}\n                initial=\"hidden\"\n                animate=\"show\"\n            >\n                <div className=\"flex flex-col md:flex-row gap-4\">\n                    <div className=\"flex-1 flex items-center gap-2\">\n                        <Input\n                            placeholder=\"Search releases...\"\n                            value={searchInput}\n                            onChange={(e) => setSearchInput(e.target.value)}\n                            onKeyDown={(e) => e.key === 'Enter' && applyFilters()}\n                            className=\"bg-background\"\n                        />\n                        <Button onClick={applyFilters} variant=\"secondary\" size=\"icon\">\n                            <Search className=\"h-4 w-4\"/>\n                        </Button>\n                    </div>\n\n                    <div className=\"flex items-center gap-2\">\n                        <Select\n                            value={filters.sort}\n                            onValueChange={handleSortChange}\n                        >\n                            <SelectTrigger className=\"w-[140px] bg-background\">\n                                <SelectValue placeholder=\"Sort by\"/>\n                            </SelectTrigger>\n                            <SelectContent>\n                                <SelectItem value=\"newest\">\n                                    <div className=\"flex items-center gap-2\">\n                                        <SortDesc className=\"h-4 w-4\"/>\n                                        <span>Newest</span>\n                                    </div>\n                                </SelectItem>\n                                <SelectItem value=\"oldest\">\n                                    <div className=\"flex items-center gap-2\">\n                                        <SortAsc className=\"h-4 w-4\"/>\n                                        <span>Oldest</span>\n                                    </div>\n                                </SelectItem>\n                            </SelectContent>\n                        </Select>\n\n                        <Popover>\n                            <PopoverTrigger asChild>\n                                <Button variant=\"outline\" className=\"bg-background\">\n                                    <Filter className=\"h-4 w-4 mr-2\"/>\n                                    Tags\n                                    {filters.tags.length > 0 && (\n                                        <Badge variant=\"secondary\" className=\"ml-2\">\n                                            {filters.tags.length}\n                                        </Badge>\n                                    )}\n                                </Button>\n                            </PopoverTrigger>\n                            <PopoverContent className=\"w-[250px] p-0\" align=\"end\">\n                                <Command>\n                                    <CommandInput placeholder=\"Search tags...\"/>\n                                    <CommandEmpty>No tags found.</CommandEmpty>\n                                    <CommandGroup>\n                                        {availableTags.map(tag => (\n                                            <CommandItem\n                                                key={tag.id}\n                                                onSelect={() => toggleTag(tag.id)}\n                                                className=\"flex items-center gap-2\"\n                                            >\n                                                <div className={cn(\n                                                    \"w-4 h-4 rounded-sm border\",\n                                                    filters.tags.includes(tag.id)\n                                                        ? \"bg-primary border-primary\"\n                                                        : \"border-border\"\n                                                )}/>\n                                                {tag.color && (\n                                                    <div\n                                                        className=\"h-3 w-3 rounded-full border border-gray-300\"\n                                                        style={{ backgroundColor: tag.color }}\n                                                    />\n                                                )}\n                                                <span>{tag.name}</span>\n                                            </CommandItem>\n                                        ))}\n                                    </CommandGroup>\n                                </Command>\n                            </PopoverContent>\n                        </Popover>\n\n                        {isFilterApplied && (\n                            <Button\n                                variant=\"ghost\"\n                                size=\"icon\"\n                                onClick={resetFilters}\n                                className=\"text-muted-foreground hover:text-foreground\"\n                            >\n                                <X className=\"h-4 w-4\"/>\n                            </Button>\n                        )}\n                    </div>\n                </div>\n            </motion.div>\n\n            <div className=\"grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-8\">\n                <motion.div\n                    variants={container}\n                    initial=\"hidden\"\n                    animate=\"show\"\n                    className=\"space-y-16\"\n                >\n                    {isLoading ? (\n                        <div className=\"space-y-16\">\n                            {[...Array(3)].map((_, i) => (\n                                <motion.div\n                                    key={i}\n                                    variants={item}\n                                    initial=\"hidden\"\n                                    animate=\"show\"\n                                >\n                                    <SkeletonEntry/>\n                                </motion.div>\n                            ))}\n                        </div>\n                    ) : allEntries.length === 0 ? (\n                        <motion.div\n                            className=\"text-center py-12\"\n                            variants={fadeIn}\n                            initial=\"hidden\"\n                            animate=\"show\"\n                        >\n                            <p className=\"text-muted-foreground text-lg\">\n                                No entries found matching your criteria.\n                            </p>\n                            {isFilterApplied && (\n                                <Button\n                                    variant=\"outline\"\n                                    className=\"mt-4\"\n                                    onClick={resetFilters}\n                                >\n                                    Reset Filters\n                                </Button>\n                            )}\n                        </motion.div>\n                    ) : (\n                        <>\n                            <AnimatePresence>\n                                {data?.pages.map((page, pageIndex) => (\n                                    <div key={pageIndex} className=\"space-y-16\">\n                                        {(page as ChangelogApiResponse).items.map((entry: ChangelogEntry) => (\n                                            <motion.div\n                                                key={entry.id}\n                                                variants={item}\n                                                data-entry-id={entry.id}\n                                                className={cn(\n                                                    \"relative transition-all duration-300\",\n                                                    activeEntry === entry.id && \"scale-[1.02]\"\n                                                )}\n                                            >\n                                                {/* Active indicator */}\n                                                <motion.div\n                                                    className={cn(\n                                                        \"absolute -left-4 top-1/2 -translate-y-1/2 w-2 h-8 rounded-full bg-primary/40\"\n                                                    )}\n                                                    initial={{opacity: 0}}\n                                                    animate={{opacity: activeEntry === entry.id ? 1 : 0}}\n                                                    transition={{duration: 0.2}}\n                                                />\n\n                                                <Card\n                                                    className=\"relative overflow-hidden border-primary/5 hover:border-primary/20 transition-all group shadow-sm hover:shadow\"\n                                                >\n                                                    <CardContent className=\"p-6\">\n                                                        <div className=\"space-y-6\">\n                                                            {/* Header */}\n                                                            <div\n                                                                className=\"flex flex-col md:flex-row md:items-start md:justify-between gap-4\"\n                                                            >\n                                                                <div className=\"space-y-2\">\n                                                                    <h3 className=\"text-2xl font-semibold tracking-tight group-hover:text-primary transition-colors\">\n                                                                        {entry.title}\n                                                                    </h3>\n                                                                    {entry.version && (\n                                                                        <div className=\"flex items-center gap-2\">\n                                                                            <GitCommit\n                                                                                className=\"w-4 h-4 text-muted-foreground\"/>\n                                                                            <Badge variant=\"outline\"\n                                                                                   className=\"font-mono\">\n                                                                                {entry.version}\n                                                                            </Badge>\n                                                                        </div>\n                                                                    )}\n                                                                </div>\n\n                                                                {entry.publishedAt && (\n                                                                    <div\n                                                                        className=\"flex items-center gap-2 text-muted-foreground\"\n                                                                    >\n                                                                        <Clock className=\"w-4 h-4\"/>\n                                                                        <time\n                                                                            dateTime={typeof entry.publishedAt === 'string'\n                                                                                ? entry.publishedAt\n                                                                                : entry.publishedAt.toISOString()}\n                                                                            className=\"text-sm tabular-nums\"\n                                                                        >\n                                                                            {format(\n                                                                                typeof entry.publishedAt === 'string'\n                                                                                    ? new Date(entry.publishedAt)\n                                                                                    : entry.publishedAt,\n                                                                                'MMMM d, yyyy'\n                                                                            )}\n                                                                        </time>\n                                                                    </div>\n                                                                )}\n                                                            </div>\n\n                                                            {/* Content with animation */}\n                                                            <motion.div\n                                                                initial={{opacity: 0.8}}\n                                                                animate={{opacity: 1}}\n                                                                transition={{duration: 0.5}}\n                                                                className=\"prose prose-lg max-w-none prose-neutral dark:prose-invert prose-p:leading-relaxed prose-pre:bg-muted/50 prose-pre:border prose-pre:border-border\"\n                                                            >\n                                                                <RenderMarkdown>\n                                                                    {truncateMarkdown(entry.content, 400)}\n                                                                </RenderMarkdown>\n                                                            </motion.div>\n\n                                                            {/* Read Full Entry Link */}\n                                                            {entry.content.length > 400 && (\n                                                                <div className=\"pt-2\">\n                                                                    <Link\n                                                                        href={isCustomDomain ? `/${entry.id}` : `/changelog/${projectId}/${entry.id}`}\n                                                                        className=\"inline-flex items-center gap-2 text-primary hover:text-primary/80 transition-colors font-medium\"\n                                                                    >\n                                                                        Read Full Entry\n                                                                        <ArrowRight className=\"w-4 h-4\"/>\n                                                                    </Link>\n                                                                </div>\n                                                            )}\n\n                                                            {/* Tags with Color Support */}\n                                                            {entry.tags?.length > 0 && (\n                                                                <div className=\"pt-6 border-t border-border\">\n                                                                    <div className=\"flex items-start gap-2\">\n                                                                        <Tag\n                                                                            className=\"w-4 h-4 text-muted-foreground mt-1\"/>\n                                                                        <div className=\"flex flex-wrap gap-2\">\n                                                                            {entry.tags.map((tag) => (\n                                                                                <ColoredTag\n                                                                                    key={tag.id}\n                                                                                    name={tag.name}\n                                                                                    color={tag.color}\n                                                                                    size=\"sm\"\n                                                                                    onClick={() => toggleTag(tag.id)}\n                                                                                    className=\"cursor-pointer hover:opacity-80 transition-opacity\"\n                                                                                />\n                                                                            ))}\n                                                                        </div>\n                                                                    </div>\n                                                                </div>\n                                                            )}\n                                                        </div>\n                                                    </CardContent>\n                                                </Card>\n                                            </motion.div>\n                                        ))}\n                                    </div>\n                                ))}\n                            </AnimatePresence>\n                        </>\n                    )}\n\n                    {/* Load more section */}\n                    <div\n                        ref={loadMoreRef}\n                        className={cn(\n                            \"h-20 flex items-center justify-center\",\n                            !hasNextPage && \"hidden\"\n                        )}\n                    >\n                        {isFetchingNextPage && (\n                            <motion.div\n                                className=\"flex items-center gap-2 text-muted-foreground\"\n                                initial={{opacity: 0, y: 20}}\n                                animate={{opacity: 1, y: 0}}\n                            >\n                                <Loader2 className=\"w-4 h-4 animate-spin\"/>\n                                <span>Loading more entries...</span>\n                            </motion.div>\n                        )}\n                    </div>\n                </motion.div>\n\n                {/* Side panel */}\n                <div className=\"hidden lg:block\">\n                    <div className=\"sticky top-4 space-y-4\">\n                        <Card className=\"shadow-sm\">\n                            <CardContent className=\"p-4\">\n                                <h4 className=\"font-semibold mb-4\">Quick Navigation</h4>\n                                <div\n                                    className=\"space-y-1 max-h-[60vh] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-primary/10 hover:scrollbar-thumb-primary/20\"\n                                >\n                                    {isLoading ? (\n                                        <div className=\"space-y-1\">\n                                            {[...Array(5)].map((_, i) => (\n                                                <SkeletonSidebarItem key={i}/>\n                                            ))}\n                                        </div>\n                                    ) : allEntries.length === 0 ? (\n                                        <p className=\"text-sm text-muted-foreground px-3 py-2\">\n                                            No entries available\n                                        </p>\n                                    ) : (\n                                        <>\n                                            {/* Filter count summary if filters applied */}\n                                            {isFilterApplied && (\n                                                <div className=\"px-3 py-2 mb-2 bg-muted/50 rounded-md text-sm\">\n                                                    <p className=\"text-muted-foreground\">\n                                                        Showing {allEntries.length} filtered\n                                                        result{allEntries.length !== 1 ? 's' : ''}\n                                                    </p>\n                                                    <Button\n                                                        variant=\"ghost\"\n                                                        size=\"sm\"\n                                                        className=\"mt-1 h-7 w-full text-xs\"\n                                                        onClick={resetFilters}\n                                                    >\n                                                        Clear All Filters\n                                                    </Button>\n                                                </div>\n                                            )}\n\n                                            {data?.pages.flatMap((page) =>\n                                                    (page as ChangelogApiResponse).items.map((entry: ChangelogEntry) => (\n                                                        <motion.button\n                                                            key={entry.id}\n                                                            onClick={() => {\n                                                                document\n                                                                    .querySelector(`[data-entry-id=\"${entry.id}\"]`)\n                                                                    ?.scrollIntoView({behavior: 'smooth'})\n                                                            }}\n                                                            className={cn(\n                                                                \"flex items-center w-full text-left px-3 py-2 rounded-md transition-all\",\n                                                                \"text-sm hover:bg-primary/10\",\n                                                                activeEntry === entry.id ?\n                                                                    \"bg-primary/20 text-primary font-medium\" :\n                                                                    \"text-muted-foreground\"\n                                                            )}\n                                                            initial={{opacity: 0, x: -20}}\n                                                            animate={{opacity: 1, x: 0}}\n                                                            transition={{delay: 0.1}}\n                                                        >\n                                                            <ChevronRight className={cn(\n                                                                \"w-4 h-4 mr-2 transition-transform\",\n                                                                activeEntry === entry.id && \"rotate-90\"\n                                                            )}/>\n                                                            <div className=\"truncate\">\n                                                                <span className=\"truncate\">{entry.title}</span>\n                                                                {entry.version && (\n                                                                    <span className=\"ml-2 text-xs opacity-70 font-mono\">\n                                  {entry.version}\n                                </span>\n                                                                )}\n                                                            </div>\n                                                        </motion.button>\n                                                    ))\n                                            )}\n                                        </>\n                                    )}\n                                </div>\n                            </CardContent>\n                        </Card>\n\n                        {/* Tag filter card with colors */}\n                        {availableTags.length > 0 && (\n                            <Card className=\"shadow-sm\">\n                                <CardContent className=\"p-4\">\n                                    <h4 className=\"font-semibold mb-4\">Filter by Tags</h4>\n                                    <div className=\"flex flex-wrap gap-2\">\n                                        {availableTags.map(tag => (\n                                            <ColoredTag\n                                                key={tag.id}\n                                                name={tag.name}\n                                                color={tag.color}\n                                                size=\"sm\"\n                                                variant={filters.tags.includes(tag.id) ? \"default\" : \"outline\"}\n                                                onClick={() => toggleTag(tag.id)}\n                                                removable={filters.tags.includes(tag.id)}\n                                                onRemove={filters.tags.includes(tag.id) ? () => toggleTag(tag.id) : undefined}\n                                                className=\"cursor-pointer hover:opacity-80 transition-opacity\"\n                                            />\n                                        ))}\n                                    </div>\n                                </CardContent>\n                            </Card>\n                        )}\n                    </div>\n                </div>\n            </div>\n        </div>\n    )\n}"
  },
  {
    "path": "components/changelog/RequestHandler.tsx",
    "content": "// components/changelog/DestructiveActionRequest.tsx ( this was the original name, kept for internal docs )\nimport { useState } from 'react'\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\nimport {\n    AlertDialog,\n    AlertDialogAction,\n    AlertDialogCancel,\n    AlertDialogContent,\n    AlertDialogDescription,\n    AlertDialogFooter,\n    AlertDialogHeader,\n    AlertDialogTitle,\n    AlertDialogTrigger,\n} from '@/components/ui/alert-dialog'\nimport { Button } from '@/components/ui/button'\nimport { useToast } from '@/hooks/use-toast'\nimport { Loader2, Trash2, AlertCircle } from 'lucide-react'\nimport { useAuth } from '@/context/auth'\nimport { Role, hasAdminAccess } from '@/lib/types/auth'\n\ntype ActionType = 'DELETE_PROJECT' | 'DELETE_TAG' | 'DELETE_ENTRY'\n\ninterface ChangelogRequest {\n    id: string\n    type: ActionType\n    status: 'PENDING' | 'APPROVED' | 'REJECTED'\n    projectId: string\n    targetId?: string\n    createdAt: string\n}\n\ninterface DestructiveActionRequestProps {\n    projectId: string\n    action: ActionType\n    targetId?: string\n    targetName?: string\n    onSuccess?: () => void\n}\n\nexport function DestructiveActionRequest({\n                                             projectId,\n                                             action,\n                                             targetId,\n                                             targetName,\n                                             onSuccess\n                                         }: DestructiveActionRequestProps) {\n    const { user } = useAuth()\n    const { toast } = useToast()\n    const queryClient = useQueryClient()\n    const [isOpen, setIsOpen] = useState(false)\n    const [isSubmitting, setIsSubmitting] = useState(false)\n\n    const userRole = user?.role as (Role | null | undefined)\n\n    // Query existing requests\n    const { data: existingRequests, isFetched } = useQuery<ChangelogRequest[]>({\n        queryKey: ['changelog-requests', projectId],\n        queryFn: async () => {\n            const response = await fetch(`/api/changelog/requests?projectId=${projectId}`)\n            if (!response.ok) {\n                const error = await response.json()\n                throw new Error(error.message || 'Failed to fetch requests')\n            }\n            return response.json()\n        },\n        enabled: !hasAdminAccess(userRole),\n        staleTime: 30000,\n    })\n\n    // Check for existing pending request\n    const existingRequest = existingRequests?.find(\n        (req) =>\n            req.status === 'PENDING' &&\n            req.projectId === projectId &&\n            req.type === action &&\n            (action === 'DELETE_PROJECT' || req.targetId === targetId)\n    )\n\n    // Create request mutation\n    const createRequest = useMutation({\n        mutationFn: async () => {\n            setIsSubmitting(true)\n            try {\n                const requestData = {\n                    type: action,\n                    projectId,\n                    targetId: action !== 'DELETE_PROJECT' ? targetId : undefined\n                }\n\n                const response = await fetch('/api/changelog/requests', {\n                    method: 'POST',\n                    headers: {\n                        'Content-Type': 'application/json',\n                    },\n                    body: JSON.stringify(requestData),\n                })\n\n                if (!response.ok) {\n                    const errorData = await response.json()\n                    throw new Error(errorData.error || 'Failed to create request')\n                }\n\n                return response.json()\n            } catch (error) {\n                console.error('Request creation error:', error)\n                throw error\n            } finally {\n                setIsSubmitting(false)\n            }\n        },\n        onSuccess: () => {\n            queryClient.invalidateQueries({ queryKey: ['changelog-requests', projectId] })\n            toast({\n                title: 'Request Submitted',\n                description: 'An admin will review your request shortly.',\n            })\n            setIsOpen(false)\n            onSuccess?.()\n        },\n        onError: (error: Error) => {\n            console.error('Request error:', error)\n            toast({\n                title: 'Error',\n                description: error.message || 'Failed to submit request',\n                variant: 'destructive',\n            })\n            setIsOpen(false)\n        }\n    })\n\n    // If user is admin or data hasn't been fetched yet, don't render\n    if (hasAdminAccess(userRole) || (!isFetched && !hasAdminAccess(userRole))) {\n        return null\n    }\n\n    const actionLabel = action === 'DELETE_PROJECT'\n        ? 'Delete Project'\n        : `Delete ${action === 'DELETE_TAG' ? 'Tag' : 'Entry'} \"${targetName}\"`\n\n    // Show disabled state for existing request\n    if (existingRequest) {\n        return (\n            <div className=\"inline-flex items-center\">\n                {action === 'DELETE_TAG' || action === 'DELETE_ENTRY' ? (\n                    <button\n                        className=\"ml-1 text-muted-foreground cursor-not-allowed\"\n                        disabled\n                        title=\"A deletion request is pending\"\n                    >\n                        <Trash2 className=\"h-3 w-3\" />\n                    </button>\n                ) : (\n                    <Button\n                        variant=\"outline\"\n                        disabled\n                        className=\"inline-flex items-center gap-2\"\n                    >\n                        <AlertCircle className=\"h-4 w-4\" />\n                        Request Pending\n                    </Button>\n                )}\n            </div>\n        )\n    }\n\n    const handleCreateRequest = () => {\n        if (!projectId || isSubmitting) return\n        createRequest.mutate()\n    }\n\n    return (\n        <AlertDialog open={isOpen} onOpenChange={setIsOpen}>\n            <AlertDialogTrigger asChild>\n                {action === 'DELETE_TAG' || action === 'DELETE_ENTRY' ? (\n                    <button\n                        onClick={() => setIsOpen(true)}\n                        className=\"ml-1 hover:text-destructive\"\n                    >\n                        <Trash2 className=\"h-3 w-3\" />\n                    </button>\n                ) : (\n                    <Button variant=\"destructive\">{actionLabel}</Button>\n                )}\n            </AlertDialogTrigger>\n            <AlertDialogContent>\n                <AlertDialogHeader>\n                    <AlertDialogTitle>Request {actionLabel}</AlertDialogTitle>\n                    <AlertDialogDescription>\n                        {action === 'DELETE_PROJECT'\n                            ? 'This will request deletion of the entire project and all its data.'\n                            : `This will request deletion of the ${action === 'DELETE_TAG' ? 'tag' : 'entry'} \"${targetName}\" from this project.`}\n                        <br /><br />\n                        This action requires admin approval. Would you like to submit a request?\n                    </AlertDialogDescription>\n                </AlertDialogHeader>\n                <AlertDialogFooter>\n                    <AlertDialogCancel>Cancel</AlertDialogCancel>\n                    <AlertDialogAction\n                        className=\"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\n                        onClick={handleCreateRequest}\n                        disabled={isSubmitting}\n                    >\n                        {isSubmitting ? (\n                            <>\n                                <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                                Submitting...\n                            </>\n                        ) : (\n                            'Submit Request'\n                        )}\n                    </AlertDialogAction>\n                </AlertDialogFooter>\n            </AlertDialogContent>\n        </AlertDialog>\n    )\n}"
  },
  {
    "path": "components/changelog/ScrollToTopButton.tsx",
    "content": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { ArrowUp } from 'lucide-react'\n\nexport function useScrollVisibility() {\n    const [isVisible, setIsVisible] = useState(false)\n\n    useEffect(() => {\n        const toggleVisibility = () => {\n            // Show button when page is scrolled down 300px\n            if (window.scrollY > 300) {\n                setIsVisible(true)\n            } else {\n                setIsVisible(false)\n            }\n        }\n\n        window.addEventListener('scroll', toggleVisibility)\n\n        return () => window.removeEventListener('scroll', toggleVisibility)\n    }, [])\n\n    return isVisible\n}\n\nexport default function ScrollToTopButton() {\n    const scrollToTop = () => {\n        window.scrollTo({\n            top: 0,\n            behavior: 'smooth'\n        })\n    }\n\n    return (\n        <Button\n            size=\"icon\"\n            variant=\"outline\"\n            className=\"h-10 w-10 rounded-full backdrop-blur-sm shadow-lg hover:shadow-xl hover:scale-110 active:scale-95 origin-center\"\n            onClick={scrollToTop}\n            aria-label=\"Scroll to top\"\n        >\n            <ArrowUp className=\"h-5 w-5\" />\n        </Button>\n    )\n}"
  },
  {
    "path": "components/changelog/ShareButton.tsx",
    "content": "'use client'\n\nimport { useState, useEffect } from 'react'\nimport { Share2, Check } from 'lucide-react'\nimport {\n    Tooltip,\n    TooltipContent,\n    TooltipProvider,\n    TooltipTrigger\n} from '@/components/ui/tooltip'\nimport { Button } from '@/components/ui/button'\nimport { useToast } from '@/hooks/use-toast'\n\ninterface ShareButtonProps {\n    url: string\n    title?: string\n    text?: string\n}\n\nexport default function ShareButton({\n                                        url,\n                                        title = 'Changelog',\n                                        text = 'Check out this project changelog'\n                                    }: ShareButtonProps) {\n    const [copied, setCopied] = useState(false)\n    const [canUseNativeShare, setCanUseNativeShare] = useState(false)\n    const { toast } = useToast()\n\n    // Check if the Web Share API is available\n    useEffect(() => {\n        setCanUseNativeShare(\n            typeof navigator !== 'undefined' &&\n            !!navigator.share &&\n            !!navigator.canShare\n        )\n    }, [])\n\n    const handleShare = async () => {\n        try {\n            if (canUseNativeShare) {\n                const shareData = { url, title, text }\n\n                // Check if the data is shareable\n                if (navigator.canShare(shareData)) {\n                    await navigator.share(shareData)\n                    // toast({\n                    //     title: \"DEBUG\",\n                    //     description: \"The navigator function was called successfully\",\n                    //     duration: 2000\n                    // })\n                    return\n                }\n            }\n\n            // Fallback for browsers without Web Share API or if sharing fails\n            await navigator.clipboard.writeText(url)\n            setCopied(true)\n\n            toast({\n                title: \"URL copied\",\n                description: \"Changelog URL has been copied to clipboard\",\n                duration: 2000\n            })\n\n            // Reset copied state after 2 seconds\n            setTimeout(() => {\n                setCopied(false)\n            }, 2000)\n        } catch (error) {\n            // User canceled or sharing failed\n            console.error('Error sharing:', error)\n\n            // Only try clipboard as fallback if it wasn't an abort\n            if (error instanceof Error && error.name !== 'AbortError') {\n                await navigator.clipboard.writeText(url)\n                setCopied(true)\n\n                toast({\n                    title: \"URL copied\",\n                    description: \"Changelog URL has been copied to clipboard\",\n                    duration: 2000\n                })\n\n                // Reset copied state after 2 seconds\n                setTimeout(() => {\n                    setCopied(false)\n                }, 2000)\n            }\n        }\n    }\n\n    return (\n        <TooltipProvider>\n            <Tooltip>\n                <TooltipTrigger asChild>\n                    <Button\n                        variant=\"ghost\"\n                        className=\"flex items-center gap-2 p-0 h-auto\"\n                        onClick={handleShare}\n                        aria-label=\"Share changelog\"\n                    >\n                        {copied ? (\n                            <Check className=\"w-5 h-5 text-green-500\" />\n                        ) : (\n                            <Share2 className=\"w-5 h-5\" />\n                        )}\n                        <span className=\"font-medium text-lg\">\n                            {copied ? 'Copied!' : 'Share'}\n                        </span>\n                    </Button>\n                </TooltipTrigger>\n                <TooltipContent>\n                    {canUseNativeShare ? 'Share this changelog' : 'Copy changelog URL'}\n                </TooltipContent>\n            </Tooltip>\n        </TooltipProvider>\n    )\n}"
  },
  {
    "path": "components/changelog/ThemeToggle.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { useTheme } from \"next-themes\"\nimport { Moon, Sun } from \"lucide-react\"\n\ninterface ThemeToggleProps {\n    projectId: string\n}\n\n/**\n * Per-project theme toggle for public changelog pages.\n * Overrides the global theme provider to use project-specific storage.\n * Key format: `changelog-theme-${projectId}`\n */\nexport function ThemeToggle({ projectId }: ThemeToggleProps) {\n    const { theme: globalTheme, setTheme: setGlobalTheme, resolvedTheme } = useTheme()\n    const [mounted, setMounted] = React.useState(false)\n\n    const storageKey = `changelog-theme-${projectId}`\n\n    // Override global theme with per-project theme on mount and when projectId changes\n    React.useEffect(() => {\n        const stored = localStorage.getItem(storageKey) as \"light\" | \"dark\" | null\n        const projectTheme = stored || \"light\"\n\n        // Only set if different from current theme to avoid loops\n        if (projectTheme !== globalTheme) {\n            setGlobalTheme(projectTheme)\n        }\n\n        setMounted(true)\n    }, [projectId, storageKey])\n\n    // When global theme changes, save to project-specific storage\n    React.useEffect(() => {\n        if (!mounted || !globalTheme) return\n\n        localStorage.setItem(storageKey, globalTheme)\n        // Also update the global storage to keep next-themes from interfering\n        localStorage.setItem(\"theme\", globalTheme)\n    }, [globalTheme, storageKey, mounted])\n\n    const toggleTheme = () => {\n        const newTheme = resolvedTheme === \"dark\" ? \"light\" : \"dark\"\n        setGlobalTheme(newTheme)\n    }\n\n    if (!mounted) {\n        return (\n            <button\n                className=\"h-10 w-10 flex items-center justify-center rounded-full bg-background/80 backdrop-blur-sm border border-border/40 shadow-lg transition-all hover:shadow-xl hover:scale-110 active:scale-95 origin-center\"\n                aria-label=\"Toggle theme\"\n            >\n                <Sun className=\"h-5 w-5 text-muted-foreground\" />\n            </button>\n        )\n    }\n\n    const isDark = resolvedTheme === \"dark\"\n\n    return (\n        <button\n            onClick={toggleTheme}\n            className=\"h-10 w-10 flex items-center justify-center rounded-full bg-background/80 backdrop-blur-sm border border-border/40 shadow-lg transition-all hover:shadow-xl hover:scale-110 active:scale-95 origin-center\"\n            aria-label={`Switch to ${isDark ? \"light\" : \"dark\"} theme`}\n            title={`Switch to ${isDark ? \"light\" : \"dark\"} theme`}\n        >\n            {isDark ? (\n                <Sun className=\"h-5 w-5 text-foreground\" />\n            ) : (\n                <Moon className=\"h-5 w-5 text-foreground\" />\n            )}\n        </button>\n    )\n}\n"
  },
  {
    "path": "components/changelog/WidgetPreview.tsx",
    "content": "import React, {FC, useState} from 'react';\nimport { Card } from '@/components/ui/card';\nimport { Alert, AlertDescription } from '@/components/ui/alert';\nimport { X } from 'lucide-react';\nimport {WidgetConfig} from \"@/app/dashboard/projects/[projectId]/integrations/widget/widget-config\";\n\nexport interface WidgetPreviewProps {\n    config: WidgetConfig; // Define the type for 'config' prop\n    isOpen: boolean; // Define 'isOpen' prop\n    onClose: () => void; // Define 'onClose' prop\n}\n\nconst FAKE_ENTRIES = [\n    {\n        id: '1',\n        title: 'New Dashboard Interface',\n        content: 'We\\'ve completely redesigned the dashboard for better usability and performance.',\n        tags: [{ name: 'Feature' }]\n    },\n    {\n        id: '2',\n        title: 'Bug Fixes and Performance Improvements',\n        content: 'Fixed several issues with the data synchronization and improved overall system performance.',\n        tags: [{ name: 'Fix' }]\n    },\n    {\n        id: '3',\n        title: 'Dark Mode Support',\n        content: 'Added system-wide dark mode support with automatic theme detection.',\n        tags: [{ name: 'Enhancement' }]\n    },\n    {\n        id: '4',\n        title: 'API Rate Limiting',\n        content: 'Implemented smart rate limiting to prevent API abuse while maintaining service quality.',\n        tags: [{ name: 'Security' }]\n    },\n    {\n        id: '5',\n        title: 'Mobile Responsive Design',\n        content: 'Enhanced mobile experience with improved layouts and touch interactions.',\n        tags: [{ name: 'Feature' }]\n    },\n    {\n        id: '6',\n        title: 'Export Functionality',\n        content: 'Added support for exporting data in CSV, PDF, and Excel formats.',\n        tags: [{ name: 'Feature' }]\n    },\n    {\n        id: '7',\n        title: 'Search Optimization',\n        content: 'Improved search algorithm for faster and more accurate results.',\n        tags: [{ name: 'Enhancement' }]\n    },\n    {\n        id: '8',\n        title: 'Accessibility Updates',\n        content: 'Implemented ARIA labels and improved keyboard navigation throughout the application.',\n        tags: [{ name: 'Enhancement' }]\n    },\n    {\n        id: '9',\n        title: 'Authentication Enhancements',\n        content: 'Added support for multi-factor authentication and SSO integration.',\n        tags: [{ name: 'Security' }]\n    },\n    {\n        id: '10',\n        title: 'Real-time Notifications',\n        content: 'Introduced real-time notifications for important updates and system events.',\n        tags: [{ name: 'Feature' }]\n    }\n];\n\nconst ChangelogWidget: FC<WidgetPreviewProps> = ({ config, isOpen, onClose }) => {\n    const [showRSSAlert, setShowRSSAlert] = useState(false);\n    const isDark = config.theme === 'dark';\n\n    return (\n        <>\n            <Card className={`\n        w-[300px] overflow-hidden\n        ${isDark ? '!bg-zinc-800 !text-white' : '!bg-white !text-gray-900'}\n        ${config.isPopup ? 'fixed z-40' : 'relative'}\n        ${config.isPopup ? (isOpen ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4 pointer-events-none') : ''}\n        transition-all duration-200\n      `}\n                  style={{\n                      ...(config.isPopup && {\n                          bottom: config.position.includes('bottom') ? '1rem' : 'auto',\n                          top: config.position.includes('top') ? '1rem' : 'auto',\n                          right: config.position.includes('right') ? '1rem' : 'auto',\n                          left: config.position.includes('left') ? '1rem' : 'auto'\n                      })\n                  }}\n            >\n                {/* Header */}\n                <div className={`\n          p-4 border-b flex justify-between items-center\n          ${isDark ? '!border-zinc-700' : '!border-gray-100'}\n        `}>\n                    <span className=\"font-semibold\">Latest Updates</span>\n                    {config.isPopup && (\n                        <button\n                            onClick={onClose}\n                            className={`\n                hover:opacity-70 transition-opacity\n                ${isDark ? '!text-gray-400' : '!text-gray-500'}\n              `}\n                        >\n                            <X size={16} />\n                        </button>\n                    )}\n                </div>\n\n                {/* Content */}\n                <div\n                    className=\"overflow-y-auto\"\n                    style={{ maxHeight: config.maxHeight }}\n                >\n                    {FAKE_ENTRIES.slice(0, config.maxEntries).map((entry) => (\n                        <div\n                            key={entry.id}\n                            className={`\n                p-4 border-b last:border-b-0\n                ${isDark ? '!border-zinc-700' : '!border-gray-100'}\n              `}\n                        >\n                            {entry.tags?.[0] && (\n                                <span className={`\n                  inline-block px-2 py-1 text-xs rounded-md mb-2\n                  ${isDark\n                                    ? '!bg-blue-900/50 !text-blue-300'\n                                    : '!bg-blue-100 !text-blue-700'}\n                `}>\n                  {entry.tags[0].name}\n                </span>\n                            )}\n                            <h3 className={`\n                font-medium mb-1\n                ${isDark ? '!text-white' : '!text-gray-900'}\n              `}>\n                                {entry.title}\n                            </h3>\n                            <p className={`\n                text-sm mb-2\n                ${isDark ? '!text-gray-400' : '!text-gray-600'}\n              `}>\n                                {entry.content}\n                            </p>\n                            <a\n                                href=\"#\"\n                                className={`\n                  text-xs hover:opacity-70 transition-opacity\n                  ${isDark\n                                    ? '!text-blue-400'\n                                    : '!text-blue-600'}\n                `}\n                            >\n                                Read more\n                            </a>\n                        </div>\n                    ))}\n                </div>\n\n                {/* Footer */}\n                <div className={`\n          p-3 text-xs border-t flex justify-between items-center\n          ${isDark\n                    ? '!border-zinc-700 !text-gray-400'\n                    : '!border-gray-100 !text-gray-500'}\n        `}>\n                    <span>Powered by Changerawr</span>\n                    <button\n                        onClick={() => setShowRSSAlert(true)}\n                        className=\"hover:opacity-70 transition-opacity\"\n                    >\n                        RSS\n                    </button>\n                </div>\n            </Card>\n\n            {/* RSS Alert Modal */}\n            {showRSSAlert && (\n                <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center z-50\">\n                    <Alert className={`\n            max-w-md mx-4 relative\n            ${isDark ? '!bg-zinc-900 !text-white' : '!bg-white !text-gray-900'}\n          `}>\n                        <button\n                            onClick={() => setShowRSSAlert(false)}\n                            className={`\n                absolute top-2 right-2 hover:opacity-70 transition-opacity\n                ${isDark ? '!text-gray-400' : '!text-gray-500'}\n              `}\n                        >\n                            <X size={16} />\n                        </button>\n                        <AlertDescription>\n                            RSS feed functionality is only available in the embedded widget. This is just a preview.\n                        </AlertDescription>\n                    </Alert>\n                </div>\n            )}\n        </>\n    );\n};\n\nexport default function WidgetPreview({ config }: { config: WidgetConfig }) {\n    const [isOpen, setIsOpen] = useState(!config.isPopup);\n    const isDark = config.theme === 'dark';\n\n    return (\n        <div className=\"flex flex-col items-center justify-center w-full\">\n            <div className={`\n        w-full max-w-3xl rounded-xl p-8 relative min-h-[500px] \n        flex flex-col items-center justify-center\n        ${isDark ? '!bg-zinc-900' : '!bg-zinc-100'}\n      `}>\n                {/* Browser chrome decoration */}\n                <div className=\"absolute top-4 left-4 flex items-center gap-2\">\n                    <div className=\"w-3 h-3 rounded-full bg-red-500\" />\n                    <div className=\"w-3 h-3 rounded-full bg-yellow-500\" />\n                    <div className=\"w-3 h-3 rounded-full bg-green-500\" />\n                </div>\n\n                {/* Trigger button for popup mode */}\n                {config.isPopup && (\n                    <button\n                        onClick={() => setIsOpen(true)}\n                        className=\"mb-4 px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n                    >\n                        View Updates\n                    </button>\n                )}\n\n                {/* Widget */}\n                <ChangelogWidget\n                    config={config}\n                    isOpen={isOpen}\n                    onClose={() => setIsOpen(false)}\n                />\n\n                {/* Widget placeholder when in popup mode and closed */}\n                {config.isPopup && !isOpen && (\n                    <div className=\"absolute inset-0 flex items-center justify-center pointer-events-none\">\n                        <div className={`\n              border-2 border-dashed rounded-lg w-[300px] h-[400px] \n              flex items-center justify-center\n              ${isDark ? 'border-zinc-700' : 'border-zinc-300'}\n            `}>\n                            <p className={`\n                text-sm\n                ${isDark ? 'text-zinc-400' : 'text-zinc-500'}\n              `}>\n                                Click the button above to show the widget\n                            </p>\n                        </div>\n                    </div>\n                )}\n            </div>\n        </div>\n    );\n}"
  },
  {
    "path": "components/changelog/editor/AITitleGenerator.tsx",
    "content": "import React, { useState, useEffect, useRef } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Sparkles, Pencil, Lock, UnlockIcon, Wand2, Lightbulb, Check, X, Palette, ChevronDown, RotateCcw } from 'lucide-react';\nimport { motion, AnimatePresence, Variants } from 'framer-motion';\nimport { cn } from '@/lib/utils';\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';\nimport { createSectonClient } from '@/lib/utils/ai/secton';\nimport confetti from 'canvas-confetti';\nimport { createPortal } from 'react-dom';\n\ninterface AITitleGeneratorProps {\n    content: string;\n    onSelectTitle: (title: string) => void;\n    apiKey?: string;\n    initialTitle?: string;\n}\n\ninterface TitleSuggestion {\n    text: string;\n    isLocked: boolean;\n    style: 'primary' | 'creative' | 'technical';\n    score: number; // AI confidence score (1-100)\n}\n\n// Extract the most important content from markdown\nconst extractImportantContent = (markdown: string, maxLength: number = 500): string => {\n    if (!markdown || markdown.length === 0) return \"\";\n\n    // Extract headings for better context\n    const headings = markdown.split('\\n')\n        .filter(line => line.trim().startsWith('#'))\n        .map(line => line.replace(/^#+\\s+/, '').trim())\n        .join(' ');\n\n    // Get first substantial paragraph\n    const firstParagraph = markdown.split('\\n\\n')[0] || '';\n\n    // Combine and ensure we don't exceed max length\n    let extracted = headings ? `${headings}. ${firstParagraph}` : firstParagraph;\n    if (extracted.length > maxLength) {\n        extracted = extracted.substring(0, maxLength - 3) + '...';\n    }\n\n    return extracted;\n};\n\n// Title generation prompt\nconst getTitleGenerationPrompt = (content: string, lockedTitles: TitleSuggestion[] = []): string => {\n    let contextSection = \"\";\n    if (lockedTitles.length > 0) {\n        // Pass locked titles with their styles and scores to influence AI\n        contextSection = `\\nContext: User has locked these titles as preferred styles:\\n`;\n        lockedTitles.forEach(title => {\n            contextSection += `- \"${title.text}\" (Style: ${title.style}, Score: ${title.score})\\n`;\n        });\n    }\n\n    return `Generate 3 distinct, impactful title suggestions for a changelog entry.\nContent: \"${content}\"\n${contextSection}\nFor each title, assign:\n1. A style tag: primary (clear, direct), creative (metaphorical), or technical (developer-focused)\n2. A confidence score (1-100) representing how well it fits the content\n\nBased on the locked titles (if any), prioritize generating titles that complement their style and maintain similar themes.\n\nFormat exactly as follows:\nTITLE: [Title text]\nSTYLE: [style]\nSCORE: [score]\n\nTITLE: [Title text]\nSTYLE: [style]\nSCORE: [score]\n\nTITLE: [Title text]\nSTYLE: [style]\nSCORE: [score]`;\n};\n\nexport default function AITitleGenerator({ content, onSelectTitle, apiKey, initialTitle }: AITitleGeneratorProps) {\n    const [isOpen, setIsOpen] = useState(false);\n    const [isLoading, setIsLoading] = useState(false);\n    const [error, setError] = useState<Error | null>(null);\n    const [titles, setTitles] = useState<TitleSuggestion[]>([]);\n    const [selectedTitle, setSelectedTitle] = useState<string | null>(initialTitle || null);\n    const [aiRecommendation, setAiRecommendation] = useState<number | null>(null);\n    const [showSuccessScreen, setShowSuccessScreen] = useState(false);\n    const [hasSelectedBefore, setHasSelectedBefore] = useState(false);\n    const modalRef = useRef<HTMLDivElement>(null);\n\n    // Create a ref to handle clicks outside the modal\n    const handleOutsideClick = (e: MouseEvent) => {\n        if (modalRef.current && !modalRef.current.contains(e.target as Node)) {\n            if (!showSuccessScreen) { // Don't close during success animation\n                setIsOpen(false);\n            }\n        }\n    };\n\n    // Reset success screen when modal is reopened\n    useEffect(() => {\n        if (isOpen) {\n            setShowSuccessScreen(false);\n        }\n    }, [isOpen]);\n\n    // Set up listener for outside clicks\n    useEffect(() => {\n        if (isOpen) {\n            document.addEventListener('mousedown', handleOutsideClick);\n        } else {\n            document.removeEventListener('mousedown', handleOutsideClick);\n        }\n\n        return () => {\n            document.removeEventListener('mousedown', handleOutsideClick);\n        };\n    }, [isOpen, showSuccessScreen]);\n\n    // When modal opens, generate titles if needed\n    useEffect(() => {\n        if (isOpen && !titles.length && !isLoading) {\n            generateTitles();\n        }\n    }, [isOpen, titles.length, isLoading]);\n\n    // Add selected title to titles list if provided and not already there\n    useEffect(() => {\n        if (initialTitle && !hasSelectedBefore) {\n            const titleExists = titles.some(title => title.text === initialTitle);\n\n            if (!titleExists) {\n                setTitles(prevTitles => [\n                    ...prevTitles,\n                    {\n                        text: initialTitle,\n                        isLocked: true,\n                        style: 'primary', // Default style\n                        score: 85 // Default high score\n                    }\n                ]);\n            }\n        }\n    }, [initialTitle, titles, hasSelectedBefore]);\n\n    // Show confetti effect on selection\n    useEffect(() => {\n        if (showSuccessScreen) {\n            // Multiple confetti bursts for a more impressive effect\n            const duration = 1500;\n            const animationEnd = Date.now() + duration;\n\n            // Configure different types of confetti\n            const confettiOptions = {\n                particleCount: 80,\n                spread: 100,\n                origin: { x: 0.5, y: 0.3 },\n                colors: ['#10B981', '#3B82F6', '#8B5CF6', '#F59E0B'],\n                disableForReducedMotion: true,\n            };\n\n            const interval = setInterval(() => {\n                const timeLeft = animationEnd - Date.now();\n\n                if (timeLeft <= 0) {\n                    clearInterval(interval);\n                    // Close modal after animation finishes with delay for a better UX\n                    setTimeout(() => {\n                        setShowSuccessScreen(false);\n                        setHasSelectedBefore(true);\n                        // Now we delay the closing to allow the animation to complete\n                        setTimeout(() => setIsOpen(false), 300);\n                    }, 500);\n                    return;\n                }\n\n                // Launch confetti from different positions\n                confetti({\n                    ...confettiOptions,\n                    origin: { x: 0.3, y: 0.5 },\n                });\n                confetti({\n                    ...confettiOptions,\n                    origin: { x: 0.7, y: 0.5 },\n                });\n            }, 250);\n\n            return () => clearInterval(interval);\n        }\n    }, [showSuccessScreen]);\n\n    // Parse titles from AI response\n    const parseTitles = (text: string): TitleSuggestion[] => {\n        const titleRegex = /TITLE: (.*)\\nSTYLE: (.*)\\nSCORE: (\\d+)/gi;\n        const matches = [...text.matchAll(titleRegex)];\n\n        const parsedTitles = matches.map(match => ({\n            text: match[1].trim(),\n            isLocked: false,\n            style: match[2].trim().toLowerCase() as 'primary' | 'creative' | 'technical',\n            score: parseInt(match[3].trim(), 10)\n        }));\n\n        // Find the title with the highest score to recommend\n        let highestScoreIndex = 0;\n        let highestScore = 0;\n\n        parsedTitles.forEach((title, index) => {\n            if (title.score > highestScore) {\n                highestScore = title.score;\n                highestScoreIndex = index;\n            }\n        });\n\n        setAiRecommendation(highestScoreIndex);\n\n        return parsedTitles.slice(0, 3); // Ensure we have at most 3 titles\n    };\n\n    // Generate title suggestions\n    const generateTitles = async () => {\n        if (!apiKey || !content.trim()) {\n            setError(new Error(content.trim() ? \"API key is required\" : \"Content is required\"));\n            return;\n        }\n\n        setIsLoading(true);\n        setError(null);\n\n        try {\n            // Create a Secton client instance\n            const sectonClient = createSectonClient({\n                apiKey,\n                defaultModel: 'copilot-zero',\n            });\n\n            // Process content to extract the most important parts\n            const processedContent = extractImportantContent(content);\n\n            // Get locked titles for context - we pass the full title object to influence AI\n            const lockedTitles = titles.filter(title => title.isLocked);\n\n            // Generate titles\n            const prompt = getTitleGenerationPrompt(processedContent, lockedTitles);\n            const generatedText = await sectonClient.generateText(prompt, {\n                temperature: 0.8,\n                max_tokens: 200,\n            });\n\n            // Parse the titles\n            const parsedTitles = parseTitles(generatedText);\n\n            if (parsedTitles.length === 0) {\n                throw new Error(\"Failed to generate valid titles\");\n            }\n\n            // Keep locked titles, add new ones\n            const lockedTitlesSet = new Set(lockedTitles.map(t => t.text));\n\n            // Keep locked titles and replace unlocked titles with new ones\n            const newTitles = [\n                ...titles.filter(t => t.isLocked),\n                ...parsedTitles.filter(t => !lockedTitlesSet.has(t.text))\n            ];\n\n            // Ensure we have at most 3 titles, prioritizing locked titles\n            const finalTitles = newTitles.slice(0, 3);\n\n            // Make sure there's at least one title of each style if possible\n            const styles = ['primary', 'creative', 'technical'];\n            const existingStyles = new Set(finalTitles.map(t => t.style));\n\n            // If there are missing styles and we have space, add them from parsedTitles\n            if (finalTitles.length < 3) {\n                const missingStyles = styles.filter(style => !existingStyles.has(style as 'primary' | 'creative' | 'technical'));\n\n                for (const style of missingStyles) {\n                    const titleOfStyle = parsedTitles.find(t =>\n                        t.style === style && !lockedTitlesSet.has(t.text) &&\n                        !finalTitles.some(ft => ft.text === t.text)\n                    );\n\n                    if (titleOfStyle && finalTitles.length < 3) {\n                        finalTitles.push(titleOfStyle);\n                    }\n                }\n            }\n\n            setTitles(finalTitles);\n        } catch (err) {\n            console.error(\"Error generating titles:\", err);\n            setError(err instanceof Error ? err : new Error(String(err)));\n        } finally {\n            setIsLoading(false);\n        }\n    };\n\n    // Toggle lock status for a title\n    const toggleLockTitle = (index: number) => {\n        setTitles(prevTitles =>\n            prevTitles.map((title, i) =>\n                i === index ? { ...title, isLocked: !title.isLocked } : title\n            )\n        );\n    };\n\n    // Handle title selection\n    const handleSelectTitle = (title: string) => {\n        setSelectedTitle(title);\n        setShowSuccessScreen(true);\n        onSelectTitle(title);\n    };\n\n    // Reset and generate new titles\n    const handleReset = () => {\n        // Keep locked titles but regenerate\n        generateTitles();\n    };\n\n    // Get background style based on title style\n    const getTitleBackground = (style: 'primary' | 'creative' | 'technical', isRecommended: boolean) => {\n        const gradientMap = {\n            primary: isRecommended\n                ? \"bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/10 border-blue-200 dark:border-blue-800/20\"\n                : \"bg-gradient-to-r from-slate-50 to-blue-50 dark:from-slate-900/40 dark:to-blue-900/10 border-slate-200 dark:border-slate-800/20\",\n            creative: isRecommended\n                ? \"bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/10 border-purple-200 dark:border-purple-800/20\"\n                : \"bg-gradient-to-r from-slate-50 to-purple-50 dark:from-slate-900/40 dark:to-purple-900/10 border-slate-200 dark:border-slate-800/20\",\n            technical: isRecommended\n                ? \"bg-gradient-to-r from-emerald-50 to-teal-50 dark:from-emerald-900/20 dark:to-teal-900/10 border-emerald-200 dark:border-emerald-800/20\"\n                : \"bg-gradient-to-r from-slate-50 to-emerald-50 dark:from-slate-900/40 dark:to-emerald-900/10 border-slate-200 dark:border-slate-800/20\",\n        };\n\n        return gradientMap[style];\n    };\n\n    // Get icon based on title style\n    const getTitleIcon = (style: 'primary' | 'creative' | 'technical') => {\n        switch(style) {\n            case 'primary': return <Check className=\"h-3.5 w-3.5\" />;\n            case 'creative': return <Wand2 className=\"h-3.5 w-3.5\" />;\n            case 'technical': return <ChevronDown className=\"h-3.5 w-3.5\" />;\n            default: return <Check className=\"h-3.5 w-3.5\" />;\n        }\n    };\n\n    // Get color for style badge\n    const getStyleColor = (style: 'primary' | 'creative' | 'technical') => {\n        switch(style) {\n            case 'primary': return \"text-blue-700 dark:text-blue-400\";\n            case 'creative': return \"text-purple-700 dark:text-purple-400\";\n            case 'technical': return \"text-emerald-700 dark:text-emerald-400\";\n            default: return \"text-slate-700 dark:text-slate-400\";\n        }\n    };\n\n    // Animation variants\n    const overlayVariants: Variants = {\n        hidden: { opacity: 0 },\n        visible: { opacity: 1 },\n        exit: { opacity: 0 }\n    };\n\n    const modalVariants: Variants = {\n        hidden: { y: 20, scale: 0.95, opacity: 0 },\n        visible: { y: 0, scale: 1, opacity: 1, transition: { type: \"spring\", duration: 0.5, delay: 0.1 } },\n        exit: { y: -20, scale: 0.95, opacity: 0, transition: { duration: 0.2 } }\n    };\n\n    const successVariants: Variants = {\n        hidden: { scale: 0.8, opacity: 0 },\n        visible: { scale: 1, opacity: 1, transition: { type: \"spring\", damping: 12 } },\n        exit: { scale: 1.2, opacity: 0 }\n    };\n\n    // Custom modal implementation\n    const Modal = isOpen ? createPortal(\n        <AnimatePresence mode=\"wait\">\n            <motion.div\n                key=\"overlay\"\n                initial=\"hidden\"\n                animate=\"visible\"\n                exit=\"exit\"\n                variants={overlayVariants}\n                className=\"fixed inset-0 z-50 backdrop-blur-sm bg-black/50 flex items-center justify-center p-4\"\n            >\n                <motion.div\n                    ref={modalRef}\n                    initial=\"hidden\"\n                    animate=\"visible\"\n                    exit=\"exit\"\n                    variants={modalVariants}\n                    className=\"max-w-md w-full overflow-hidden\"\n                >\n                    {/* Success Screen */}\n                    {showSuccessScreen ? (\n                        <motion.div\n                            key=\"success\"\n                            variants={successVariants}\n                            className=\"bg-white dark:bg-gray-900 rounded-xl shadow-2xl border border-green-100 dark:border-green-900 overflow-hidden\"\n                        >\n                            <div className=\"p-6 text-center\">\n                                <div className=\"w-16 h-16 mx-auto mb-4 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center\">\n                                    <Check className=\"h-8 w-8 text-green-600 dark:text-green-400\" />\n                                </div>\n                                <h3 className=\"text-xl font-semibold mb-2\">Perfect Title Selected!</h3>\n                                <p className=\"text-lg text-muted-foreground mb-4\">Your changelog is going to look great!</p>\n                                <div className=\"bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/10 p-4 rounded-lg border border-green-100 dark:border-green-800/30 mt-4\">\n                                    <p className=\"font-medium\">{selectedTitle}</p>\n                                </div>\n\n                                <div className=\"mt-6 flex justify-center gap-2\">\n                                    <Button\n                                        variant=\"outline\"\n                                        size=\"sm\"\n                                        onClick={() => {\n                                            setShowSuccessScreen(false);\n                                        }}\n                                    >\n                                        <RotateCcw className=\"mr-2 h-3.5 w-3.5\" />\n                                        Pick Another Title\n                                    </Button>\n                                </div>\n                            </div>\n                        </motion.div>\n                    ) : (\n                        <div className=\"bg-white dark:bg-gray-900 rounded-xl shadow-2xl overflow-hidden\">\n                            {/* Header */}\n                            <div className=\"bg-gradient-to-r from-slate-50 to-slate-100 dark:from-slate-800 dark:to-slate-900 border-b p-5 flex justify-between items-center\">\n                                <div className=\"flex items-center gap-2\">\n                                    <div className=\"bg-primary/10 p-1.5 rounded-md\">\n                                        <Pencil className=\"w-5 h-5 text-primary\" />\n                                    </div>\n                                    <h3 className=\"font-semibold text-lg\">Title Generator</h3>\n                                </div>\n                                <Button\n                                    variant=\"ghost\"\n                                    size=\"icon\"\n                                    onClick={() => setIsOpen(false)}\n                                    className=\"h-8 w-8 rounded-full\"\n                                >\n                                    <X className=\"h-4 w-4\" />\n                                </Button>\n                            </div>\n\n                            {/* Content */}\n                            <div className=\"p-5\">\n                                {isLoading ? (\n                                    <div className=\"flex flex-col items-center justify-center py-12\">\n                                        <div className=\"relative w-16 h-16\">\n                                            <motion.div\n                                                animate={{ rotate: 360 }}\n                                                transition={{ duration: 2, repeat: Infinity, ease: \"linear\" }}\n                                                className=\"absolute inset-0 rounded-full border-t-2 border-primary\"\n                                            />\n                                            <Sparkles className=\"h-6 w-6 text-primary absolute inset-0 m-auto\" />\n                                        </div>\n                                        <p className=\"text-muted-foreground mt-4\">Crafting perfect titles for your changelog...</p>\n                                    </div>\n                                ) : error ? (\n                                    <div className=\"text-center py-8\">\n                                        <p className=\"text-destructive mb-4\">\n                                            {error.message || 'Failed to generate titles'}\n                                        </p>\n                                        <Button onClick={() => generateTitles()}>\n                                            <Sparkles className=\"mr-2 h-4 w-4\" />\n                                            Try Again\n                                        </Button>\n                                    </div>\n                                ) : (\n                                    <>\n                                        <div className=\"flex items-center justify-between mb-5\">\n                                            <p className=\"text-sm text-muted-foreground\">\n                                                Select the perfect title for your changelog.\n                                            </p>\n                                            {selectedTitle && (\n                                                <div className=\"text-xs text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-900/20 py-1 px-2 rounded-full flex items-center gap-1\">\n                                                    <Check className=\"h-3 w-3\" />\n                                                    <span>Title selected</span>\n                                                </div>\n                                            )}\n                                        </div>\n\n                                        <div className=\"space-y-3 mb-6\">\n                                            {titles.map((title, index) => (\n                                                <div\n                                                    key={index}\n                                                    className={cn(\n                                                        \"relative rounded-lg border transition-all duration-200\",\n                                                        getTitleBackground(title.style, index === aiRecommendation),\n                                                        title.isLocked ? \"ring-2 ring-primary/30\" : \"\",\n                                                        index === aiRecommendation ? \"shadow-md\" : \"\",\n                                                        title.text === selectedTitle ? \"ring-2 ring-green-400\" : \"\"\n                                                    )}\n                                                >\n                                                    {/* AI recommendation badge */}\n                                                    {index === aiRecommendation && (\n                                                        <div className=\"absolute -top-2 -right-2 bg-[hsl(var(--secondary)_/_1)] z-50 text-white text-xs font-medium px-2 py-0.5 rounded-full flex items-center gap-1 shadow-sm\">\n                                                            <Lightbulb className=\"h-3 w-3\" />\n                                                            <span>AI Pick</span>\n                                                        </div>\n                                                    )}\n\n                                                    <div\n                                                        className=\"p-4 cursor-pointer hover:bg-black/5 dark:hover:bg-white/5 transition-all\"\n                                                        onClick={() => handleSelectTitle(title.text)}\n                                                    >\n                                                        <div className=\"flex justify-between items-start mb-2\">\n                                                            <div\n                                                                className={cn(\n                                                                    \"text-xs font-medium rounded-full py-0.5 px-2 bg-white/60 dark:bg-gray-800/40 flex items-center gap-1\",\n                                                                    getStyleColor(title.style)\n                                                                )}\n                                                            >\n                                                                {getTitleIcon(title.style)}\n                                                                <span className=\"capitalize\">{title.style}</span>\n                                                            </div>\n                                                            <Button\n                                                                variant=\"ghost\"\n                                                                size=\"icon\"\n                                                                className=\"h-6 w-6 rounded-full\"\n                                                                onClick={(e) => {\n                                                                    e.stopPropagation();\n                                                                    toggleLockTitle(index);\n                                                                }}\n                                                            >\n                                                                {title.isLocked ? (\n                                                                    <Lock className=\"h-3.5 w-3.5 text-primary\" />\n                                                                ) : (\n                                                                    <UnlockIcon className=\"h-3.5 w-3.5 text-muted-foreground\" />\n                                                                )}\n                                                            </Button>\n                                                        </div>\n                                                        <p className=\"text-base font-medium\">{title.text}</p>\n\n                                                        <div className=\"mt-2 flex items-center gap-1\">\n                                                            <div className=\"h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full flex-grow\">\n                                                                <div\n                                                                    className={cn(\n                                                                        \"h-full rounded-full\",\n                                                                        title.style === 'primary' ? \"bg-blue-500\" :\n                                                                            title.style === 'creative' ? \"bg-purple-500\" :\n                                                                                \"bg-emerald-500\"\n                                                                    )}\n                                                                    style={{ width: `${title.score}%` }}\n                                                                />\n                                                            </div>\n                                                            <span className=\"text-xs text-muted-foreground\">{title.score}%</span>\n                                                        </div>\n                                                    </div>\n                                                </div>\n                                            ))}\n                                        </div>\n\n                                        <div className=\"flex gap-2 justify-center items-center\">\n                                            <Button\n                                                variant=\"outline\"\n                                                size=\"sm\"\n                                                onClick={handleReset}\n                                                className=\"gap-1\"\n                                            >\n                                                <Palette className=\"h-3.5 w-3.5\" />\n                                                <span>Generate New Options</span>\n                                            </Button>\n\n                                            <div className=\"text-xs text-muted-foreground flex items-center gap-1 ml-2\">\n                                                <Lock className=\"h-3 w-3\" />\n                                                <span>Lock titles to keep them</span>\n                                            </div>\n                                        </div>\n                                    </>\n                                )}\n                            </div>\n                        </div>\n                    )}\n                </motion.div>\n            </motion.div>\n        </AnimatePresence>,\n        document.body\n    ) : null;\n\n    return (\n        <>\n            <Tooltip>\n                <TooltipTrigger asChild>\n                    <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        className={cn(\n                            \"h-8 w-8 p-0\",\n                            selectedTitle ? \"text-primary bg-primary/10\" : \"\"\n                        )}\n                        onClick={() => setIsOpen(true)}\n                    >\n                        <Sparkles className=\"h-4 w-4\" />\n                    </Button>\n                </TooltipTrigger>\n                <TooltipContent side=\"top\">\n                    {selectedTitle ? \"Change AI-generated title\" : \"Generate title with AI\"}\n                </TooltipContent>\n            </Tooltip>\n\n            {Modal}\n        </>\n    );\n}"
  },
  {
    "path": "components/changelog/editor/EditorHeader.tsx",
    "content": "import React, {useCallback, useMemo} from 'react';\nimport {Button} from '@/components/ui/button';\nimport {CheckCircle2, ChevronLeft, Clock, Edit3, Loader2, Save, Star, ExternalLink, MoreHorizontal} from 'lucide-react';\nimport {Separator} from '@/components/ui/separator';\nimport {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from '@/components/ui/tooltip';\nimport {Alert, AlertDescription} from '@/components/ui/alert';\nimport {Badge} from '@/components/ui/badge';\nimport {Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetTrigger} from '@/components/ui/sheet';\nimport {ChangelogActionRequest} from \"@/components/changelog/ChangelogActionRequest\";\nimport {ScheduleEntryDialog} from \"@/components/changelog/editor/scheduler/ScheduleEntryDialog\";\nimport {useQuery, useQueryClient} from '@tanstack/react-query';\nimport {AnimatePresence, motion} from 'framer-motion';\nimport {cn} from '@/lib/utils';\nimport {formatDistanceToNow, isAfter} from 'date-fns';\nimport TagSelector from './TagSelector';\nimport VersionSelector from './VersionSelector';\nimport AITitleGenerator from './AITitleGenerator';\nimport {useBookmarks} from \"@/hooks/useBookmarks\";\nimport {toast} from \"@/hooks/use-toast\";\n\n// ===== Type Definitions =====\n\ninterface Tag {\n    id: string;\n    name: string;\n}\n\ninterface EntryData {\n    publishedAt?: string;\n    scheduledAt?: string;\n    title: string;\n    version: string;\n    content: string;\n    tags: Tag[];\n    createdAt?: string;\n    updatedAt?: string;\n}\n\ninterface ProjectData {\n    id: string;\n    name: string;\n    requireApproval: boolean;\n    allowAutoPublish: boolean;\n    emailConfig?: {\n        enabled: boolean;\n    };\n}\n\ninterface UserData {\n    id: string;\n    email: string;\n    role: 'ADMIN' | 'STAFF' | 'VIEWER';\n}\n\ninterface EditorHeaderProps {\n    title: string;\n    isSaving: boolean;\n    hasUnsavedChanges: boolean;\n    lastSaveError: string | null;\n    onManualSave: () => Promise<void>;\n    onBack: () => void;\n    isPublished: boolean;\n    projectId: string;\n    entryId?: string;\n    version: string;\n    onVersionChange: (version: string) => void;\n    onVersionConflict?: (hasConflict: boolean) => void;\n    hasVersionConflict?: boolean;\n    selectedTags: Tag[];\n    availableTags: Tag[];\n    onTagsChange: (tags: Tag[]) => void;\n    onTitleChange: (title: string) => void;\n    content: string;\n    aiApiKey?: string;\n}\n\n// ===== Bookmark Button Component =====\n\ninterface BookmarkButtonProps {\n    entryId?: string;\n    projectId: string;\n    title: string;\n}\n\nfunction BookmarkButton({entryId, projectId, title}: BookmarkButtonProps) {\n    const {toggleBookmark, isBookmarked} = useBookmarks({\n        projectId,\n        entryId: entryId || undefined\n    });\n\n    const handleBookmarkClick = useCallback(async () => {\n        if (!entryId) return;\n        await toggleBookmark(entryId, title, projectId);\n    }, [entryId, title, projectId, toggleBookmark]);\n\n    // Don't show bookmark button for new entries (no entryId)\n    if (!entryId) {\n        return null;\n    }\n\n    return (\n        <TooltipProvider>\n            <Tooltip>\n                <TooltipTrigger asChild>\n                    <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        onClick={handleBookmarkClick}\n                        className={cn(\n                            \"flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-colors\",\n                            isBookmarked && \"text-amber-600 hover:text-amber-700\"\n                        )}\n                    >\n                        <Star\n                            className={cn(\n                                \"h-3.5 w-3.5\",\n                                isBookmarked && \"fill-amber-500 text-amber-500\"\n                            )}\n                        />\n                        <span className=\"text-xs font-medium\">\n                            {isBookmarked ? \"Bookmarked\" : \"Bookmark\"}\n                        </span>\n                    </Button>\n                </TooltipTrigger>\n                <TooltipContent>\n                    <p className=\"text-xs\">\n                        {isBookmarked\n                            ? \"Remove bookmark from sidebar\"\n                            : \"Add bookmark to sidebar\"\n                        }\n                    </p>\n                </TooltipContent>\n            </Tooltip>\n        </TooltipProvider>\n    );\n}\n\n// ===== WWC Open Button Component =====\n\ninterface WWCOpenButtonProps {\n    title: string;\n    content: string;\n    version: string;\n    tags: Tag[];\n    projectId: string;\n    entryId?: string;\n}\n\nfunction WWCOpenButton({title, content, version, tags, projectId, entryId}: WWCOpenButtonProps) {\n    const handleOpenInWWC = useCallback(() => {\n        try {\n            // Generate WWC protocol URL\n            const baseUrl = 'wwc://open';\n            const url = new URL(baseUrl);\n\n            // Add path segments\n            if (projectId) {\n                url.pathname = `/${projectId}`;\n                if (entryId) {\n                    url.pathname += `/${entryId}`;\n                }\n            }\n\n            // Add query parameters\n            url.searchParams.set('serverUrl', window.location.origin);\n            url.searchParams.set('instanceType', 'changerawr');\n            url.searchParams.set('action', entryId ? 'edit' : 'create');\n\n            if (title) {\n                url.searchParams.set('title', encodeURIComponent(title));\n            }\n            if (content) {\n                url.searchParams.set('content', encodeURIComponent(content));\n            }\n            if (version) {\n                url.searchParams.set('version', encodeURIComponent(version));\n            }\n            if (tags && tags.length > 0) {\n                const tagsString = tags.map(tag => encodeURIComponent(tag.name)).join(',');\n                url.searchParams.set('tags', tagsString);\n            }\n\n            const wwcUrl = url.toString();\n\n            // Try to open the protocol URL directly\n            window.location.href = wwcUrl;\n\n            toast({\n                title: \"Opening in WriteWithCum\",\n                description: \"If the app doesn't open, please ensure WriteWithCum is installed.\",\n                duration: 3000\n            });\n        } catch (error) {\n            console.error('Failed to generate WWC URL:', error);\n            toast({\n                title: \"Failed to open\",\n                description: \"Could not generate the protocol URL to open in WriteWithCum.\",\n                variant: \"destructive\"\n            });\n        }\n    }, [title, content, version, tags, projectId, entryId]);\n\n    // Only show if there's meaningful content to share\n    const hasContent = title.trim() || content.trim() || version.trim();\n    if (!hasContent) {\n        return null;\n    }\n\n    if (process.env.NEXT_PUBLIC_SHOW_WWC_TOOLING !== 'true') return null;\n\n    return (\n        <Button\n            variant=\"outline\"\n            size=\"sm\"\n            onClick={handleOpenInWWC}\n            className=\"flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors border-dashed\"\n        >\n            <ExternalLink className=\"h-3.5 w-3.5\" />\n            <span className=\"text-xs font-medium\">Open in WWC</span>\n        </Button>\n    );\n}\n\n// ===== Main Component =====\n\nconst EditorHeader: React.FC<EditorHeaderProps> = ({\n                                                       title,\n                                                       isSaving,\n                                                       hasUnsavedChanges,\n                                                       lastSaveError,\n                                                       onManualSave,\n                                                       onBack,\n                                                       // eslint-disable-next-line @typescript-eslint/no-unused-vars\n                                                       isPublished,\n                                                       projectId,\n                                                       entryId,\n                                                       version,\n                                                       onVersionChange,\n                                                       onVersionConflict,\n                                                       hasVersionConflict = false,\n                                                       selectedTags,\n                                                       availableTags,\n                                                       onTagsChange,\n                                                       onTitleChange,\n                                                       content,\n                                                       aiApiKey,\n                                                   }) => {\n    const queryClient = useQueryClient();\n\n    // ===== Data Fetching =====\n    const {data: entryData} = useQuery<EntryData>({\n        queryKey: ['changelog-entry', projectId, entryId],\n        queryFn: async (): Promise<EntryData> => {\n            if (!entryId) throw new Error('No entry ID provided');\n            const response = await fetch(`/api/projects/${projectId}/changelog/${entryId}`);\n            if (!response.ok) {\n                throw new Error(`Failed to fetch entry: ${response.statusText}`);\n            }\n            return response.json();\n        },\n        enabled: !!entryId,\n        staleTime: 1000 * 60 * 2,\n    });\n\n    const {data: projectData} = useQuery<ProjectData>({\n        queryKey: ['project-settings', projectId],\n        queryFn: async (): Promise<ProjectData> => {\n            const [settingsResponse, emailResponse] = await Promise.all([\n                fetch(`/api/projects/${projectId}/settings`),\n                fetch(`/api/projects/${projectId}/integrations/email`).catch(() => null)\n            ]);\n\n            if (!settingsResponse.ok) {\n                throw new Error('Failed to fetch project settings');\n            }\n\n            const settings = await settingsResponse.json();\n            let emailConfig = null;\n\n            if (emailResponse?.ok) {\n                emailConfig = await emailResponse.json();\n            }\n\n            return {\n                ...settings,\n                emailConfig\n            };\n        },\n        staleTime: 1000 * 60 * 5,\n    });\n\n    const {data: userData} = useQuery<UserData>({\n        queryKey: ['current-user'],\n        queryFn: async (): Promise<UserData> => {\n            const response = await fetch('/api/auth/me');\n            if (!response.ok) {\n                throw new Error('Failed to fetch user data');\n            }\n            return response.json();\n        },\n        staleTime: 1000 * 60 * 10,\n    });\n\n    // ===== Computed Values =====\n    const computedValues = useMemo(() => {\n        // Use entryData.publishedAt as the source of truth for published status\n        const currentPublishStatus = !!entryData?.publishedAt;\n        const currentScheduleStatus = !!entryData?.scheduledAt;\n        const scheduledAt = entryData?.scheduledAt;\n\n        // Validation checks\n        const hasTitle = title.trim() !== '';\n        const hasContent = content.trim() !== '';\n        const hasVersion = version.trim() !== '';\n        const noConflict = !hasVersionConflict;\n\n        // Can perform actions\n        const canSave = hasUnsavedChanges && !isSaving && hasTitle && hasContent && hasVersion && noConflict;\n        const canPublish = hasTitle && hasContent && hasVersion && noConflict;\n        const canSchedule = hasTitle && hasContent && hasVersion && noConflict && !currentPublishStatus;\n\n        // Schedule status info\n        const isScheduledInFuture = scheduledAt && isAfter(new Date(scheduledAt), new Date());\n        const scheduleTimeDistance = scheduledAt && isScheduledInFuture\n            ? formatDistanceToNow(new Date(scheduledAt), {addSuffix: true})\n            : null;\n\n        // Entry metadata\n        const isNewEntry = !entryId;\n        const lastUpdated = entryData?.updatedAt ? formatDistanceToNow(new Date(entryData.updatedAt), {addSuffix: true}) : null;\n\n        return {\n            currentPublishStatus,\n            currentScheduleStatus,\n            scheduledAt,\n            isScheduledInFuture,\n            scheduleTimeDistance,\n            canSave,\n            canPublish,\n            canSchedule,\n            hasTitle,\n            hasContent,\n            hasVersion,\n            noConflict,\n            isNewEntry,\n            lastUpdated\n        };\n    }, [\n        entryData?.publishedAt, // This is the key change - rely on server data\n        entryData?.scheduledAt,\n        entryData?.updatedAt,\n        title,\n        content,\n        version,\n        hasVersionConflict,\n        hasUnsavedChanges,\n        isSaving,\n        entryId\n    ]); // Removed isPublished from dependencies since we use entryData\n\n    // ===== Event Handlers =====\n    const handleActionSuccess = useCallback(() => {\n        // Invalidate all related queries to ensure fresh data\n        queryClient.invalidateQueries({queryKey: ['changelog-entry', projectId, entryId]});\n        queryClient.invalidateQueries({queryKey: ['project-versions', projectId]});\n        queryClient.invalidateQueries({queryKey: ['changelog-entries', projectId]});\n\n        // Force a refetch of the current entry data\n        queryClient.refetchQueries({queryKey: ['changelog-entry', projectId, entryId]});\n    }, [queryClient, projectId, entryId]);\n\n    const handleScheduleChange = useCallback(() => {\n        // Invalidate and refetch entry data when schedule changes\n        queryClient.invalidateQueries({queryKey: ['changelog-entry', projectId, entryId]});\n        queryClient.refetchQueries({queryKey: ['changelog-entry', projectId, entryId]});\n    }, [queryClient, projectId, entryId]);\n\n    const handleDeleteSuccess = useCallback(() => {\n        queryClient.invalidateQueries({queryKey: ['changelog-entry', projectId]});\n        onBack();\n    }, [queryClient, projectId, onBack]);\n\n    // ===== Enhanced Status Bar Component =====\n    const StatusBar = useMemo(() => {\n        const items = [];\n\n        // Primary status (published/scheduled/draft)\n        if (computedValues.currentPublishStatus) {\n            items.push(\n                <Badge\n                    key=\"published\"\n                    variant=\"success\"\n                >\n                    <CheckCircle2 className=\"h-3 w-3 mr-1\"/>\n                    Published\n                    {computedValues.lastUpdated && (\n                        <span className=\"ml-1 opacity-75\">• {computedValues.lastUpdated}</span>\n                    )}\n                </Badge>\n            );\n        } else if (computedValues.currentScheduleStatus && computedValues.isScheduledInFuture) {\n            items.push(\n                <Badge\n                    key=\"scheduled\"\n                    variant=\"info\"\n                >\n                    <Clock className=\"h-3 w-3 mr-1\"/>\n                    Scheduled {computedValues.scheduleTimeDistance}\n                </Badge>\n            );\n        } else if (!computedValues.isNewEntry) {\n            items.push(\n                <Badge\n                    key=\"draft\"\n                    variant=\"outline\"\n                    className=\"text-gray-600 hover:bg-gray-50 transition-colors\"\n                >\n                    <Edit3 className=\"h-3 w-3 mr-1\"/>\n                    Draft\n                    {computedValues.lastUpdated && (\n                        <span className=\"ml-1 opacity-75\">• {computedValues.lastUpdated}</span>\n                    )}\n                </Badge>\n            );\n        }\n\n        // Save status\n        if (isSaving) {\n            items.push(\n                <div key=\"saving\" className=\"flex items-center text-sm text-blue-600 font-medium\">\n                    <Loader2 className=\"h-3 w-3 mr-2 animate-spin\"/>\n                    Saving changes...\n                </div>\n            );\n        } else if (hasUnsavedChanges) {\n            items.push(\n                <div key=\"unsaved\" className=\"flex items-center text-sm text-amber-600 font-medium\">\n                    <div className=\"h-2 w-2 bg-amber-500 rounded-full mr-2 animate-pulse\"/>\n                    Unsaved changes\n                </div>\n            );\n        }\n\n        // Conflict status\n        if (hasVersionConflict) {\n            items.push(\n                <div key=\"conflict\" className=\"flex items-center text-sm text-red-600 font-medium\">\n                    Version conflict\n                </div>\n            );\n        }\n\n        if (items.length === 0) return null;\n\n        return (\n            <div className=\"flex items-center gap-3\">\n                <AnimatePresence mode=\"popLayout\">\n                    {items.map((item) => (\n                        <motion.div\n                            key={item.key}\n                            initial={{opacity: 0, x: -20}}\n                            animate={{opacity: 1, x: 0}}\n                            exit={{opacity: 0, x: 20}}\n                            transition={{duration: 0.2}}\n                        >\n                            {item}\n                        </motion.div>\n                    ))}\n                </AnimatePresence>\n            </div>\n        );\n    }, [\n        computedValues.currentPublishStatus,\n        computedValues.currentScheduleStatus,\n        computedValues.isScheduledInFuture,\n        computedValues.scheduleTimeDistance,\n        computedValues.lastUpdated,\n        computedValues.isNewEntry,\n        isSaving,\n        hasUnsavedChanges,\n        hasVersionConflict\n    ]);\n\n    // ===== Action Buttons =====\n    const SaveButton = useMemo(() => (\n        <Tooltip>\n            <TooltipTrigger asChild>\n                <Button\n                    variant=\"outline\"\n                    size=\"sm\"\n                    onClick={onManualSave}\n                    disabled={!computedValues.canSave}\n                    className={cn(\n                        \"transition-all duration-200 shadow-sm hover:shadow-md\",\n                        hasVersionConflict && \"border-red-300 text-red-600 hover:bg-red-50\",\n                        computedValues.canSave && \"border-blue-200 text-blue-700 hover:bg-blue-50\"\n                    )}\n                >\n                    {isSaving ? (\n                        <>\n                            <Loader2 className=\"h-4 w-4 mr-2 animate-spin\"/>\n                            Saving...\n                        </>\n                    ) : (\n                        <>\n                            <Save className=\"h-4 w-4 mr-2\"/>\n                            Save\n                        </>\n                    )}\n                </Button>\n            </TooltipTrigger>\n            <TooltipContent>\n                {!computedValues.canSave\n                    ? (isSaving ? \"Saving in progress...\" : hasVersionConflict ? \"Resolve version conflict first\" : \"No changes to save\")\n                    : \"Save your changes\"}\n            </TooltipContent>\n        </Tooltip>\n    ), [computedValues.canSave, hasVersionConflict, isSaving, onManualSave]);\n\n    const ScheduleButton = useMemo(() => {\n        if (!entryId || !projectData || !userData) return null;\n\n        return (\n            <ScheduleEntryDialog\n                entryId={entryId}\n                projectId={projectId}\n                entryTitle={title}\n                isScheduled={computedValues.currentScheduleStatus}\n                scheduledAt={computedValues.scheduledAt}\n                isPublished={computedValues.currentPublishStatus}\n                projectRequiresApproval={projectData.requireApproval}\n                projectHasEmailConfig={!!projectData.emailConfig?.enabled}\n                userRole={userData.role}\n                onScheduleChange={handleScheduleChange}\n            />\n        );\n    }, [\n        entryId,\n        projectId,\n        title,\n        projectData,\n        userData,\n        computedValues.currentScheduleStatus,\n        computedValues.scheduledAt,\n        computedValues.currentPublishStatus,\n        handleScheduleChange\n    ]);\n\n    const PublishButton = useMemo(() => {\n        if (!entryId) return null;\n\n        const isDisabled = (!computedValues.canPublish && !computedValues.currentPublishStatus) ||\n            (computedValues.currentScheduleStatus && computedValues.isScheduledInFuture);\n\n        return (\n            <span>\n                        <ChangelogActionRequest\n                            projectId={projectId}\n                            entryId={entryId}\n                            action={computedValues.currentPublishStatus ? \"UNPUBLISH\" : \"PUBLISH\"}\n                            title={title}\n                            isPublished={computedValues.currentPublishStatus}\n                            variant={computedValues.currentPublishStatus ? \"outline\" : \"default\"}\n                            size=\"sm\"\n                            onSuccess={handleActionSuccess}\n                            className={cn(\n                                \"transition-all duration-200 shadow-sm hover:shadow-md\",\n                                !computedValues.currentPublishStatus && !isDisabled,\n                                isDisabled && \"opacity-50\"\n                            )}\n                        />\n                    </span>\n\n        );\n    }, [\n        entryId,\n        projectId,\n        computedValues.currentPublishStatus,\n        computedValues.canPublish,\n        computedValues.currentScheduleStatus,\n        computedValues.isScheduledInFuture,\n        title,\n        handleActionSuccess\n    ]);\n\n    const DeleteButton = useMemo(() => {\n        if (!entryId) return null;\n\n        return (\n            <ChangelogActionRequest\n                projectId={projectId}\n                entryId={entryId}\n                action=\"DELETE\"\n                title={title}\n                variant=\"destructive\"\n                size=\"sm\"\n                onSuccess={handleDeleteSuccess}\n                className=\"shadow-sm hover:shadow-md transition-all duration-200\"\n            />\n        );\n    }, [entryId, projectId, title, handleDeleteSuccess]);\n\n    // ===== Error Alert =====\n    const ErrorAlert = useMemo(() => {\n        if (!lastSaveError && !hasVersionConflict) return null;\n\n        return (\n            <motion.div\n                initial={{opacity: 0, y: -10}}\n                animate={{opacity: 1, y: 0}}\n                exit={{opacity: 0, y: -10}}\n                transition={{duration: 0.2}}\n            >\n                <Alert\n                    variant={hasVersionConflict ? \"default\" : \"destructive\"}\n                    className={cn(\n                        \"max-w-md shadow-lg border-l-4\",\n                        hasVersionConflict ? \"border-l-amber-500 bg-amber-50/50\" : \"border-l-red-500\"\n                    )}\n                >\n                    <AlertDescription className=\"font-medium\">\n                        {hasVersionConflict\n                            ? \"Version conflict detected - please select a different version\"\n                            : lastSaveError\n                        }\n                    </AlertDescription>\n                </Alert>\n            </motion.div>\n        );\n    }, [lastSaveError, hasVersionConflict]);\n\n    // ===== Main Render =====\n    return (\n        <TooltipProvider>\n            <div\n                className=\"border-b bg-background/95 backdrop-blur-lg supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 shadow-sm\">\n                <div className=\"container max-w-7xl py-3 md:py-4\">\n                    <div className=\"flex flex-col gap-3 md:gap-4\">\n\n                        {/* === Row 1: Navigation and Primary Actions === */}\n                        <div className=\"flex items-center justify-between\">\n                            {/* Left: Back + Project */}\n                            <div className=\"flex items-center gap-2 md:gap-4 min-w-0\">\n                                <Button\n                                    variant=\"ghost\"\n                                    size=\"sm\"\n                                    onClick={onBack}\n                                    className=\"hover:bg-accent transition-colors duration-200 -ml-2\"\n                                >\n                                    <ChevronLeft className=\"h-4 w-4 mr-1\"/>\n                                    <span className=\"hidden sm:inline\">Back</span>\n                                </Button>\n\n                                {projectData && (\n                                    <div className=\"text-sm text-muted-foreground font-medium truncate\">\n                                        {projectData.name}\n                                    </div>\n                                )}\n                            </div>\n\n                            {/* Right: Error + Actions */}\n                            <div className=\"flex items-center gap-3\">\n                                <AnimatePresence mode=\"wait\">\n                                    {ErrorAlert}\n                                </AnimatePresence>\n\n                                {/* Desktop action buttons — original grouped layout */}\n                                <div className=\"hidden md:flex items-center gap-2\">\n                                    <WWCOpenButton\n                                        title={title}\n                                        content={content}\n                                        version={version}\n                                        tags={selectedTags}\n                                        projectId={projectId}\n                                        entryId={entryId}\n                                    />\n\n                                    {SaveButton}\n\n                                    {entryId && (\n                                        <>\n                                            <Separator orientation=\"vertical\" className=\"h-5\"/>\n\n                                            <div className=\"flex items-center gap-2\">\n                                                {ScheduleButton}\n                                                {PublishButton}\n                                            </div>\n\n                                            <Separator orientation=\"vertical\" className=\"h-5\"/>\n                                            {DeleteButton}\n                                        </>\n                                    )}\n                                </div>\n\n                                {/* Mobile: Save + Sheet trigger */}\n                                <div className=\"flex md:hidden items-center gap-1.5\">\n                                    {SaveButton}\n\n                                    {entryId && (\n                                        <Sheet>\n                                            <SheetTrigger asChild>\n                                                <Button variant=\"ghost\" size=\"sm\" className=\"h-8 w-8 p-0\">\n                                                    <MoreHorizontal className=\"h-4 w-4\"/>\n                                                    <span className=\"sr-only\">More actions</span>\n                                                </Button>\n                                            </SheetTrigger>\n                                            <SheetContent side=\"bottom\" className=\"rounded-t-xl pb-8\">\n                                                <SheetHeader className=\"text-left pb-4\">\n                                                    <SheetTitle className=\"text-base\">Entry Actions</SheetTitle>\n                                                    <SheetDescription className=\"text-sm\">\n                                                        {title || 'Untitled Entry'}\n                                                    </SheetDescription>\n                                                </SheetHeader>\n\n                                                <div className=\"flex flex-col gap-4\">\n                                                    {/* Status */}\n                                                    {StatusBar && (\n                                                        <div className=\"flex items-center gap-2 flex-wrap\">\n                                                            {StatusBar}\n                                                        </div>\n                                                    )}\n\n                                                    {/* Quick actions row */}\n                                                    <div className=\"flex items-center gap-2 flex-wrap\">\n                                                        <BookmarkButton\n                                                            entryId={entryId}\n                                                            projectId={projectId}\n                                                            title={title}\n                                                        />\n                                                        {aiApiKey && computedValues.hasContent && (\n                                                            <AITitleGenerator\n                                                                content={content}\n                                                                onSelectTitle={onTitleChange}\n                                                                apiKey={aiApiKey}\n                                                            />\n                                                        )}\n                                                        <WWCOpenButton\n                                                            title={title}\n                                                            content={content}\n                                                            version={version}\n                                                            tags={selectedTags}\n                                                            projectId={projectId}\n                                                            entryId={entryId}\n                                                        />\n                                                    </div>\n\n                                                    {/* Selectors row */}\n                                                    <div className=\"flex items-center gap-2\">\n                                                        <TagSelector\n                                                            selectedTags={selectedTags}\n                                                            availableTags={availableTags}\n                                                            onTagsChange={onTagsChange}\n                                                            content={content}\n                                                            aiApiKey={aiApiKey}\n                                                            projectId={projectId}\n                                                        />\n                                                        <VersionSelector\n                                                            version={version}\n                                                            onVersionChange={onVersionChange}\n                                                            onConflictDetected={onVersionConflict}\n                                                            projectId={projectId}\n                                                            entryId={entryId}\n                                                            disabled={isSaving}\n                                                        />\n                                                    </div>\n\n                                                    <Separator/>\n\n                                                    {/* Publishing actions — full-width buttons */}\n                                                    <div className=\"flex flex-col gap-2\">\n                                                        {ScheduleButton && (\n                                                            <div className=\"w-full [&>*]:w-full [&_button]:w-full\">\n                                                                {ScheduleButton}\n                                                            </div>\n                                                        )}\n                                                        {PublishButton && (\n                                                            <div className=\"w-full [&>*]:w-full [&_button]:w-full [&_span]:w-full\">\n                                                                {PublishButton}\n                                                            </div>\n                                                        )}\n                                                    </div>\n\n                                                    {/* Delete — separated at bottom */}\n                                                    {DeleteButton && (\n                                                        <>\n                                                            <Separator/>\n                                                            <div className=\"w-full [&>*]:w-full [&_button]:w-full\">\n                                                                {DeleteButton}\n                                                            </div>\n                                                        </>\n                                                    )}\n                                                </div>\n                                            </SheetContent>\n                                        </Sheet>\n                                    )}\n                                </div>\n                            </div>\n                        </div>\n\n                        {/* === Row 2: Title + Metadata (Desktop) === */}\n                        <div className=\"flex items-start justify-between gap-4\">\n                            <div className=\"flex-1 min-w-0\">\n                                <div className=\"flex items-center gap-3\">\n                                    <h1 className={cn(\n                                        \"text-lg md:text-2xl font-bold truncate\",\n                                        !computedValues.hasTitle && \"text-muted-foreground italic\"\n                                    )}>\n                                        {title || 'Untitled Entry'}\n                                    </h1>\n\n                                    {/* Desktop: Bookmark + AI inline with title */}\n                                    <div className=\"hidden md:flex items-center gap-1 flex-shrink-0\">\n                                        <BookmarkButton\n                                            entryId={entryId}\n                                            projectId={projectId}\n                                            title={title}\n                                        />\n                                        {aiApiKey && computedValues.hasContent && (\n                                            <AITitleGenerator\n                                                content={content}\n                                                onSelectTitle={onTitleChange}\n                                                apiKey={aiApiKey}\n                                            />\n                                        )}\n                                    </div>\n                                </div>\n\n                                {/* Status Bar — desktop: below title, mobile: in sheet */}\n                                <div className=\"mt-2 hidden md:block\">\n                                    {StatusBar}\n                                </div>\n                            </div>\n\n                            {/* Desktop: Tags + Version selectors */}\n                            <div className=\"hidden md:flex items-center gap-2 flex-shrink min-w-0\">\n                                <TagSelector\n                                    selectedTags={selectedTags}\n                                    availableTags={availableTags}\n                                    onTagsChange={onTagsChange}\n                                    content={content}\n                                    aiApiKey={aiApiKey}\n                                    projectId={projectId}\n                                />\n                                <VersionSelector\n                                    version={version}\n                                    onVersionChange={onVersionChange}\n                                    onConflictDetected={onVersionConflict}\n                                    projectId={projectId}\n                                    entryId={entryId}\n                                    disabled={isSaving}\n                                />\n                            </div>\n                        </div>\n\n                        {/* Mobile-only: status below title (tags/version are in the sheet) */}\n                        <div className=\"flex md:hidden items-center gap-2 flex-wrap -mt-1\">\n                            {StatusBar}\n                        </div>\n                    </div>\n                </div>\n            </div>\n        </TooltipProvider>\n    );\n};\n\nexport default EditorHeader;"
  },
  {
    "path": "components/changelog/editor/TagColorPicker.tsx",
    "content": "// components/ui/color-picker.tsx\n'use client';\n\nimport React, {useState} from 'react';\nimport {Button} from '@/components/ui/button';\nimport {Input} from '@/components/ui/input';\nimport {Label} from '@/components/ui/label';\nimport {\n    Popover,\n    PopoverContent,\n    PopoverTrigger,\n} from '@/components/ui/popover';\nimport {\n    Command,\n    CommandEmpty,\n    CommandGroup,\n    CommandInput,\n    CommandItem,\n} from '@/components/ui/command';\nimport {Check, Palette, X} from 'lucide-react';\nimport {cn} from '@/lib/utils';\nimport {TAG_COLOR_OPTIONS, getTagColorInfo, type TagColorOption} from '@/lib/types/changelog';\nimport {Badge} from '@/components/ui/badge'\n\ninterface ColorPickerProps {\n    value?: string | null;\n    onChange: (color: string | null) => void;\n    disabled?: boolean;\n    placeholder?: string;\n    showCustomInput?: boolean;\n}\n\nexport function ColorPicker({\n                                value,\n                                onChange,\n                                disabled = false,\n                                placeholder = 'Select a color',\n                                showCustomInput = true,\n                            }: ColorPickerProps) {\n    const [isOpen, setIsOpen] = useState(false);\n    const [customColor, setCustomColor] = useState('');\n    const [showCustom, setShowCustom] = useState(false);\n\n    const selectedColor = getTagColorInfo(value);\n\n    const handleColorSelect = (colorOption: TagColorOption) => {\n        onChange(colorOption.color);\n        setIsOpen(false);\n        setShowCustom(false);\n    };\n\n    const handleCustomColorSubmit = () => {\n        const hexPattern = /^#[0-9A-Fa-f]{6}$/;\n        if (hexPattern.test(customColor)) {\n            onChange(customColor);\n            setCustomColor('');\n            setShowCustom(false);\n            setIsOpen(false);\n        }\n    };\n\n    const handleClear = () => {\n        onChange(null);\n        setIsOpen(false);\n    };\n\n    return (\n        <Popover open={isOpen} onOpenChange={setIsOpen}>\n            <PopoverTrigger asChild>\n                <Button\n                    variant=\"outline\"\n                    disabled={disabled}\n                    className={cn(\n                        'w-full justify-start text-left font-normal',\n                        !value && 'text-muted-foreground'\n                    )}\n                >\n                    <div className=\"flex items-center gap-2\">\n                        {value ? (\n                            <>\n                                <div\n                                    className=\"h-4 w-4 rounded border border-gray-300\"\n                                    style={{backgroundColor: selectedColor.color}}\n                                />\n                                <span>{selectedColor.label}</span>\n                            </>\n                        ) : (\n                            <>\n                                <Palette className=\"h-4 w-4\"/>\n                                <span>{placeholder}</span>\n                            </>\n                        )}\n                    </div>\n                </Button>\n            </PopoverTrigger>\n            <PopoverContent className=\"w-64 p-0\" align=\"start\">\n                <Command>\n                    <CommandInput placeholder=\"Search colors...\"/>\n                    <CommandEmpty>No colors found.</CommandEmpty>\n                    <CommandGroup>\n                        <div className=\"grid grid-cols-4 gap-2 p-2\">\n                            {TAG_COLOR_OPTIONS.map((colorOption) => {\n                                const isSelected = value === colorOption.color;\n                                return (\n                                    <CommandItem\n                                        key={colorOption.value}\n                                        onSelect={() => handleColorSelect(colorOption)}\n                                        className=\"flex flex-col items-center gap-1 p-2 cursor-pointer hover:bg-accent rounded\"\n                                    >\n                                        <div className=\"relative\">\n                                            <div\n                                                className=\"h-8 w-8 rounded border-2 border-gray-300\"\n                                                style={{backgroundColor: colorOption.color}}\n                                            />\n                                            {isSelected && (\n                                                <div className=\"absolute inset-0 flex items-center justify-center\">\n                                                    <Check\n                                                        className=\"h-4 w-4\"\n                                                        style={{color: colorOption.textColor || '#ffffff'}}\n                                                    />\n                                                </div>\n                                            )}\n                                        </div>\n                                        <span className=\"text-xs text-center\">{colorOption.label}</span>\n                                    </CommandItem>\n                                );\n                            })}\n                        </div>\n                    </CommandGroup>\n                </Command>\n\n                <div className=\"border-t p-3 space-y-3\">\n                    {showCustomInput && (\n                        <>\n                            <Button\n                                variant=\"ghost\"\n                                size=\"sm\"\n                                onClick={() => setShowCustom(!showCustom)}\n                                className=\"w-full\"\n                            >\n                                <Palette className=\"h-4 w-4 mr-2\"/>\n                                Custom Color\n                            </Button>\n\n                            {showCustom && (\n                                <div className=\"space-y-2\">\n                                    <Label htmlFor=\"custom-color\" className=\"text-sm\">\n                                        Hex Color Code\n                                    </Label>\n                                    <div className=\"flex gap-2\">\n                                        <Input\n                                            id=\"custom-color\"\n                                            value={customColor}\n                                            onChange={(e) => setCustomColor(e.target.value)}\n                                            placeholder=\"#3b82f6\"\n                                            className=\"flex-1\"\n                                            pattern=\"^#[0-9A-Fa-f]{6}$\"\n                                        />\n                                        <Button\n                                            size=\"sm\"\n                                            onClick={handleCustomColorSubmit}\n                                            disabled={!/^#[0-9A-Fa-f]{6}$/.test(customColor)}\n                                        >\n                                            <Check className=\"h-4 w-4\"/>\n                                        </Button>\n                                    </div>\n                                </div>\n                            )}\n                        </>\n                    )}\n\n                    {value && (\n                        <Button\n                            variant=\"ghost\"\n                            size=\"sm\"\n                            onClick={handleClear}\n                            className=\"w-full text-muted-foreground hover:text-foreground\"\n                        >\n                            <X className=\"h-4 w-4 mr-2\"/>\n                            Clear Color\n                        </Button>\n                    )}\n                </div>\n            </PopoverContent>\n        </Popover>\n    );\n}\n\n// Utility component for displaying colored tags\ninterface ColoredTagProps {\n    name: string;\n    color?: string | null;\n    variant?: 'default' | 'outline' | 'secondary';\n    size?: 'sm' | 'default' | 'lg';\n    className?: string;\n    onClick?: () => void;\n    removable?: boolean;\n    onRemove?: () => void;\n}\n\nexport function ColoredTag({\n                               name,\n                               color,\n                               variant = 'default',\n                               size = 'default',\n                               className,\n                               onClick,\n                               removable = false,\n                               onRemove\n                           }: ColoredTagProps) {\n\n    return (\n        <Badge\n            variant={color ? variant : 'secondary'}\n            customColor={color || undefined}\n            className={cn(\n                size === 'sm' && 'text-xs px-2 py-1',\n                size === 'default' && 'text-sm px-2.5 py-1.5',\n                size === 'lg' && 'text-base px-3 py-2',\n                onClick && 'cursor-pointer hover:opacity-80',\n                className\n            )}\n            onClick={onClick}\n        >\n            {name}\n            {removable && onRemove && (\n                <button\n                    onClick={(e) => {\n                        e.stopPropagation();\n                        onRemove();\n                    }}\n                    className=\"ml-1 hover:bg-black/10 rounded-full p-0.5\"\n                >\n                    <X className=\"h-3 w-3\" />\n                </button>\n            )}\n        </Badge>\n    );\n}"
  },
  {
    "path": "components/changelog/editor/TagSelector.tsx",
    "content": "// components/changelog/editor/TagSelector.tsx\nimport React, {useState, useCallback} from 'react';\nimport {Button} from '@/components/ui/button';\nimport {Popover, PopoverContent, PopoverTrigger} from '@/components/ui/popover';\nimport {\n    Command,\n    CommandEmpty,\n    CommandGroup,\n    CommandInput,\n    CommandItem,\n    CommandList,\n    CommandSeparator\n} from \"@/components/ui/command\";\nimport {Tags, Check, Plus, Sparkles, Loader2, X, AlertCircle, CheckCircle, Palette, Lightbulb} from 'lucide-react';\nimport {Separator} from '@/components/ui/separator';\nimport {Badge} from \"@/components/ui/badge\";\nimport {cn} from '@/lib/utils';\nimport {motion, AnimatePresence} from 'framer-motion';\nimport {\n    Tooltip,\n    TooltipContent,\n    TooltipTrigger,\n    TooltipProvider\n} from '@/components/ui/tooltip';\nimport {Input} from '@/components/ui/input';\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardFooter,\n    CardHeader,\n    CardTitle,\n} from \"@/components/ui/card\";\nimport {ColorPicker, ColoredTag} from '@/components/changelog/editor/TagColorPicker';\nimport {TAG_COLOR_OPTIONS} from '@/lib/types/changelog';\n\ninterface Tag {\n    id: string;\n    name: string;\n    color?: string | null;\n}\n\ninterface TagSelectorProps {\n    selectedTags: Tag[];\n    availableTags: Tag[];\n    onTagsChange: (tags: Tag[]) => void;\n    content?: string; // For AI-powered tag suggestions\n    aiApiKey?: string; // API key for AI features\n    projectId: string; // Needed for creating new tags\n}\n\n// AI content processing parameters\nconst MAX_CHARS_PER_SECTION = 150; // Characters per extracted section\nconst SECTIONS_TO_EXTRACT = 3;     // Number of sections to extract\nconst SECTIONS_TO_ANALYZE = 3;     // Number of sections to actually send to AI\n\n// Tag suggestions shown when no tags exist - hardcoded suggestions\ninterface TagSuggestion {\n    name: string;\n    colorValue: string; // Reference to TAG_COLOR_OPTIONS value\n}\n\nconst TAG_SUGGESTIONS: TagSuggestion[] = [\n    {name: 'Feature', colorValue: 'green'},\n    {name: 'Bug Fixes', colorValue: 'red'},\n    {name: 'Improvement', colorValue: 'blue'},\n    {name: 'Other', colorValue: 'gray'},\n];\n\n\nexport default function TagSelector({\n                                        selectedTags,\n                                        availableTags,\n                                        onTagsChange,\n                                        content = '',\n                                        aiApiKey,\n                                        projectId\n                                    }: TagSelectorProps) {\n    const [search, setSearch] = useState('');\n    const [isOpen, setIsOpen] = useState(false);\n    const [isCreating, setIsCreating] = useState(false);\n    const [newTagName, setNewTagName] = useState('');\n    const [newTagColor, setNewTagColor] = useState<string | null>(null);\n    const [showColorPicker, setShowColorPicker] = useState(false);\n\n    // AI suggestion state\n    const [isGenerating, setIsGenerating] = useState(false);\n    const [suggestedTags, setSuggestedTags] = useState<Tag[]>([]);\n    const [suggestionError, setSuggestionError] = useState<string | null>(null);\n    const [showSuggestions, setShowSuggestions] = useState(false);\n\n    // Filter tags based on search\n    const filteredTags = search\n        ? availableTags.filter(tag =>\n            tag.name.toLowerCase().includes(search.toLowerCase()))\n        : availableTags;\n\n    // Calculate which suggested tags haven't been selected yet\n    const unselectedSuggestions = suggestedTags.filter(\n        tag => !selectedTags.some(selected => selected.id === tag.id)\n    );\n\n    /**\n     * Extract meaningful sections from content optimized for tag detection\n     * This approach samples from beginning, middle and end of the document\n     * to get a better representation of the full content.\n     */\n    const extractContentSections = (text: string): string[] => {\n        if (!text) return [];\n\n        const cleanedText = text.trim();\n        if (cleanedText.length <= MAX_CHARS_PER_SECTION * SECTIONS_TO_EXTRACT) {\n            return [cleanedText]; // Return all content if it's short enough\n        }\n\n        const sections: string[] = [];\n\n        // Extract beginning section (always include)\n        sections.push(extractSection(cleanedText, 0, MAX_CHARS_PER_SECTION));\n\n        // If there's more content, extract middle section\n        if (cleanedText.length > MAX_CHARS_PER_SECTION * 2) {\n            const middleStart = Math.floor(cleanedText.length / 2) - (MAX_CHARS_PER_SECTION / 2);\n            sections.push(extractSection(cleanedText, middleStart, MAX_CHARS_PER_SECTION));\n        }\n\n        // If there's more content, extract ending section\n        if (cleanedText.length > MAX_CHARS_PER_SECTION * 3) {\n            const endStart = Math.max(0, cleanedText.length - MAX_CHARS_PER_SECTION);\n            sections.push(extractSection(cleanedText, endStart, MAX_CHARS_PER_SECTION));\n        }\n\n        // Extract headings if there are any (often contains important context)\n        const headingMatches = cleanedText.match(/#+\\s+.*$/gm) || [];\n        if (headingMatches.length > 0) {\n            const headings = headingMatches.slice(0, 5).join('\\n');\n            if (headings.length > 0) {\n                sections.push(`Key sections:\\n${headings}`);\n            }\n        }\n\n        return sections;\n    };\n\n    /**\n     * Extract a section of content with intelligent boundaries\n     */\n    const extractSection = (text: string, startPos: number, length: number): string => {\n        // Safety checks\n        if (!text || startPos >= text.length) return '';\n\n        // Find start at paragraph or sentence boundary if possible\n        let actualStart = startPos;\n        let actualEnd = Math.min(text.length, startPos + length);\n\n        // If not starting at beginning, find a good start boundary\n        if (startPos > 0) {\n            // Try to find paragraph start\n            const paraStart = text.lastIndexOf('\\n\\n', startPos) + 2;\n            if (paraStart > 0 && paraStart < startPos && (startPos - paraStart) < length / 2) {\n                actualStart = paraStart;\n            } else {\n                // Try to find sentence start\n                const sentStart = text.lastIndexOf('. ', startPos) + 2;\n                if (sentStart > 0 && sentStart < startPos && (startPos - sentStart) < length / 3) {\n                    actualStart = sentStart;\n                }\n            }\n        }\n\n        // Find a good end boundary\n        if (actualEnd < text.length) {\n            // Try to find paragraph end\n            const paraEnd = text.indexOf('\\n\\n', actualEnd - 20);\n            if (paraEnd > 0 && paraEnd < (actualEnd + length / 3)) {\n                actualEnd = paraEnd;\n            } else {\n                // Try to find sentence end\n                const sentEnd = text.indexOf('. ', actualEnd - 20);\n                if (sentEnd > 0 && sentEnd < (actualEnd + length / 4)) {\n                    actualEnd = sentEnd + 1; // Include the period\n                }\n            }\n        }\n\n        // If this is not the beginning, add indication\n        const prefix = actualStart > 0 ? '... ' : '';\n        // If this is not the end, add indication\n        const suffix = actualEnd < text.length ? ' ...' : '';\n\n        return prefix + text.substring(actualStart, actualEnd) + suffix;\n    };\n\n    // Apply all suggested tags at once\n    const applyAllSuggestions = () => {\n        if (unselectedSuggestions.length === 0) return;\n\n        // Add all suggested tags that aren't already selected\n        const newTags = [...selectedTags, ...unselectedSuggestions];\n        onTagsChange(newTags);\n    };\n\n    // Handle creating a new tag with color support\n    const handleCreateTag = useCallback(async (name: string, color?: string | null) => {\n        if (!name.trim()) return;\n\n        setIsCreating(true);\n        try {\n            const response = await fetch(`/api/projects/${projectId}/changelog/tags`, {\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json'\n                },\n                body: JSON.stringify({\n                    name: name.trim(),\n                    color: color !== undefined ? color : newTagColor\n                })\n            });\n\n            if (!response.ok) {\n                throw new Error('Failed to create tag');\n            }\n\n            const newTag = await response.json();\n\n            // Add new tag to selected tags\n            onTagsChange([...selectedTags, newTag]);\n\n            // Reset form\n            setSearch('');\n            setNewTagName('');\n            setNewTagColor(null);\n            setShowColorPicker(false);\n        } catch (error) {\n            console.error('Error creating tag:', error);\n        } finally {\n            setIsCreating(false);\n        }\n    }, [projectId, selectedTags, onTagsChange, newTagColor]);\n\n    // Handle AI tag suggestions\n    const generateTagSuggestions = useCallback(async () => {\n        if (!content || !aiApiKey || !availableTags.length) {\n            setSuggestionError('Cannot generate suggestions without content or available tags');\n            return;\n        }\n\n        if (content.trim().length < 20) {\n            setSuggestionError('Content is too short for meaningful tag suggestions');\n            return;\n        }\n\n        setIsGenerating(true);\n        setSuggestionError(null);\n\n        try {\n            // Prepare prompt for the AI\n            const tagNames = availableTags.map(tag => tag.name).join(', ');\n\n            // Extract key sections from the content\n            const contentSections = extractContentSections(content);\n\n            // Limit number of sections if too many\n            const sectionsToUse = contentSections.slice(0, SECTIONS_TO_ANALYZE);\n\n            // Combine sections with section numbers for readability\n            const formattedSections = sectionsToUse.map((section, index) =>\n                `Section ${index + 1}:\\n${section}`\n            ).join('\\n\\n');\n\n            const prompt = `\nI need to categorize the following changelog content with appropriate tags.\nAvailable tags: ${tagNames}\n\nI'll provide key sections from the content below. Based on these sections, which tags (maximum 3) would be most relevant? \nOnly respond with tags from the provided list above, separated by commas.\nDo not add any explanations, just return the tag names.\n\n${formattedSections}\n            `.trim();\n\n            // Call AI API\n            const response = await fetch('https://api.secton.org/v1/chat/completions', {\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json',\n                    'Authorization': `Bearer ${aiApiKey}`\n                },\n                body: JSON.stringify({\n                    model: 'copilot-zero',\n                    messages: [\n                        {\n                            role: 'system',\n                            content: 'You are a skilled content tagger for a changelog system. Your job is to select the most appropriate tags for content.'\n                        },\n                        {role: 'user', content: prompt}\n                    ],\n                    temperature: 0.3,\n                    max_tokens: 30 // Reduced to save tokens\n                })\n            });\n\n            if (!response.ok) {\n                throw new Error('Failed to get tag suggestions');\n            }\n\n            const result = await response.json();\n            const suggestedTagsText = result.messages[result.messages.length - 1]?.content || '';\n\n            // Process the AI's response\n            const suggestedTagNames = suggestedTagsText\n                .split(',')\n                .map((tag: string) => tag.trim())\n                .filter(Boolean);\n\n            // Map the suggested tag names to actual tag objects\n            const validSuggestions = suggestedTagNames\n                .map((name: string) => {\n                    // Find case-insensitive match\n                    return availableTags.find(tag =>\n                        tag.name.toLowerCase() === name.toLowerCase()\n                    );\n                })\n                .filter(Boolean) as Tag[];\n\n            if (validSuggestions.length === 0) {\n                setSuggestionError('Could not generate suitable tag suggestions');\n            } else {\n                setSuggestedTags(validSuggestions);\n                setShowSuggestions(true);\n            }\n        } catch (err) {\n            console.error('Error suggesting tags:', err);\n            setSuggestionError(err instanceof Error ? err.message : 'Failed to analyze content');\n        } finally {\n            setIsGenerating(false);\n        }\n    }, [content, aiApiKey, availableTags]);\n\n    // Toggle tag selection\n    const toggleTag = useCallback((tag: Tag) => {\n        const isSelected = selectedTags.some(t => t.id === tag.id);\n\n        if (isSelected) {\n            onTagsChange(selectedTags.filter(t => t.id !== tag.id));\n        } else {\n            onTagsChange([...selectedTags, tag]);\n        }\n    }, [selectedTags, onTagsChange]);\n\n    // Clear all selected tags\n    const clearTags = () => {\n        if (selectedTags.length === 0) return;\n        onTagsChange([]);\n    };\n\n    return (\n        <TooltipProvider>\n            <Popover open={isOpen} onOpenChange={setIsOpen}>\n                <PopoverTrigger asChild>\n                    <Button variant=\"outline\" className=\"h-8 border-dashed\">\n                        <Tags className=\"mr-2 h-4 w-4\"/>\n                        {selectedTags?.length > 0 ? (\n                            <>\n                                <span className=\"hidden md:inline-block\">\n                                  {selectedTags.length} selected\n                                </span>\n                                <Separator orientation=\"vertical\" className=\"mx-2 h-4\"/>\n                            </>\n                        ) : (\n                            <span className=\"hidden md:inline-block\">Select tags</span>\n                        )}\n                        <Badge\n                            variant=\"secondary\"\n                            className=\"rounded-sm px-1 font-normal\"\n                        >\n                            {selectedTags?.length}\n                        </Badge>\n                    </Button>\n                </PopoverTrigger>\n                <PopoverContent className=\"w-[350px] p-0\" align=\"start\">\n                    {/* Header section with actions */}\n                    <div className=\"flex items-center justify-between p-2 border-b\">\n                        <div className=\"flex items-center gap-2\">\n                            <Tags className=\"h-4 w-4 text-muted-foreground\"/>\n                            <span className=\"text-sm font-medium\">Tags</span>\n                        </div>\n\n                        <div className=\"flex items-center gap-1\">\n                            <Tooltip>\n                                <TooltipTrigger asChild>\n                                    <Button\n                                        variant=\"ghost\"\n                                        size=\"sm\"\n                                        className=\"h-7 px-2\"\n                                        onClick={clearTags}\n                                        disabled={selectedTags.length === 0}\n                                    >\n                                        <X className=\"h-3.5 w-3.5 mr-1\"/>\n                                        <span className=\"text-xs\">Clear</span>\n                                    </Button>\n                                </TooltipTrigger>\n                                <TooltipContent side=\"bottom\">Clear all selected tags</TooltipContent>\n                            </Tooltip>\n                        </div>\n                    </div>\n\n                    <Command>\n                        <div className=\"flex items-center border-b p-1\">\n                            <CommandInput\n                                placeholder=\"Search tags...\"\n                                value={search}\n                                onValueChange={setSearch}\n                                className=\"flex-1\"\n                            />\n                            {aiApiKey && content.length > 20 && (\n                                <Tooltip>\n                                    <TooltipTrigger asChild>\n                                        <Button\n                                            variant=\"ghost\"\n                                            size=\"icon\"\n                                            className=\"h-8 w-8 ml-1\"\n                                            disabled={isGenerating}\n                                            onClick={() => {\n                                                generateTagSuggestions();\n                                            }}\n                                        >\n                                            {isGenerating ? (\n                                                <Loader2 className=\"h-4 w-4 animate-spin\"/>\n                                            ) : (\n                                                <Sparkles className=\"h-4 w-4\"/>\n                                            )}\n                                        </Button>\n                                    </TooltipTrigger>\n                                    <TooltipContent side=\"bottom\">\n                                        Suggest tags with AI\n                                    </TooltipContent>\n                                </Tooltip>\n                            )}\n                        </div>\n\n                        <CommandList>\n                            {/* AI Suggestions Section */}\n                            <AnimatePresence>\n                                {showSuggestions && suggestedTags.length > 0 && (\n                                    <motion.div\n                                        initial={{opacity: 0, height: 0}}\n                                        animate={{opacity: 1, height: 'auto'}}\n                                        exit={{opacity: 0, height: 0}}\n                                        transition={{duration: 0.2}}\n                                        className=\"overflow-hidden\"\n                                    >\n                                        <Card className=\"border-0 shadow-none rounded-none bg-primary/5\">\n                                            <CardHeader className=\"p-2 pb-0\">\n                                                <CardTitle className=\"text-sm flex items-center gap-2\">\n                                                    <Sparkles className=\"h-3.5 w-3.5 text-primary\"/>\n                                                    AI Suggested Tags\n                                                </CardTitle>\n                                                <CardDescription className=\"text-xs\">\n                                                    Based on your changelog content\n                                                </CardDescription>\n                                            </CardHeader>\n                                            <CardContent className=\"p-2 pt-0\">\n                                                <div className=\"flex flex-wrap gap-1 mt-1\">\n                                                    {suggestedTags.map((tag, index) => {\n                                                        const isSelected = selectedTags.some(t => t.id === tag.id);\n\n                                                        return (\n                                                            <Badge\n                                                                key={`suggested-${tag.id}-${index}`}\n                                                                variant={isSelected ? \"default\" : \"outline\"}\n                                                                color={tag.color || undefined}\n                                                                className={cn(\n                                                                    \"cursor-pointer transition-all duration-200\",\n                                                                    !tag.color && (isSelected ? \"bg-primary\" : \"bg-primary/10 hover:bg-primary/20\")\n                                                                )}\n                                                                onClick={() => toggleTag(tag)}\n                                                            >\n                                                                {isSelected && <Check className=\"h-3 w-3 mr-1\"/>}\n                                                                {tag.name}\n                                                            </Badge>\n                                                        );\n                                                    })}\n                                                </div>\n                                            </CardContent>\n\n                                            <CardFooter className=\"p-2 pt-0 flex justify-between\">\n                                                {unselectedSuggestions.length > 0 && (\n                                                    <Button\n                                                        variant=\"ghost\"\n                                                        size=\"sm\"\n                                                        className=\"h-7 text-xs\"\n                                                        onClick={applyAllSuggestions}\n                                                    >\n                                                        <CheckCircle className=\"h-3 w-3 mr-1\"/>\n                                                        Apply all\n                                                    </Button>\n                                                )}\n                                                <Button\n                                                    variant=\"ghost\"\n                                                    size=\"sm\"\n                                                    className={cn(\"h-7 text-xs\", unselectedSuggestions.length > 0 ? \"\" : \"ml-auto\")}\n                                                    onClick={() => setShowSuggestions(false)}\n                                                >\n                                                    Hide\n                                                </Button>\n                                            </CardFooter>\n                                        </Card>\n                                        <CommandSeparator/>\n                                    </motion.div>\n                                )}\n                            </AnimatePresence>\n\n                            {suggestionError && (\n                                <div className=\"px-2 py-2 text-xs text-destructive flex items-start gap-2\">\n                                    <AlertCircle className=\"h-4 w-4 flex-shrink-0 mt-0.5\"/>\n                                    <span>{suggestionError}</span>\n                                </div>\n                            )}\n\n                            <CommandEmpty>\n                                <div className=\"py-3 px-4 text-center text-sm\">\n                                    {!search && availableTags.length === 0 && (\n                                        <div className=\"space-y-3\">\n                                            <div className=\"flex items-center justify-center gap-2 pb-2\">\n                                                <Lightbulb className=\"h-4 w-4 text-yellow-500\"/>\n                                                <p className=\"text-sm font-medium text-foreground\">Quick start tags</p>\n                                            </div>\n                                            <div className=\"flex flex-wrap gap-2 justify-center\">\n                                                {TAG_SUGGESTIONS.map((suggestion) => {\n                                                    const colorOption = TAG_COLOR_OPTIONS.find(opt => opt.value === suggestion.colorValue);\n                                                    return (\n                                                        <button\n                                                            key={suggestion.name}\n                                                            onClick={() => {\n                                                                handleCreateTag(suggestion.name, colorOption?.color || null);\n                                                            }}\n                                                            className=\"group transition-all hover:scale-105\"\n                                                        >\n                                                            <ColoredTag\n                                                                name={suggestion.name}\n                                                                color={colorOption?.color}\n                                                                size=\"sm\"\n                                                                className=\"cursor-pointer opacity-70 group-hover:opacity-100\"\n                                                            />\n                                                        </button>\n                                                    );\n                                                })}\n                                            </div>\n                                        </div>\n                                    )}\n                                    {search && (\n                                        <>\n                                            <p className=\"text-muted-foreground mb-2\">No tags found</p>\n                                        </>\n                                    )}\n                                    {search && (\n                                        <div className=\"mt-2 space-y-3\">\n                                            <p className=\"text-xs text-muted-foreground mb-2\">Create a new tag:</p>\n                                            <div className=\"flex gap-2\">\n                                                <Input\n                                                    type=\"text\"\n                                                    value={newTagName || search}\n                                                    onChange={(e) => setNewTagName(e.target.value)}\n                                                    className=\"flex-1 h-8 px-2 text-sm border border-input rounded-md\"\n                                                    placeholder=\"Tag name\"\n                                                />\n                                                <Button\n                                                    variant=\"outline\"\n                                                    size=\"sm\"\n                                                    className=\"h-8 px-2\"\n                                                    onClick={() => setShowColorPicker(!showColorPicker)}\n                                                >\n                                                    <Palette className=\"h-3.5 w-3.5\"/>\n                                                </Button>\n                                            </div>\n\n                                            {showColorPicker && (\n                                                <div className=\"w-full\">\n                                                    <ColorPicker\n                                                        value={newTagColor}\n                                                        onChange={setNewTagColor}\n                                                        placeholder=\"Choose tag color\"\n                                                        showCustomInput={true}\n                                                    />\n                                                </div>\n                                            )}\n\n                                            <Button\n                                                variant=\"default\"\n                                                size=\"sm\"\n                                                className=\"h-8 w-full\"\n                                                disabled={isCreating || !(newTagName || search).trim()}\n                                                onClick={() => handleCreateTag(newTagName || search)}\n                                            >\n                                                {isCreating ? (\n                                                    <Loader2 className=\"h-3.5 w-3.5 animate-spin mr-1\"/>\n                                                ) : (\n                                                    <Plus className=\"h-3.5 w-3.5 mr-1\"/>\n                                                )}\n                                                Create &ldquo;{(newTagName || search).trim()}&rdquo;\n                                            </Button>\n                                        </div>\n                                    )}\n                                </div>\n                            </CommandEmpty>\n\n                            <CommandGroup heading=\"Available Tags\">\n                                <div className=\"max-h-[200px] overflow-y-auto\">\n                                    {filteredTags.map((tag) => {\n                                        const isSelected = selectedTags.some(\n                                            (selectedTag) => selectedTag.id === tag.id\n                                        );\n                                        const isSuggested = suggestedTags.some(\n                                            (suggestedTag) => suggestedTag.id === tag.id\n                                        );\n\n                                        return (\n                                            <CommandItem\n                                                key={tag.id}\n                                                onSelect={() => toggleTag(tag)}\n                                                className={cn(\n                                                    isSuggested && !isSelected && \"bg-primary/5\"\n                                                )}\n                                            >\n                                                <div className={cn(\n                                                    \"mr-2 h-4 w-4 flex items-center justify-center rounded-sm\",\n                                                    isSelected ? \"bg-primary text-primary-foreground\" : \"border border-primary/20\"\n                                                )}>\n                                                    {isSelected && <Check className=\"h-3 w-3\"/>}\n                                                </div>\n\n                                                {tag.color && (\n                                                    <div\n                                                        className=\"h-3 w-3 rounded-full border border-gray-300 mr-2\"\n                                                        style={{backgroundColor: tag.color}}\n                                                    />\n                                                )}\n\n                                                <span className=\"flex-1\">{tag.name}</span>\n                                                <div className=\"flex items-center gap-1\">\n                                                    {isSuggested && !isSelected && (\n                                                        <Badge variant=\"outline\"\n                                                               className=\"ml-auto text-xs bg-primary/10\">\n                                                            Suggested\n                                                        </Badge>\n                                                    )}\n                                                    {isSelected && (\n                                                        <Badge variant=\"default\" className=\"ml-auto text-xs\">\n                                                            Selected\n                                                        </Badge>\n                                                    )}\n                                                </div>\n                                            </CommandItem>\n                                        );\n                                    })}\n                                </div>\n                            </CommandGroup>\n\n                            {selectedTags.length > 0 && (\n                                <div className=\"border-t p-2 bg-muted/10\">\n                                    <div className=\"flex flex-wrap gap-1 mb-1\">\n                                        <p className=\"text-xs text-muted-foreground w-full mb-1\">Selected tags:</p>\n                                        {selectedTags.map((tag, index) => (\n                                            <Badge\n                                                key={`selected-${tag.id}-${index}`}\n                                                variant=\"default\"\n                                                color={tag.color || undefined}\n                                                className={cn(\n                                                    \"text-xs cursor-pointer flex items-center gap-1\",\n                                                    !tag.color && \"bg-primary\"\n                                                )}\n                                                onClick={() => toggleTag(tag)}\n                                            >\n                                                {tag.name}\n                                                <X className=\"h-3 w-3 ml-1\"/>\n                                            </Badge>\n                                        ))}\n                                    </div>\n                                </div>\n                            )}\n                        </CommandList>\n                    </Command>\n                </PopoverContent>\n            </Popover>\n        </TooltipProvider>\n    );\n}"
  },
  {
    "path": "components/changelog/editor/TagSuggester.tsx",
    "content": "import React, { useState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Sparkles, Check, Loader2 } from 'lucide-react';\nimport { Badge } from '@/components/ui/badge';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';\nimport {\n    Popover,\n    PopoverContent,\n    PopoverTrigger\n} from '@/components/ui/popover';\n\ninterface Tag {\n    id: string;\n    name: string;\n}\n\ninterface TagSuggesterProps {\n    content: string;\n    availableTags: Tag[];\n    selectedTags: Tag[];\n    onTagsChange: (tags: Tag[]) => void;\n    apiKey?: string;\n}\n\n// AI prompt parameters\nconst MAX_CHARS_PER_SECTION = 500; // Characters per extracted section\nconst SECTIONS_TO_EXTRACT = 3;     // Number of sections to extract\nconst SECTIONS_TO_ANALYZE = 3;     // Number of sections to actually send to AI\n\nexport default function TagSuggester({\n                                         content,\n                                         availableTags,\n                                         selectedTags,\n                                         onTagsChange,\n                                         apiKey\n                                     }: TagSuggesterProps) {\n    const [isLoading, setIsLoading] = useState(false);\n    const [suggestions, setSuggestions] = useState<Tag[]>([]);\n    const [error, setError] = useState<string | null>(null);\n    const [isOpen, setIsOpen] = useState(false);\n\n    /**\n     * Extract meaningful sections from content optimized for tag detection\n     * This approach samples from beginning, middle and end of the document\n     * to get a better representation of the full content.\n     */\n    const extractContentSections = (text: string): string[] => {\n        if (!text) return [];\n\n        const cleanedText = text.trim();\n        if (cleanedText.length <= MAX_CHARS_PER_SECTION * SECTIONS_TO_EXTRACT) {\n            return [cleanedText]; // Return all content if it's short enough\n        }\n\n        const sections: string[] = [];\n\n        // Extract beginning section (always include)\n        sections.push(extractSection(cleanedText, 0, MAX_CHARS_PER_SECTION));\n\n        // If there's more content, extract middle section\n        if (cleanedText.length > MAX_CHARS_PER_SECTION * 2) {\n            const middleStart = Math.floor(cleanedText.length / 2) - (MAX_CHARS_PER_SECTION / 2);\n            sections.push(extractSection(cleanedText, middleStart, MAX_CHARS_PER_SECTION));\n        }\n\n        // If there's more content, extract ending section\n        if (cleanedText.length > MAX_CHARS_PER_SECTION * 3) {\n            const endStart = Math.max(0, cleanedText.length - MAX_CHARS_PER_SECTION);\n            sections.push(extractSection(cleanedText, endStart, MAX_CHARS_PER_SECTION));\n        }\n\n        // Extract headings if there are any (often contains important context)\n        const headingMatches = cleanedText.match(/#+\\s+.*$/gm) || [];\n        if (headingMatches.length > 0) {\n            const headings = headingMatches.slice(0, 5).join('\\n');\n            if (headings.length > 0) {\n                sections.push(`Key sections:\\n${headings}`);\n            }\n        }\n\n        return sections;\n    };\n\n    /**\n     * Extract a section of content with intelligent boundaries\n     */\n    const extractSection = (text: string, startPos: number, length: number): string => {\n        // Safety checks\n        if (!text || startPos >= text.length) return '';\n\n        // Find start at paragraph or sentence boundary if possible\n        let actualStart = startPos;\n        let actualEnd = Math.min(text.length, startPos + length);\n\n        // If not starting at beginning, find a good start boundary\n        if (startPos > 0) {\n            // Try to find paragraph start\n            const paraStart = text.lastIndexOf('\\n\\n', startPos) + 2;\n            if (paraStart > 0 && paraStart < startPos && (startPos - paraStart) < length / 2) {\n                actualStart = paraStart;\n            } else {\n                // Try to find sentence start\n                const sentStart = text.lastIndexOf('. ', startPos) + 2;\n                if (sentStart > 0 && sentStart < startPos && (startPos - sentStart) < length / 3) {\n                    actualStart = sentStart;\n                }\n            }\n        }\n\n        // Find a good end boundary\n        if (actualEnd < text.length) {\n            // Try to find paragraph end\n            const paraEnd = text.indexOf('\\n\\n', actualEnd - 20);\n            if (paraEnd > 0 && paraEnd < (actualEnd + length / 3)) {\n                actualEnd = paraEnd;\n            } else {\n                // Try to find sentence end\n                const sentEnd = text.indexOf('. ', actualEnd - 20);\n                if (sentEnd > 0 && sentEnd < (actualEnd + length / 4)) {\n                    actualEnd = sentEnd + 1; // Include the period\n                }\n            }\n        }\n\n        // If this is not the beginning, add indication\n        const prefix = actualStart > 0 ? '... ' : '';\n        // If this is not the end, add indication\n        const suffix = actualEnd < text.length ? ' ...' : '';\n\n        return prefix + text.substring(actualStart, actualEnd) + suffix;\n    };\n\n    const analyzeContent = async () => {\n        if (!content || !apiKey || !availableTags.length) {\n            setError('Cannot generate suggestions without content or available tags');\n            return;\n        }\n\n        if (content.trim().length < 20) {\n            setError('Content is too short for meaningful tag suggestions');\n            return;\n        }\n\n        setIsLoading(true);\n        setError(null);\n\n        try {\n            // Prepare prompt for the AI\n            const tagNames = availableTags.map(tag => tag.name).join(', ');\n\n            // Extract key sections from the content\n            const contentSections = extractContentSections(content);\n\n            // Limit number of sections if too many\n            const sectionsToUse = contentSections.slice(0, SECTIONS_TO_ANALYZE);\n\n            // Combine sections with section numbers for readability\n            const formattedSections = sectionsToUse.map((section, index) =>\n                `Section ${index + 1}:\\n${section}`\n            ).join('\\n\\n');\n\n            const prompt = `\nI need to categorize the following changelog content with appropriate tags.\nAvailable tags: ${tagNames}\n\nI'll provide key sections from the content below. Based on these sections, which tags (maximum 3) would be most relevant? \nOnly respond with tags from the provided list above, separated by commas.\nDo not add any explanations, just return the tag names.\n\n${formattedSections}\n      `.trim();\n\n            // Call AI API\n            const response = await fetch('https://api.secton.org/v1/chat/completions', {\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json',\n                    'Authorization': `Bearer ${apiKey}`\n                },\n                body: JSON.stringify({\n                    model: 'copilot-zero',\n                    messages: [\n                        {\n                            role: 'system',\n                            content: 'You are a skilled content tagger for a changelog system. Your job is to select the most appropriate tags for content.'\n                        },\n                        { role: 'user', content: prompt }\n                    ],\n                    temperature: 0.3,\n                    max_tokens: 30 // Further reduced since we only need tag names\n                })\n            });\n\n            if (!response.ok) {\n                throw new Error(`Failed to get tag suggestions, AI content: ${prompt}`);\n            }\n\n            const result = await response.json();\n            const suggestedTagsText = result.messages[result.messages.length - 1]?.content || '';\n\n            // Process the AI's response\n            const suggestedTagNames = suggestedTagsText\n                .split(',')\n                .map((tag: string) => tag.trim())\n                .filter(Boolean);\n\n            // Map the suggested tag names to actual tag objects\n            const validSuggestions = suggestedTagNames\n                .map((name: string) => {\n                    // Find case-insensitive match\n                    return availableTags.find(tag =>\n                        tag.name.toLowerCase() === name.toLowerCase()\n                    );\n                })\n                .filter(Boolean) as Tag[];\n\n            if (validSuggestions.length === 0) {\n                setError('Could not generate suitable tag suggestions');\n            } else {\n                setSuggestions(validSuggestions);\n                setIsOpen(true);\n            }\n        } catch (err) {\n            console.error('Error suggesting tags:', err);\n            setError(err instanceof Error ? err.message : 'Failed to analyze content');\n        } finally {\n            setIsLoading(false);\n        }\n    };\n\n    const handleSelectTag = (tag: Tag) => {\n        // Check if tag is already selected\n        const isSelected = selectedTags.some(t => t.id === tag.id);\n\n        if (isSelected) {\n            // Remove tag if already selected\n            onTagsChange(selectedTags.filter(t => t.id !== tag.id));\n        } else {\n            // Add tag if not selected\n            onTagsChange([...selectedTags, tag]);\n        }\n    };\n\n    return (\n        <Popover open={isOpen} onOpenChange={setIsOpen}>\n            <PopoverTrigger asChild>\n                <Tooltip>\n                    <TooltipTrigger asChild>\n                        <Button\n                            variant=\"outline\"\n                            size=\"sm\"\n                            className=\"h-8\"\n                            disabled={isLoading || !apiKey}\n                            onClick={!isOpen ? analyzeContent : undefined}\n                        >\n                            {isLoading ? (\n                                <Loader2 className=\"h-4 w-4 mr-1 animate-spin\" />\n                            ) : (\n                                <Sparkles className=\"h-4 w-4 mr-1\" />\n                            )}\n                            <span className=\"hidden sm:inline\">Suggest Tags</span>\n                        </Button>\n                    </TooltipTrigger>\n                    <TooltipContent>{apiKey ? 'AI-powered tag suggestions' : 'AI features not available'}</TooltipContent>\n                </Tooltip>\n            </PopoverTrigger>\n\n            <PopoverContent className=\"w-[250px] p-4\" align=\"end\">\n                <h4 className=\"text-sm font-medium mb-2\">Suggested Tags</h4>\n\n                {error ? (\n                    <p className=\"text-xs text-destructive\">{error}</p>\n                ) : (\n                    <>\n                        <div className=\"flex flex-wrap gap-2 mb-3\">\n                            <AnimatePresence>\n                                {suggestions.map(tag => {\n                                    const isSelected = selectedTags.some(t => t.id === tag.id);\n\n                                    return (\n                                        <motion.div\n                                            key={tag.id}\n                                            initial={{ opacity: 0, scale: 0.9 }}\n                                            animate={{ opacity: 1, scale: 1 }}\n                                            exit={{ opacity: 0, scale: 0.9 }}\n                                            transition={{ duration: 0.2 }}\n                                        >\n                                            <Badge\n                                                variant={isSelected ? \"default\" : \"outline\"}\n                                                className=\"cursor-pointer flex items-center gap-1\"\n                                                onClick={() => handleSelectTag(tag)}\n                                            >\n                                                {isSelected && <Check className=\"h-3 w-3\" />}\n                                                {tag.name}\n                                            </Badge>\n                                        </motion.div>\n                                    );\n                                })}\n                            </AnimatePresence>\n                        </div>\n\n                        <p className=\"text-xs text-muted-foreground\">\n                            Click on a tag to add or remove it from your selection.\n                        </p>\n                    </>\n                )}\n\n                <div className=\"mt-3 flex justify-end\">\n                    <Button\n                        variant=\"secondary\"\n                        size=\"sm\"\n                        className=\"w-full\"\n                        onClick={() => {\n                            setIsOpen(false);\n                            setSuggestions([]);\n                            setError(null);\n                        }}\n                    >\n                        Close\n                    </Button>\n                </div>\n            </PopoverContent>\n        </Popover>\n    );\n}"
  },
  {
    "path": "components/changelog/editor/VersionSelector.tsx",
    "content": "import React, { useCallback, useEffect, useMemo, useReducer, useRef } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';\nimport {\n    Command,\n    CommandEmpty,\n    CommandGroup,\n    CommandInput,\n    CommandItem,\n    CommandList,\n    CommandSeparator,\n} from \"@/components/ui/command\";\nimport {\n    ChevronRight, Check, AlertTriangle, Loader2,\n    RefreshCw, Sparkles, Tag, Calendar, Info, ChevronsUpDown,\n} from 'lucide-react';\nimport { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@/components/ui/tooltip';\nimport { Badge } from \"@/components/ui/badge\";\nimport { Separator } from '@/components/ui/separator';\nimport { cn } from '@/lib/utils';\nimport { useQuery } from '@tanstack/react-query';\nimport { motion, AnimatePresence } from 'framer-motion';\n\n// ===== Types =====\n\ntype VersionType = 'major' | 'minor' | 'patch' | 'custom';\ntype TabMode = 'semver' | 'custom';\n\ninterface ProcessedVersion {\n    value: string;\n    type: VersionType;\n    isStandard: boolean;\n    isConflict: boolean;\n    isCurrent: boolean;\n}\n\ninterface ResolvedTemplate {\n    value: string;\n    label: string;\n    isConflict: boolean;\n    isCurrent: boolean;\n}\n\ninterface TimezoneResponse {\n    timezone: string;\n    customDateTemplates?: { format: string; label: string }[] | null;\n}\n\ninterface VersionSelectorProps {\n    version: string;\n    onVersionChange: (version: string) => void;\n    projectId: string;\n    entryId?: string;\n    onConflictDetected?: (hasConflict: boolean) => void;\n    disabled?: boolean;\n}\n\ninterface State {\n    input: string;\n    tab: TabMode;\n    isOpen: boolean;\n    showPrevious: boolean;\n    hasConflict: boolean;\n    isValidating: boolean;\n}\n\ntype Action =\n    | Partial<State>\n    | { type: 'reset' }\n    | { type: 'toggle-previous' };\n\n// ===== Constants =====\n\nconst TYPE_CONFIG: Record<VersionType, { dot: string; label: string; badge: string }> = {\n    patch:  { dot: 'bg-blue-500',    label: 'Patch',  badge: 'text-blue-600 dark:text-blue-400' },\n    minor:  { dot: 'bg-emerald-500', label: 'Minor',  badge: 'text-emerald-600 dark:text-emerald-400' },\n    major:  { dot: 'bg-orange-500',  label: 'Major',  badge: 'text-orange-600 dark:text-orange-400' },\n    custom: { dot: 'bg-muted-foreground', label: 'Custom', badge: 'text-muted-foreground' },\n};\n\nconst BUILT_IN_TEMPLATES: { format: string; label: string }[] = [\n    { format: 'v{YYYY}.{MM}.{DD}', label: 'Date (dotted)' },\n    { format: 'v{YYYY}.{MM}.{DD}.1', label: 'Date (rev)' },\n    { format: 'v{YYYY}{MM}{DD}', label: 'Date (compact)' },\n];\n\n// ===== Helpers =====\n\nfunction reducer(state: State, action: Action): State {\n    if (typeof action === 'object' && 'type' in action) {\n        if (action.type === 'reset') return { ...state, hasConflict: false, isValidating: false };\n        if (action.type === 'toggle-previous') return { ...state, showPrevious: !state.showPrevious };\n        return state;\n    }\n    return { ...state, ...action };\n}\n\nfunction isSemVer(v: string): boolean {\n    if (!v) return false;\n    const n = v.startsWith('v') ? v.substring(1) : v;\n    const p = n.split('.');\n    return p.length === 3 && p.every(x => /^\\d+$/.test(x));\n}\n\nfunction stripV(v: string): string {\n    return v.startsWith('v') ? v.substring(1) : v;\n}\n\nfunction addV(v: string): string {\n    if (!v) return '';\n    const t = v.trim();\n    return t.startsWith('v') ? t : `v${t}`;\n}\n\nfunction getType(v: string): VersionType {\n    if (!isSemVer(v)) return 'custom';\n    const s = stripV(v);\n    if (s.endsWith('.0.0')) return 'major';\n    if (/\\.\\d+\\.0$/.test(s)) return 'minor';\n    return 'patch';\n}\n\nfunction parseSemVer(v: string): [number, number, number] | null {\n    const parts = stripV(v).split('.').map(Number);\n    if (parts.length !== 3 || parts.some(isNaN)) return null;\n    return parts as [number, number, number];\n}\n\nfunction resolveTemplate(\n    format: string,\n    tz: string,\n    latestParts: [number, number, number] | null,\n): string {\n    const now = new Date();\n\n    const dp = new Intl.DateTimeFormat('en-CA', {\n        timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit',\n    }).formatToParts(now);\n    const tp = new Intl.DateTimeFormat('en-GB', {\n        timeZone: tz, hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,\n    }).formatToParts(now);\n\n    const Y = dp.find(p => p.type === 'year')?.value ?? '2026';\n    const M = dp.find(p => p.type === 'month')?.value ?? '01';\n    const D = dp.find(p => p.type === 'day')?.value ?? '01';\n    const h = tp.find(p => p.type === 'hour')?.value ?? '00';\n    const m = tp.find(p => p.type === 'minute')?.value ?? '00';\n    const s = tp.find(p => p.type === 'second')?.value ?? '00';\n\n    const [maj, min, pat] = latestParts ?? [0, 0, 0];\n\n    return format\n        .replace(/\\{YYYY}/g, Y)\n        .replace(/\\{YY}/g, Y.slice(-2))\n        .replace(/\\{MM}/g, M)\n        .replace(/\\{DD}/g, D)\n        .replace(/\\{hh}/g, h)\n        .replace(/\\{mm}/g, m)\n        .replace(/\\{ss}/g, s)\n        .replace(/\\{MAJOR}/g, String(maj))\n        .replace(/\\{MINOR}/g, String(min))\n        .replace(/\\{PATCH}/g, String(pat))\n        .replace(/\\{VERSION}/g, `${maj}.${min}.${pat}`)\n        .replace(/\\{NEXT_PATCH}/g, String(pat + 1))\n        .replace(/\\{NEXT_MINOR}/g, String(min + 1))\n        .replace(/\\{NEXT_MAJOR}/g, String(maj + 1));\n}\n\n// ===== Component =====\n\nconst VersionSelector: React.FC<VersionSelectorProps> = ({\n    version,\n    onVersionChange,\n    projectId,\n    entryId,\n    onConflictDetected,\n    disabled = false,\n}) => {\n    const inputRef = useRef<HTMLInputElement>(null);\n    const debounceRef = useRef('');\n\n    const [state, dispatch] = useReducer(reducer, {\n        input: version || '',\n        tab: 'semver' as TabMode,\n        isOpen: false,\n        showPrevious: false,\n        hasConflict: false,\n        isValidating: false,\n    });\n\n    const { input, tab, isOpen, showPrevious, hasConflict, isValidating } = state;\n\n    // ----- Data fetching -----\n\n    const { data: tzData } = useQuery<TimezoneResponse>({\n        queryKey: ['system-timezone'],\n        queryFn: async () => {\n            const r = await fetch('/api/config/timezone');\n            if (!r.ok) return { timezone: 'UTC' };\n            return r.json();\n        },\n        staleTime: 300_000,\n    });\n\n    const tz = tzData?.timezone ?? 'UTC';\n    const adminTemplates = tzData?.customDateTemplates;\n    const hasAdminTemplates = Array.isArray(adminTemplates) && adminTemplates.length > 0;\n\n    const {\n        data: versionData,\n        isLoading: versionsLoading,\n        refetch: refetchVersions,\n    } = useQuery<{ versions: string[] }>({\n        queryKey: ['project-versions', projectId],\n        queryFn: async () => {\n            const r = await fetch(`/api/projects/${projectId}/versions`);\n            if (!r.ok) throw new Error('Failed');\n            return r.json();\n        },\n        staleTime: 60_000,\n    });\n\n    const existingSet = useMemo(\n        () => new Set(versionData?.versions || []),\n        [versionData?.versions],\n    );\n\n    // ----- Debounced conflict check -----\n\n    useEffect(() => {\n        const t = setTimeout(() => {\n            debounceRef.current = input;\n            if (input.trim()) dispatch({ isValidating: true });\n        }, 300);\n        return () => clearTimeout(t);\n    }, [input]);\n\n    const { data: conflictResult } = useQuery({\n        queryKey: ['version-conflict', projectId, debounceRef.current, entryId],\n        queryFn: () => {\n            const v = debounceRef.current;\n            if (!v.trim()) return { hasConflict: false };\n            const fv = addV(v);\n            if (fv === addV(version)) return { hasConflict: false };\n            return { hasConflict: existingSet.has(fv) || existingSet.has(stripV(fv)) };\n        },\n        enabled: Boolean(debounceRef.current && versionData),\n        staleTime: 0,\n    });\n\n    useEffect(() => {\n        if (conflictResult) {\n            dispatch({ hasConflict: conflictResult.hasConflict, isValidating: false });\n            onConflictDetected?.(conflictResult.hasConflict);\n        }\n    }, [conflictResult, onConflictDetected]);\n\n    // ----- Focus + reset -----\n\n    useEffect(() => {\n        if (isOpen && inputRef.current) {\n            const t = setTimeout(() => inputRef.current?.focus(), 80);\n            return () => clearTimeout(t);\n        }\n    }, [isOpen, tab]);\n\n    useEffect(() => {\n        if (isOpen) dispatch({ type: 'reset' });\n    }, [isOpen, version]);\n\n    useEffect(() => {\n        if (version && !isSemVer(version)) dispatch({ tab: 'custom' });\n    }, [version]);\n\n    // ----- Processed versions -----\n\n    const processed = useMemo((): ProcessedVersion[] => {\n        const cv = addV(version);\n        return (versionData?.versions || []).map(v => ({\n            value: v,\n            type: getType(v),\n            isStandard: isSemVer(v),\n            isConflict: false,\n            isCurrent: v === cv,\n        }));\n    }, [versionData?.versions, version]);\n\n    const semverVersions = useMemo(() => processed.filter(v => v.isStandard), [processed]);\n    const customVersions = useMemo(() => processed.filter(v => !v.isStandard), [processed]);\n\n    const latestParts = useMemo((): [number, number, number] | null => {\n        if (!semverVersions.length) return null;\n        return parseSemVer(semverVersions[0].value);\n    }, [semverVersions]);\n\n    // ----- Suggestions -----\n\n    const suggestions = useMemo((): ProcessedVersion[] => {\n        if (!latestParts) {\n            return [{ value: 'v1.0.0', type: 'major', isStandard: true, isConflict: false, isCurrent: false }];\n        }\n\n        const [maj, min, pat] = latestParts;\n        const cv = addV(version);\n\n        const next = (bM: number, bm: number, bp: number, type: VersionType): ProcessedVersion => {\n            let cM = bM, cm = bm, cp = bp;\n            for (let i = 0; i < 10; i++) {\n                const candidate = `v${cM}.${cm}.${cp}`;\n                const isCurr = candidate === cv;\n                if (!isCurr && !existingSet.has(candidate) && !existingSet.has(stripV(candidate))) {\n                    return { value: candidate, type, isStandard: true, isConflict: false, isCurrent: false };\n                }\n                if (isCurr) return { value: candidate, type, isStandard: true, isConflict: false, isCurrent: true };\n                if (type === 'patch') cp++;\n                else if (type === 'minor') { cm++; cp = 0; }\n                else { cM++; cm = 0; cp = 0; }\n            }\n            return { value: `v${cM}.${cm}.${cp}`, type, isStandard: true, isConflict: true, isCurrent: false };\n        };\n\n        return [\n            next(maj, min, pat + 1, 'patch'),\n            next(maj, min + 1, 0, 'minor'),\n            next(maj + 1, 0, 0, 'major'),\n        ];\n    }, [latestParts, existingSet, version]);\n\n    // ----- Templates -----\n\n    const templates = useMemo((): ResolvedTemplate[] => {\n        const source = hasAdminTemplates ? adminTemplates! : BUILT_IN_TEMPLATES;\n        const cv = addV(version);\n\n        return source.map(t => {\n            const resolved = resolveTemplate(t.format, tz, latestParts);\n            const isCurr = resolved === cv;\n            const conflict = !isCurr && (existingSet.has(resolved) || existingSet.has(stripV(resolved)));\n            return { value: resolved, label: t.label, isConflict: conflict, isCurrent: isCurr };\n        });\n    }, [hasAdminTemplates, adminTemplates, tz, latestParts, existingSet, version]);\n\n    // ----- Handlers -----\n\n    const selectVersion = useCallback((v: string) => {\n        const fv = addV(v);\n        const cv = addV(version);\n        if (fv === cv) {\n            onVersionChange(fv);\n            dispatch({ isOpen: false, hasConflict: false });\n            return;\n        }\n        if (existingSet.has(fv) || existingSet.has(stripV(fv))) {\n            dispatch({ hasConflict: true });\n            return;\n        }\n        onVersionChange(fv);\n        dispatch({ isOpen: false, hasConflict: false });\n        if (document.activeElement instanceof HTMLElement) document.activeElement.blur();\n    }, [onVersionChange, existingSet, version]);\n\n    const submitInput = useCallback(() => {\n        if (input.trim() && !hasConflict) selectVersion(input);\n    }, [input, hasConflict, selectVersion]);\n\n    const onKeyDown = useCallback((e: React.KeyboardEvent) => {\n        if (e.key === 'Enter' && input.trim() && !hasConflict) {\n            e.preventDefault();\n            selectVersion(input);\n        }\n    }, [input, hasConflict, selectVersion]);\n\n    // ----- Derived -----\n\n    const vt = version ? getType(version) : null;\n    const vtConfig = vt ? TYPE_CONFIG[vt] : null;\n    const previousVersions = tab === 'semver' ? semverVersions : customVersions;\n\n    // Check if user input matches any suggestion/template (to avoid showing redundant \"use as\" row)\n    const inputMatchesSuggestion = useMemo(() => {\n        if (!input.trim()) return true;\n        const fv = addV(input);\n        if (tab === 'semver') return suggestions.some(s => s.value === fv);\n        return templates.some(t => t.value === fv);\n    }, [input, tab, suggestions, templates]);\n\n    return (\n        <TooltipProvider>\n            <Popover open={isOpen} onOpenChange={o => !disabled && dispatch({ isOpen: o })}>\n                <PopoverTrigger asChild>\n                    <Button variant=\"outline\" disabled={disabled} className=\"h-8 border-dashed gap-1.5 min-w-0 max-w-full overflow-hidden\">\n                        <Tag className=\"h-4 w-4 shrink-0\" />\n                        {version ? (\n                            <>\n                                <span className=\"font-mono text-sm max-w-[120px] truncate\">\n                                    {version}\n                                </span>\n                                <Separator orientation=\"vertical\" className=\"mx-0.5 h-4\" />\n                                <Badge variant=\"secondary\" className=\"rounded-sm px-1 font-normal\">\n                                    <span className={cn(\"h-1.5 w-1.5 rounded-full mr-1 inline-block\", vtConfig?.dot)} />\n                                    {vtConfig?.label}\n                                </Badge>\n                            </>\n                        ) : (\n                            <span className=\"text-muted-foreground\">Set version</span>\n                        )}\n                        <ChevronsUpDown className=\"h-3.5 w-3.5 text-muted-foreground/50 ml-0.5 shrink-0\" />\n                    </Button>\n                </PopoverTrigger>\n\n                <PopoverContent className=\"w-[350px] p-0\" align=\"start\">\n                    {/* Header */}\n                    <div className=\"flex items-center justify-between px-3 py-2 border-b bg-muted/30\">\n                        <div className=\"flex items-center gap-2\">\n                            <Tag className=\"h-3.5 w-3.5 text-muted-foreground\" />\n                            <span className=\"text-sm font-medium\">Version</span>\n                        </div>\n\n                        <div className=\"flex items-center gap-1.5\">\n                            {/* Tab toggle */}\n                            <div className=\"flex items-center rounded-md border bg-background p-0.5\">\n                                <button\n                                    onClick={() => dispatch({ tab: 'semver' })}\n                                    className={cn(\n                                        \"px-2 py-0.5 rounded-[3px] text-[11px] font-medium transition-all\",\n                                        tab === 'semver'\n                                            ? \"bg-primary text-primary-foreground shadow-sm\"\n                                            : \"text-muted-foreground hover:text-foreground\"\n                                    )}\n                                >\n                                    SemVer\n                                </button>\n                                <button\n                                    onClick={() => dispatch({ tab: 'custom' })}\n                                    className={cn(\n                                        \"px-2 py-0.5 rounded-[3px] text-[11px] font-medium transition-all\",\n                                        tab === 'custom'\n                                            ? \"bg-primary text-primary-foreground shadow-sm\"\n                                            : \"text-muted-foreground hover:text-foreground\"\n                                    )}\n                                >\n                                    Custom\n                                </button>\n                            </div>\n\n                            <Tooltip>\n                                <TooltipTrigger asChild>\n                                    <Button\n                                        variant=\"ghost\"\n                                        size=\"sm\"\n                                        className=\"h-6 w-6 p-0\"\n                                        onClick={() => refetchVersions()}\n                                        disabled={versionsLoading}\n                                    >\n                                        <RefreshCw className={cn(\"h-3 w-3\", versionsLoading && \"animate-spin\")} />\n                                    </Button>\n                                </TooltipTrigger>\n                                <TooltipContent side=\"bottom\">Refresh versions</TooltipContent>\n                            </Tooltip>\n                        </div>\n                    </div>\n\n                    {/* Conflict banner */}\n                    <AnimatePresence>\n                        {hasConflict && (\n                            <motion.div\n                                initial={{ opacity: 0, height: 0 }}\n                                animate={{ opacity: 1, height: 'auto' }}\n                                exit={{ opacity: 0, height: 0 }}\n                            >\n                                <div className=\"flex items-center gap-1.5 px-3 py-1.5 bg-destructive/10 text-destructive text-xs border-b\">\n                                    <AlertTriangle className=\"h-3.5 w-3.5 shrink-0\" />\n                                    <span className=\"flex-1\">This version already exists</span>\n                                    <button\n                                        onClick={() => dispatch({ hasConflict: false, input: '' })}\n                                        className=\"underline underline-offset-2 hover:no-underline text-[11px]\"\n                                    >\n                                        Clear\n                                    </button>\n                                </div>\n                            </motion.div>\n                        )}\n                    </AnimatePresence>\n\n                    <Command shouldFilter={false}>\n                        <CommandInput\n                            placeholder={tab === 'semver' ? \"Type a version...\" : \"Type any name...\"}\n                            value={input}\n                            onValueChange={v => dispatch({ input: v, isValidating: Boolean(v.trim()) })}\n                            onKeyDown={onKeyDown}\n                            ref={inputRef}\n                        />\n\n                        <CommandList>\n                            <CommandEmpty>\n                                {versionsLoading ? (\n                                    <div className=\"py-6 text-center text-xs text-muted-foreground\">\n                                        <span className=\"flex items-center justify-center gap-1.5\">\n                                            <Loader2 className=\"h-3.5 w-3.5 animate-spin\" /> Loading...\n                                        </span>\n                                    </div>\n                                ) : input.trim() ? (\n                                    <div className=\"py-3 px-4 text-center space-y-1.5\">\n                                        {hasConflict ? (\n                                            <p className=\"text-xs text-destructive flex items-center justify-center gap-1\">\n                                                <AlertTriangle className=\"h-3 w-3\" /> Already exists\n                                            </p>\n                                        ) : (\n                                            <p className=\"text-xs text-muted-foreground\">\n                                                Press <kbd className=\"px-1 py-0.5 rounded border bg-muted text-[10px] font-mono\">Enter</kbd> to use <code className=\"font-mono text-foreground\">{addV(input)}</code>\n                                            </p>\n                                        )}\n                                    </div>\n                                ) : (\n                                    <div className=\"py-6 text-center text-xs text-muted-foreground\">\n                                        {tab === 'semver' ? 'No versions yet' : 'Type a name or pick a template'}\n                                    </div>\n                                )}\n                            </CommandEmpty>\n\n                            {/* ===== SemVer Tab ===== */}\n                            {tab === 'semver' && (\n                                <>\n                                    {/* Use-as row for typed input that doesn't match suggestions */}\n                                    {input.trim() && !inputMatchesSuggestion && (\n                                        <CommandGroup heading=\"Use as typed\">\n                                            <CommandItem\n                                                value={`typed-${input}`}\n                                                onSelect={submitInput}\n                                                disabled={hasConflict || isValidating}\n                                                className={cn(hasConflict && \"opacity-40\")}\n                                            >\n                                                <Sparkles className=\"h-3.5 w-3.5 mr-2 text-primary shrink-0\" />\n                                                <span className=\"font-mono flex-1\">{addV(input)}</span>\n                                                {isValidating ? (\n                                                    <Loader2 className=\"h-3.5 w-3.5 animate-spin text-muted-foreground\" />\n                                                ) : hasConflict ? (\n                                                    <AlertTriangle className=\"h-3.5 w-3.5 text-destructive\" />\n                                                ) : (\n                                                    <span className=\"text-[10px] text-muted-foreground\">Enter</span>\n                                                )}\n                                            </CommandItem>\n                                        </CommandGroup>\n                                    )}\n\n                                    <CommandGroup heading=\"Recommended\">\n                                        {suggestions.map(s => {\n                                            const c = TYPE_CONFIG[s.type];\n                                            return (\n                                                <CommandItem\n                                                    key={s.value}\n                                                    value={s.value}\n                                                    onSelect={() => !s.isConflict && selectVersion(s.value)}\n                                                    disabled={s.isConflict}\n                                                    className={cn(\n                                                        s.isCurrent && \"bg-primary/5\",\n                                                        s.isConflict && \"opacity-40\",\n                                                    )}\n                                                >\n                                                    <span className={cn(\"h-2 w-2 rounded-full mr-2 shrink-0\", c.dot)} />\n                                                    <span className=\"font-mono flex-1\">{s.value}</span>\n                                                    <Badge variant=\"outline\" className={cn(\"text-[10px] px-1.5 h-5 font-normal\", c.badge)}>\n                                                        {c.label}\n                                                    </Badge>\n                                                    {s.isCurrent && <Check className=\"h-3.5 w-3.5 text-primary ml-1\" />}\n                                                    {s.isConflict && <AlertTriangle className=\"h-3.5 w-3.5 text-destructive ml-1\" />}\n                                                </CommandItem>\n                                            );\n                                        })}\n                                    </CommandGroup>\n\n                                    <div className=\"px-3 py-1.5\">\n                                        <p className=\"text-[10px] text-muted-foreground/70 flex items-center gap-1\">\n                                            <Info className=\"h-2.5 w-2.5 shrink-0\" />\n                                            <code className=\"font-mono bg-muted/50 px-0.5 rounded\">vMAJOR.MINOR.PATCH</code>\n                                        </p>\n                                    </div>\n                                </>\n                            )}\n\n                            {/* ===== Custom Tab ===== */}\n                            {tab === 'custom' && (\n                                <>\n                                    {/* Use custom name */}\n                                    {input.trim() && !inputMatchesSuggestion && (\n                                        <CommandGroup heading=\"Custom Name\">\n                                            <CommandItem\n                                                value={`custom-${input}`}\n                                                onSelect={submitInput}\n                                                disabled={hasConflict || isValidating}\n                                                className={cn(hasConflict && \"opacity-40\")}\n                                            >\n                                                <Sparkles className=\"h-3.5 w-3.5 mr-2 text-primary shrink-0\" />\n                                                <span className=\"font-mono flex-1\">{addV(input)}</span>\n                                                {isValidating ? (\n                                                    <Loader2 className=\"h-3.5 w-3.5 animate-spin text-muted-foreground\" />\n                                                ) : hasConflict ? (\n                                                    <AlertTriangle className=\"h-3.5 w-3.5 text-destructive\" />\n                                                ) : (\n                                                    <span className=\"text-[10px] text-muted-foreground\">Enter</span>\n                                                )}\n                                            </CommandItem>\n                                        </CommandGroup>\n                                    )}\n\n                                    <CommandGroup heading={hasAdminTemplates ? 'Templates' : 'Date Formats'}>\n                                        {templates.map((t, i) => (\n                                            <CommandItem\n                                                key={i}\n                                                value={`tpl-${i}-${t.value}`}\n                                                onSelect={() => !t.isConflict && selectVersion(t.value)}\n                                                disabled={t.isConflict}\n                                                className={cn(\n                                                    t.isCurrent && \"bg-primary/5\",\n                                                    t.isConflict && \"opacity-40\",\n                                                )}\n                                            >\n                                                <Calendar className=\"h-3.5 w-3.5 mr-2 text-muted-foreground shrink-0\" />\n                                                <span className=\"font-mono flex-1\">{t.value}</span>\n                                                <span className=\"text-[11px] text-muted-foreground ml-2\">{t.label}</span>\n                                                {t.isCurrent && <Check className=\"h-3.5 w-3.5 text-primary ml-1\" />}\n                                                {t.isConflict && <AlertTriangle className=\"h-3.5 w-3.5 text-destructive ml-1\" />}\n                                            </CommandItem>\n                                        ))}\n                                    </CommandGroup>\n\n                                    <div className=\"px-3 py-1.5\">\n                                        <p className=\"text-[10px] text-muted-foreground/70 flex items-center gap-1\">\n                                            <Info className=\"h-2.5 w-2.5 shrink-0\" />\n                                            Type anything: <code className=\"font-mono bg-muted/50 px-0.5 rounded\">beta-1</code>,{' '}\n                                            <code className=\"font-mono bg-muted/50 px-0.5 rounded\">rc1</code>,{' '}\n                                            <code className=\"font-mono bg-muted/50 px-0.5 rounded\">nightly</code>\n                                        </p>\n                                    </div>\n                                </>\n                            )}\n\n                            {/* ===== Previous versions ===== */}\n                            {previousVersions.length > 0 && (\n                                <>\n                                    <CommandSeparator />\n                                    <button\n                                        onClick={() => dispatch({ type: 'toggle-previous' })}\n                                        className=\"flex items-center gap-1.5 w-full px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors\"\n                                    >\n                                        <motion.div animate={{ rotate: showPrevious ? 90 : 0 }} transition={{ duration: 0.15 }}>\n                                            <ChevronRight className=\"h-3 w-3\" />\n                                        </motion.div>\n                                        Previous {tab === 'semver' ? 'versions' : 'custom versions'}\n                                        <Badge variant=\"secondary\" className=\"ml-auto h-4 text-[10px] px-1.5 rounded-sm font-mono\">\n                                            {previousVersions.length}\n                                        </Badge>\n                                    </button>\n\n                                    <AnimatePresence>\n                                        {showPrevious && (\n                                            <motion.div\n                                                initial={{ height: 0, opacity: 0 }}\n                                                animate={{ height: 'auto', opacity: 1 }}\n                                                exit={{ height: 0, opacity: 0 }}\n                                                transition={{ duration: 0.15 }}\n                                            >\n                                                <CommandGroup>\n                                                    <div className=\"max-h-[150px] overflow-y-auto\">\n                                                        {previousVersions.map(v => {\n                                                            const c = TYPE_CONFIG[v.type];\n                                                            return (\n                                                                <CommandItem\n                                                                    key={v.value}\n                                                                    value={v.value}\n                                                                    onSelect={() => selectVersion(v.value)}\n                                                                    className={cn(\n                                                                        \"text-muted-foreground hover:text-foreground\",\n                                                                        v.isCurrent && \"bg-primary/5 text-foreground\",\n                                                                    )}\n                                                                >\n                                                                    <span className={cn(\"h-1.5 w-1.5 rounded-full mr-2 shrink-0\", c.dot)} />\n                                                                    <span className=\"font-mono flex-1 text-[13px]\">{v.value}</span>\n                                                                    {v.isCurrent && <Check className=\"h-3.5 w-3.5 text-primary\" />}\n                                                                </CommandItem>\n                                                            );\n                                                        })}\n                                                    </div>\n                                                </CommandGroup>\n                                            </motion.div>\n                                        )}\n                                    </AnimatePresence>\n                                </>\n                            )}\n                        </CommandList>\n                    </Command>\n                </PopoverContent>\n            </Popover>\n        </TooltipProvider>\n    );\n};\n\nexport default VersionSelector;\n"
  },
  {
    "path": "components/changelog/editor/scheduler/ScheduleEntryDialog.tsx",
    "content": "\"use client\";\n\nimport React, {useState} from 'react';\nimport {z} from 'zod';\nimport {useForm} from 'react-hook-form';\nimport {zodResolver} from '@hookform/resolvers/zod';\nimport {format, addDays, addHours, isAfter} from 'date-fns';\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogHeader,\n    DialogTitle,\n    DialogTrigger,\n} from '@/components/ui/dialog';\nimport {\n    Form,\n    FormControl,\n    FormDescription,\n    FormField,\n    FormItem,\n    FormLabel,\n    FormMessage,\n} from '@/components/ui/form';\nimport {Button} from '@/components/ui/button';\nimport {Input} from '@/components/ui/input';\nimport {Badge} from '@/components/ui/badge';\nimport {Alert, AlertDescription} from '@/components/ui/alert';\nimport {Switch} from '@/components/ui/switch';\nimport {\n    Calendar,\n    Clock,\n    AlertCircle,\n    CheckCircle,\n    Mail,\n} from 'lucide-react';\nimport {useToast} from '@/hooks/use-toast';\n\nconst scheduleSchema = z.object({\n    scheduledAt: z.date().refine(\n        (date) => isAfter(date, new Date()),\n        \"Schedule time must be in the future\"\n    ),\n    sendEmailNotification: z.boolean().default(false),\n});\n\ntype ScheduleFormData = z.infer<typeof scheduleSchema>;\n\ninterface ScheduleEntryDialogProps {\n    entryId: string;\n    projectId: string;\n    entryTitle: string;\n    isScheduled: boolean;\n    scheduledAt?: string | null;\n    isPublished: boolean;\n    projectRequiresApproval: boolean;\n    projectHasEmailConfig: boolean;\n    userRole: 'ADMIN' | 'STAFF' | 'VIEWER';\n    onScheduleChange?: () => void;\n}\n\nexport const ScheduleEntryDialog: React.FC<ScheduleEntryDialogProps> = ({\n                                                                            entryId,\n                                                                            projectId,\n                                                                            entryTitle,\n                                                                            isScheduled,\n                                                                            scheduledAt,\n                                                                            isPublished,\n                                                                            projectRequiresApproval,\n                                                                            projectHasEmailConfig,\n                                                                            userRole,\n                                                                            onScheduleChange,\n                                                                        }) => {\n    const [open, setOpen] = useState(false);\n    const [isLoading, setIsLoading] = useState(false);\n    const {toast} = useToast();\n\n    const form = useForm<ScheduleFormData>({\n        resolver: zodResolver(scheduleSchema),\n        defaultValues: {\n            scheduledAt: scheduledAt ? new Date(scheduledAt) : addHours(new Date(), 1),\n            sendEmailNotification: false,\n        },\n    });\n\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    const canSchedule = userRole === 'ADMIN' ||\n        (userRole === 'STAFF' && !projectRequiresApproval);\n    const needsApproval = userRole === 'STAFF' && projectRequiresApproval;\n\n    const handleSchedule = async (data: ScheduleFormData) => {\n        if (isPublished) {\n            toast({\n                title: \"Cannot schedule\",\n                description: \"This entry is already published.\",\n                variant: \"destructive\",\n            });\n            return;\n        }\n\n        setIsLoading(true);\n\n        try {\n            if (needsApproval) {\n                const response = await fetch(\n                    `/api/projects/${projectId}/changelog/${entryId}/schedule/approval`,\n                    {\n                        method: 'POST',\n                        headers: {'Content-Type': 'application/json'},\n                        body: JSON.stringify({\n                            action: 'request_approval',\n                            scheduledAt: data.scheduledAt.toISOString(),\n                        }),\n                    }\n                );\n\n                if (!response.ok) {\n                    const error = await response.json();\n                    throw new Error(error.error || 'Failed to request approval');\n                }\n\n                toast({\n                    title: \"Approval requested\",\n                    description: \"Your schedule request has been sent to administrators.\",\n                });\n            } else {\n                const response = await fetch(\n                    `/api/projects/${projectId}/changelog/${entryId}/schedule`,\n                    {\n                        method: 'POST',\n                        headers: {'Content-Type': 'application/json'},\n                        body: JSON.stringify({\n                            action: 'schedule',\n                            scheduledAt: data.scheduledAt.toISOString(),\n                        }),\n                    }\n                );\n\n                if (!response.ok) {\n                    const error = await response.json();\n                    throw new Error(error.error || 'Failed to schedule entry');\n                }\n\n                if (data.sendEmailNotification && projectHasEmailConfig) {\n                    try {\n                        await fetch(\n                            `/api/projects/${projectId}/integrations/email/send`,\n                            {\n                                method: 'POST',\n                                headers: {'Content-Type': 'application/json'},\n                                body: JSON.stringify({\n                                    subject: `Scheduled: ${entryTitle}`,\n                                    changelogEntryId: entryId,\n                                    subscriptionTypes: ['ALL_UPDATES'],\n                                }),\n                            }\n                        );\n                    } catch (emailError) {\n                        console.error('Failed to send notification email:', emailError);\n                    }\n                }\n\n                toast({\n                    title: isScheduled ? \"Schedule updated\" : \"Entry scheduled\",\n                    description: isScheduled\n                        ? `Entry rescheduled for ${format(data.scheduledAt, 'PPP p')}.`\n                        : `Entry will be published on ${format(data.scheduledAt, 'PPP p')}.`,\n                });\n            }\n\n            setOpen(false);\n            onScheduleChange?.();\n        } catch (error) {\n            console.error('Scheduling error:', error);\n            toast({\n                title: \"Scheduling failed\",\n                description: error instanceof Error ? error.message : \"Something went wrong\",\n                variant: \"destructive\",\n            });\n        } finally {\n            setIsLoading(false);\n        }\n    };\n\n    const handleUnschedule = async () => {\n        setIsLoading(true);\n\n        try {\n            const response = await fetch(\n                `/api/projects/${projectId}/changelog/${entryId}/schedule`,\n                {\n                    method: 'POST',\n                    headers: {'Content-Type': 'application/json'},\n                    body: JSON.stringify({action: 'unschedule'}),\n                }\n            );\n\n            if (!response.ok) {\n                const error = await response.json();\n                throw new Error(error.error || 'Failed to unschedule entry');\n            }\n\n            toast({\n                title: \"Entry unscheduled\",\n                description: \"The scheduled publication has been cancelled.\",\n            });\n\n            setOpen(false);\n            onScheduleChange?.();\n        } catch (error) {\n            console.error('Unscheduling error:', error);\n            toast({\n                title: \"Unscheduling failed\",\n                description: error instanceof Error ? error.message : \"Something went wrong\",\n                variant: \"destructive\",\n            });\n        } finally {\n            setIsLoading(false);\n        }\n    };\n\n    const getQuickOptions = () => [\n        {\n            label: 'In 1 hour',\n            date: addHours(new Date(), 1),\n        },\n        {\n            label: 'Tomorrow 9 AM',\n            date: (() => {\n                const tomorrow = addDays(new Date(), 1);\n                tomorrow.setHours(9, 0, 0, 0);\n                return tomorrow;\n            })(),\n        },\n        {\n            label: 'Next Monday 9 AM',\n            date: (() => {\n                const nextMonday = new Date();\n                const daysUntilMonday = (8 - nextMonday.getDay()) % 7 || 7;\n                nextMonday.setDate(nextMonday.getDate() + daysUntilMonday);\n                nextMonday.setHours(9, 0, 0, 0);\n                return nextMonday;\n            })(),\n        },\n    ];\n\n    return (\n        <Dialog open={open} onOpenChange={setOpen}>\n            <DialogTrigger asChild>\n                <Button\n                    variant={isScheduled ? \"secondary\" : \"outline\"}\n                    size=\"sm\"\n                    disabled={isPublished}\n                >\n                    <Calendar className=\"h-4 w-4 mr-2\"/>\n                    {isScheduled ? 'Edit Schedule' : 'Schedule'}\n                </Button>\n            </DialogTrigger>\n\n            <DialogContent className=\"sm:max-w-[500px]\">\n                <DialogHeader>\n                    <DialogTitle className=\"flex items-center gap-2\">\n                        <Calendar className=\"h-5 w-5 text-primary\"/>\n                        Schedule Publication\n                    </DialogTitle>\n                    <DialogDescription>\n                        {needsApproval\n                            ? \"Request approval to schedule this changelog entry.\"\n                            : \"Schedule this changelog entry for automatic publication.\"\n                        }\n                    </DialogDescription>\n                </DialogHeader>\n\n                <div className=\"space-y-6\">\n                    {/* Entry Info */}\n                    <div className=\"p-3 bg-muted/50 rounded-lg\">\n                        <div className=\"flex items-center justify-between\">\n                            <span className=\"text-sm font-medium\">{entryTitle}</span>\n                            {isScheduled && scheduledAt && (\n                                <Badge variant=\"secondary\">\n                                    {format(new Date(scheduledAt), 'MMM d, h:mm a')}\n                                </Badge>\n                            )}\n                        </div>\n                    </div>\n\n                    {/* Approval Warning */}\n                    {needsApproval && (\n                        <Alert variant=\"warning\">\n                            <AlertDescription className=\"text-amber-800\">\n                                This project requires administrator approval for scheduling.\n                            </AlertDescription>\n                        </Alert>\n                    )}\n\n                    <Form {...form}>\n                        <form onSubmit={form.handleSubmit(handleSchedule)} className=\"space-y-4\">\n                            {/* Quick Options - show for both new and existing schedules */}\n                            <div className=\"space-y-2\">\n                <span className=\"text-sm font-medium\">\n                  {isScheduled ? \"Reschedule Options\" : \"Quick Schedule\"}\n                </span>\n                                <div className=\"flex gap-2 flex-wrap\">\n                                    {getQuickOptions().map((option, index) => (\n                                        <Button\n                                            key={index}\n                                            type=\"button\"\n                                            variant=\"outline\"\n                                            size=\"sm\"\n                                            onClick={() => form.setValue('scheduledAt', option.date)}\n                                        >\n                                            {option.label}\n                                        </Button>\n                                    ))}\n                                </div>\n                            </div>\n\n                            {/* Date/Time Input */}\n                            <FormField\n                                control={form.control}\n                                name=\"scheduledAt\"\n                                render={({field}) => (\n                                    <FormItem>\n                                        <FormLabel className=\"flex items-center gap-2\">\n                                            <Clock className=\"h-4 w-4\"/>\n                                            Schedule Date & Time\n                                        </FormLabel>\n                                        <FormControl>\n                                            <Input\n                                                type=\"datetime-local\"\n                                                value={field.value ? format(field.value, \"yyyy-MM-dd'T'HH:mm\") : ''}\n                                                onChange={(e) => {\n                                                    const date = new Date(e.target.value);\n                                                    if (!isNaN(date.getTime())) {\n                                                        field.onChange(date);\n                                                    }\n                                                }}\n                                                min={format(new Date(), \"yyyy-MM-dd'T'HH:mm\")}\n                                            />\n                                        </FormControl>\n                                        <FormDescription>\n                                            Entry will be automatically published at this time\n                                        </FormDescription>\n                                        <FormMessage/>\n                                    </FormItem>\n                                )}\n                            />\n\n                            {/* Email Notification */}\n                            {projectHasEmailConfig && (\n                                <FormField\n                                    control={form.control}\n                                    name=\"sendEmailNotification\"\n                                    render={({field}) => (\n                                        <FormItem\n                                            className=\"flex flex-row items-center justify-between rounded-lg border p-3\">\n                                            <div className=\"space-y-0.5\">\n                                                <FormLabel className=\"flex items-center gap-2 text-base\">\n                                                    <Mail className=\"h-4 w-4\"/>\n                                                    Email Notification\n                                                </FormLabel>\n                                                <FormDescription>\n                                                    Send email to subscribers when scheduled\n                                                </FormDescription>\n                                            </div>\n                                            <FormControl>\n                                                <Switch\n                                                    checked={field.value}\n                                                    onCheckedChange={field.onChange}\n                                                />\n                                            </FormControl>\n                                        </FormItem>\n                                    )}\n                                />\n                            )}\n\n                            {/* Action Buttons */}\n                            <div className=\"flex gap-3 pt-4\">\n                                {isScheduled ? (\n                                    <>\n                                        <Button\n                                            type=\"button\"\n                                            variant=\"destructive\"\n                                            onClick={handleUnschedule}\n                                            disabled={isLoading}\n                                        >\n                                            <AlertCircle className=\"h-4 w-4 mr-2\"/>\n                                            Unschedule\n                                        </Button>\n                                        <Button\n                                            type=\"submit\"\n                                            disabled={isLoading}\n                                        >\n                                            <CheckCircle className=\"h-4 w-4 mr-2\"/>\n                                            Update Schedule\n                                        </Button>\n                                    </>\n                                ) : (\n                                    <>\n                                        <Button\n                                            type=\"button\"\n                                            variant=\"outline\"\n                                            onClick={() => setOpen(false)}\n                                            disabled={isLoading}\n                                        >\n                                            Cancel\n                                        </Button>\n                                        <Button\n                                            type=\"submit\"\n                                            disabled={isLoading}\n                                        >\n                                            <Calendar className=\"h-4 w-4 mr-2\"/>\n                                            {needsApproval ? 'Request Schedule' : 'Schedule Entry'}\n                                        </Button>\n                                    </>\n                                )}\n                            </div>\n                        </form>\n                    </Form>\n                </div>\n            </DialogContent>\n        </Dialog>\n    );\n};"
  },
  {
    "path": "components/dashboard/WhatsNewModal.tsx",
    "content": "'use client'\n\nimport React, {useState, useRef, useEffect} from 'react'\nimport {Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter} from '@/components/ui/dialog'\nimport {Button} from '@/components/ui/button'\nimport {Badge} from '@/components/ui/badge'\nimport {ScrollArea} from '@/components/ui/scroll-area'\nimport {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs'\nimport {motion, useInView} from 'framer-motion'\nimport {\n    AlertCircle,\n    ArrowRight,\n    Sparkles,\n    Calendar,\n    ThumbsUp,\n    Rocket,\n    XCircle\n} from 'lucide-react'\n\nexport interface WhatsNewItem {\n    title: string\n    description: string\n    type: 'feature' | 'improvement' | 'bugfix' | 'other'\n}\n\nexport interface WhatsNewContent {\n    version: string\n    releaseDate: string\n    title: string\n    description?: string\n    items: WhatsNewItem[]\n}\n\ninterface WhatsNewModalProps {\n    content: WhatsNewContent | null\n    isOpen: boolean\n    onClose: () => void\n    previousVersions?: WhatsNewContent[]\n}\n\nconst typeIcons = {\n    feature: <Rocket className=\"h-4 w-4 text-primary\"/>,\n    improvement: <ThumbsUp className=\"h-4 w-4 text-green-500\"/>,\n    bugfix: <XCircle className=\"h-4 w-4 text-red-500\"/>,\n    other: <AlertCircle className=\"h-4 w-4 text-blue-500\"/>\n}\n\nconst typeColors = {\n    feature: \"bg-primary/10 text-primary border-primary/20\",\n    improvement: \"bg-green-100 text-green-800 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800\",\n    bugfix: \"bg-red-100 text-red-800 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800\",\n    other: \"bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800\"\n}\n\nconst ItemCard = ({item, index}: { item: WhatsNewItem; index: number }) => {\n    const ref = useRef(null)\n    const isInView = useInView(ref, {once: true, margin: \"-10% 0px -10% 0px\"})\n\n    return (\n        <motion.div\n            ref={ref}\n            initial={{opacity: 0, y: 20}}\n            animate={isInView ? {opacity: 1, y: 0} : {opacity: 0, y: 20}}\n            transition={{delay: index * 0.08, duration: 0.4}}\n            className=\"group relative\"\n        >\n            <div\n                className=\"absolute left-2 top-0 bottom-0 w-px bg-gradient-to-b from-transparent via-border to-transparent\"/>\n\n            <div className=\"relative pl-6 pb-6\">\n                <div\n                    className=\"absolute left-0 top-1 h-3 w-3 rounded-full border-2 border-background bg-muted-foreground\"/>\n\n                <div\n                    className=\"overflow-hidden rounded-lg border bg-card p-4 shadow-sm transition-all group-hover:shadow-md\">\n                    <div className=\"flex gap-3\">\n                        <div className=\"mt-0.5 flex-shrink-0\">\n                            <div className=\"rounded-full bg-muted p-1.5\">\n                                {typeIcons[item.type]}\n                            </div>\n                        </div>\n\n                        <div className=\"space-y-1.5 flex-1\">\n                            <div className=\"flex items-center gap-2 flex-wrap\">\n                                <h3 className=\"font-medium\">{item.title}</h3>\n                                <Badge variant=\"outline\" className={`${typeColors[item.type]} text-xs`}>\n                                    {item.type === 'feature' ? 'New Feature' :\n                                        item.type === 'improvement' ? 'Improvement' :\n                                            item.type === 'bugfix' ? 'Bug Fix' : 'Update'}\n                                </Badge>\n                            </div>\n\n                            <p className=\"text-sm text-muted-foreground\">{item.description}</p>\n                        </div>\n                    </div>\n                </div>\n            </div>\n        </motion.div>\n    )\n}\n\nexport const WhatsNewModal: React.FC<WhatsNewModalProps> = ({\n                                                                content,\n                                                                isOpen,\n                                                                onClose,\n                                                                previousVersions = []\n                                                            }) => {\n    const [activeTab, setActiveTab] = useState('current')\n    const [canClose, setCanClose] = useState(false)\n    const [countdown, setCountdown] = useState(3)\n    const mainRef = useRef(null)\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    const isInView = useInView(mainRef, {once: true})\n\n    // Reset to current tab when opening the modal and start countdown\n    useEffect(() => {\n        if (isOpen) {\n            setActiveTab('current')\n            setCanClose(false)\n            setCountdown(3)\n\n            const timer = setInterval(() => {\n                setCountdown((prev) => {\n                    if (prev <= 1) {\n                        setCanClose(true)\n                        clearInterval(timer)\n                        return 0\n                    }\n                    return prev - 1\n                })\n            }, 1000)\n\n            return () => clearInterval(timer)\n        }\n    }, [isOpen])\n\n    const handleClose = () => {\n        if (canClose) {\n            onClose()\n        }\n    }\n\n    if (!content) return null\n\n    // Group items by type for the summary\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    const itemsByType = content.items.reduce((acc, item) => {\n        acc[item.type] = (acc[item.type] || 0) + 1\n        return acc\n    }, {} as Record<string, number>)\n\n    return (\n        <Dialog\n            open={isOpen}\n            onOpenChange={() => {\n            }} // Prevent closing from outside clicks or escape\n        >\n            <DialogContent\n                className=\"sm:max-w-xl w-full max-w-[95vw] h-[90vh] max-h-[90vh] p-0 overflow-hidden flex flex-col gap-0 border-none bg-background/95 backdrop-blur-sm shadow-2xl\"\n                onPointerDownOutside={(e) => e.preventDefault()} // Prevent outside clicks\n                onEscapeKeyDown={(e) => e.preventDefault()} // Prevent escape key\n            >\n                <div\n                    className=\"absolute inset-0 bg-gradient-to-tr from-primary/5 via-background to-background/95 -z-10\"/>\n\n                {/* Remove the default close button by overriding it */}\n                <div className=\"absolute right-4 top-4 z-10\">\n                    {/* Empty div to occupy space where close button would be */}\n                </div>\n\n                <DialogHeader className=\"p-6 pb-4 border-b flex-shrink-0\">\n                    <div className=\"flex items-start gap-4\">\n                        <div\n                            className=\"h-12 w-12 rounded-full bg-primary/10 flex-shrink-0 flex items-center justify-center\">\n                            <Sparkles className=\"h-6 w-6 text-primary\"/>\n                        </div>\n\n                        <div className=\"space-y-1 min-w-0 flex-1\">\n                            <DialogTitle className=\"text-xl font-semibold\">\n                                What&apos;s New in v{content.version}\n                            </DialogTitle>\n\n                            <div className=\"flex items-center text-sm text-muted-foreground\">\n                                <Calendar className=\"h-3.5 w-3.5 mr-1.5 flex-shrink-0\"/>\n                                <time dateTime={content.releaseDate}>\n                                    {new Date(content.releaseDate).toLocaleDateString(undefined, {\n                                        year: 'numeric',\n                                        month: 'long',\n                                        day: 'numeric'\n                                    })}\n                                </time>\n                            </div>\n\n                            {content.description && (\n                                <p className=\"text-sm text-muted-foreground pt-2\">{content.description}</p>\n                            )}\n                        </div>\n                    </div>\n                </DialogHeader>\n\n                <div className=\"flex-1 min-h-0 overflow-hidden\">\n                    {previousVersions.length > 0 ? (\n                        <Tabs value={activeTab} onValueChange={setActiveTab} className=\"h-full flex flex-col\">\n                            <div className=\"border-b px-6 flex-shrink-0\">\n                                <TabsList className=\"bg-transparent h-auto p-0\">\n                                    <TabsTrigger\n                                        value=\"current\"\n                                        className=\"data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none py-2.5 px-3\"\n                                    >\n                                        Current\n                                    </TabsTrigger>\n                                    <TabsTrigger\n                                        value=\"history\"\n                                        className=\"data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none py-2.5 px-3\"\n                                    >\n                                        Previous Updates\n                                    </TabsTrigger>\n                                </TabsList>\n                            </div>\n\n                            <TabsContent value=\"current\" className=\"flex-1 min-h-0 p-0 m-0 overflow-hidden\">\n                                <ScrollArea className=\"h-full w-full\">\n                                    <div className=\"px-6 py-4\" ref={mainRef}>\n                                        <div className=\"space-y-0\">\n                                            {content.items.map((item, index) => (\n                                                <ItemCard key={index} item={item} index={index}/>\n                                            ))}\n                                        </div>\n                                    </div>\n                                </ScrollArea>\n                            </TabsContent>\n\n                            <TabsContent value=\"history\" className=\"flex-1 min-h-0 p-0 m-0 overflow-hidden\">\n                                <ScrollArea className=\"h-full w-full\">\n                                    <div className=\"px-6 py-4\">\n                                        <div className=\"space-y-6\">\n                                            {previousVersions.map((version, versionIndex) => (\n                                                <div key={version.version} className=\"pb-6 last:pb-0\">\n                                                    <div className=\"mb-3 border-b pb-1\">\n                                                        <h3 className=\"font-medium\">v{version.version} - {version.title}</h3>\n                                                        <div\n                                                            className=\"flex items-center text-xs text-muted-foreground\">\n                                                            <Calendar className=\"h-3 w-3 mr-1 flex-shrink-0\"/>\n                                                            <time dateTime={version.releaseDate}>\n                                                                {new Date(version.releaseDate).toLocaleDateString()}\n                                                            </time>\n                                                        </div>\n                                                    </div>\n\n                                                    <div className=\"space-y-0 text-sm\">\n                                                        {version.items.slice(0, 3).map((item, index) => (\n                                                            <ItemCard\n                                                                key={`${version.version}-${index}`}\n                                                                item={item}\n                                                                index={index + versionIndex}\n                                                            />\n                                                        ))}\n\n                                                        {version.items.length > 3 && (\n                                                            <div className=\"pl-6 text-muted-foreground text-sm\">\n                                                                + {version.items.length - 3} more changes\n                                                            </div>\n                                                        )}\n                                                    </div>\n                                                </div>\n                                            ))}\n                                        </div>\n                                    </div>\n                                </ScrollArea>\n                            </TabsContent>\n                        </Tabs>\n                    ) : (\n                        <ScrollArea className=\"h-full w-full\">\n                            <div className=\"px-6 py-4\" ref={mainRef}>\n                                <div className=\"space-y-0\">\n                                    {content.items.map((item, index) => (\n                                        <ItemCard key={index} item={item} index={index}/>\n                                    ))}\n                                </div>\n                            </div>\n                        </ScrollArea>\n                    )}\n                </div>\n\n                <DialogFooter className=\"flex sm:justify-between items-center border-t p-4 flex-shrink-0\">\n                    <div className=\"hidden sm:block text-xs text-muted-foreground\">\n                        Thanks for using Changerawr! We hope you enjoy our latest release :)\n                    </div>\n                    <Button\n                        onClick={handleClose}\n                        className=\"w-full sm:w-auto\"\n                        disabled={!canClose}\n                    >\n                        {canClose ? (\n                            <>Got it <ArrowRight className=\"ml-2 h-4 w-4\"/></>\n                        ) : (\n                            <>Wait {countdown}s <ArrowRight className=\"ml-2 h-4 w-4\"/></>\n                        )}\n                    </Button>\n                </DialogFooter>\n            </DialogContent>\n        </Dialog>\n    )\n}\n\nexport default WhatsNewModal"
  },
  {
    "path": "components/github/GitHubGenerateDialog.tsx",
    "content": "'use client';\n\nimport React, {useState, useEffect, useCallback} from 'react';\nimport {motion, AnimatePresence} from 'framer-motion';\nimport confetti from 'canvas-confetti';\nimport {\n    Github,\n    Sparkles,\n    Calendar,\n    GitBranch,\n    Tag,\n    Loader2,\n    Copy,\n    Check,\n    BookOpen,\n    Info,\n    Zap,\n    Brain,\n    ArrowRight,\n    Code2,\n    FileText,\n    ArrowLeft,\n    Clock,\n    Database,\n    Activity,\n    AlertCircle\n} from 'lucide-react';\n\nimport {\n    Dialog,\n    DialogContent,\n    DialogTrigger,\n    DialogTitle,\n} from '@/components/ui/dialog';\nimport {Button} from '@/components/ui/button';\nimport {Input} from '@/components/ui/input';\nimport {Label} from '@/components/ui/label';\nimport {Switch} from '@/components/ui/switch';\nimport {Badge} from '@/components/ui/badge';\nimport {Separator} from '@/components/ui/separator';\nimport {Alert, AlertDescription} from '@/components/ui/alert';\nimport {Slider} from '@/components/ui/slider';\nimport {ScrollArea} from '@/components/ui/scroll-area';\nimport {\n    Select,\n    SelectContent,\n    SelectItem,\n    SelectTrigger,\n    SelectValue,\n} from '@/components/ui/select';\nimport {\n    Tooltip,\n    TooltipContent,\n    TooltipProvider,\n    TooltipTrigger,\n} from '@/components/ui/tooltip';\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card';\n\n// Types\ninterface GitHubTag {\n    name: string;\n    sha: string;\n}\n\ninterface GitHubRelease {\n    name: string;\n    tagName: string;\n}\n\ninterface AISettings {\n    enableAIAssistant: boolean;\n    aiApiKey: string | null;\n    aiModel: string | null;\n}\n\ninterface GenerationOptions {\n    method: 'recent' | 'between_tags' | 'between_commits';\n    daysBack: number;\n    fromRef: string;\n    toRef: string;\n    useAI: boolean;\n    includeCodeAnalysis: boolean;\n    maxCommitsToAnalyze: number;\n    groupByType: boolean;\n    includeCommitLinks: boolean;\n}\n\ninterface GeneratedChangelog {\n    content: string;\n    version?: string;\n    commitsCount: number;\n    entriesCount: number;\n    entries: Array<{\n        category: string;\n        description: string;\n        impact?: string;\n        technicalDetails?: string;\n        files: string[];\n        commit: string;\n    }>;\n}\n\ninterface GenerateResult {\n    success: boolean;\n    changelog?: GeneratedChangelog;\n    metadata?: {\n        method: string;\n        generatedAt: string;\n        repositoryUrl: string;\n        fromRef?: string;\n        toRef?: string;\n        daysBack?: number;\n        aiEnhanced?: boolean;\n        codeAnalysis?: boolean;\n        totalCommits?: number;\n        analyzedCommits?: number;\n        hasCodeAnalysis?: boolean;\n        model?: string;\n    };\n    error?: string;\n    details?: string;\n}\n\ninterface Props {\n    projectId: string;\n    onGenerated: (content: string, version?: string) => void;\n    trigger?: React.ReactNode;\n}\n\nconst DEFAULT_OPTIONS: GenerationOptions = {\n    method: 'recent',\n    daysBack: 7,\n    fromRef: '',\n    toRef: 'HEAD',\n    useAI: false,\n    includeCodeAnalysis: false,\n    maxCommitsToAnalyze: 25,\n    groupByType: true,\n    includeCommitLinks: true,\n};\n\nexport default function GitHubGenerateDialog({\n                                                 projectId,\n                                                 onGenerated,\n                                                 trigger\n                                             }: Props) {\n    // UI State\n    const [isOpen, setIsOpen] = useState(false);\n    const [currentStep, setCurrentStep] = useState(1);\n    const [isLoading, setIsLoading] = useState(false);\n    const [isFetchingTags, setIsFetchingTags] = useState(false);\n    const [error, setError] = useState<string | undefined>();\n    const [copied, setCopied] = useState(false);\n    const [isRedirecting, setIsRedirecting] = useState(false);\n\n    // Data State\n    const [tags, setTags] = useState<GitHubTag[]>([]);\n    const [releases, setReleases] = useState<GitHubRelease[]>([]);\n    const [result, setResult] = useState<GenerateResult | undefined>();\n    const [aiSettings, setAiSettings] = useState<AISettings>({\n        enableAIAssistant: false,\n        aiApiKey: null,\n        aiModel: 'copilot-zero'\n    });\n\n    // Form State\n    const [options, setOptions] = useState<GenerationOptions>(DEFAULT_OPTIONS);\n\n    // Reset state when dialog opens/closes\n    useEffect(() => {\n        if (isOpen) {\n            loadInitialData();\n            setResult(undefined);\n            setError(undefined);\n            setCopied(false);\n            setCurrentStep(1);\n        } else {\n            setOptions(DEFAULT_OPTIONS);\n            setResult(undefined);\n            setError(undefined);\n            setCurrentStep(1);\n        }\n    }, [isOpen]);\n\n    // Load initial data\n    const loadInitialData = useCallback(async () => {\n        await Promise.all([\n            loadAISettings(),\n            loadTagsAndReleases()\n        ]);\n    }, [projectId]);\n\n    const loadAISettings = async () => {\n        try {\n            // Fetch encrypted settings\n            const response = await fetch('/api/ai/settings');\n            if (response.ok) {\n                const data = await response.json();\n\n                let decryptedApiKey: string | null = null;\n\n                // If AI is enabled and we have an encrypted API key, decrypt it\n                if (data.enableAIAssistant && data.aiApiKey) {\n                    try {\n                        const decryptResponse = await fetch('/api/ai/decrypt', {\n                            method: 'POST',\n                            headers: {\n                                'Content-Type': 'application/json',\n                            },\n                            body: JSON.stringify({ encryptedToken: data.aiApiKey }),\n                        });\n\n                        if (decryptResponse.ok) {\n                            const decryptData = await decryptResponse.json();\n                            decryptedApiKey = decryptData.decryptedKey;\n                        } else {\n                            console.error('Failed to decrypt API key:', decryptResponse.statusText);\n                        }\n                    } catch (decryptError) {\n                        console.error('Error decrypting API key:', decryptError);\n                    }\n                }\n\n                setAiSettings({\n                    enableAIAssistant: data.enableAIAssistant || false,\n                    aiApiKey: decryptedApiKey,\n                    aiModel: data.aiDefaultModel || 'copilot-zero'\n                });\n\n                if (!data.enableAIAssistant || !decryptedApiKey) {\n                    setOptions(prev => ({...prev, useAI: false}));\n                }\n            }\n        } catch (err) {\n            console.error('Failed to load AI settings:', err);\n            setAiSettings({enableAIAssistant: false, aiApiKey: null, aiModel: 'copilot-zero'});\n            setOptions(prev => ({...prev, useAI: false}));\n        }\n    };\n\n    const loadTagsAndReleases = async () => {\n        try {\n            setIsFetchingTags(true);\n            setError(undefined);\n\n            const response = await fetch(`/api/projects/${projectId}/integrations/github/tags`);\n            if (!response.ok) {\n                const errorData = await response.json().catch(() => ({}));\n                throw new Error(errorData.error || errorData.details || `Failed to load tags (${response.status})`);\n            }\n\n            const data = await response.json();\n            setTags(data.tags || []);\n            setReleases(data.releases || []);\n        } catch (err) {\n            console.error('Failed to load tags and releases:', err);\n            const errorMessage = err instanceof Error ? err.message : 'Failed to load tags and releases';\n            setError(errorMessage);\n            setTags([]);\n            setReleases([]);\n        } finally {\n            setIsFetchingTags(false);\n        }\n    };\n\n    const generateChangelog = async () => {\n        try {\n            setIsLoading(true);\n            setError(undefined);\n            setResult(undefined);\n\n            const validationError = validateOptions();\n            if (validationError) {\n                throw new Error(validationError);\n            }\n\n            const response = await fetch(`/api/projects/${projectId}/integrations/github/generate`, {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({\n                    ...options,\n                    aiModel: aiSettings.aiModel,\n                })\n            });\n\n            const data = await response.json();\n\n            if (!response.ok) {\n                throw new Error(data.details || data.error || `Request failed (${response.status})`);\n            }\n\n            if (!data.success) {\n                throw new Error(data.error || 'Generation was not successful');\n            }\n\n            setResult(data);\n            setCurrentStep(3);\n\n            // Trigger confetti on successful generation\n            confetti({\n                particleCount: 100,\n                spread: 70,\n                origin: {y: 0.6},\n                colors: ['#3b82f6', '#8b5cf6', '#06b6d4', '#10b981']\n            });\n\n        } catch (err) {\n            console.error('Generation error:', err);\n            const errorMessage = err instanceof Error ? err.message : 'Failed to generate changelog';\n            setError(errorMessage);\n        } finally {\n            setIsLoading(false);\n        }\n    };\n\n    const validateOptions = (): string | undefined => {\n        if (options.method === 'recent') {\n            if (!options.daysBack || options.daysBack < 1 || options.daysBack > 365) {\n                return 'Days back must be between 1 and 365';\n            }\n        }\n\n        if (options.method === 'between_tags' || options.method === 'between_commits') {\n            if (!options.fromRef || !options.toRef) {\n                return 'Both from and to references are required';\n            }\n            if (options.fromRef === options.toRef) {\n                return 'From and to references must be different';\n            }\n        }\n\n        return undefined;\n    };\n\n    const copyToClipboard = async () => {\n        if (!result?.changelog?.content) return;\n\n        try {\n            await navigator.clipboard.writeText(result.changelog.content);\n            setCopied(true);\n            setTimeout(() => setCopied(false), 2000);\n        } catch (err) {\n            console.error('Failed to copy to clipboard:', err);\n        }\n    };\n\n    const handleUseChangelog = () => {\n        if (result?.changelog) {\n            setIsRedirecting(true);\n\n            confetti({\n                particleCount: 150,\n                spread: 100,\n                origin: {y: 0.5},\n                colors: ['#10b981', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6']\n            });\n\n            setTimeout(() => {\n                onGenerated(result.changelog?.content ?? '', result.changelog?.version);\n            }, 800);\n        }\n    };\n\n    const updateOptions = (updates: Partial<GenerationOptions>) => {\n        setOptions(prev => ({...prev, ...updates}));\n    };\n\n    const isGenerateDisabled = (): boolean => {\n        if (isLoading || isFetchingTags) return true;\n\n        if (options.method === 'recent') {\n            return !options.daysBack || options.daysBack < 1;\n        }\n\n        if (options.method === 'between_tags' || options.method === 'between_commits') {\n            return !options.fromRef || !options.toRef;\n        }\n\n        return false;\n    };\n\n    return (\n        <TooltipProvider>\n            <Dialog open={isOpen} onOpenChange={setIsOpen}>\n                <DialogTrigger asChild>\n                    {trigger || (\n                        <Button variant=\"outline\" className=\"gap-2\">\n                            <Github className=\"h-4 w-4\"/>\n                            Generate from GitHub\n                        </Button>\n                    )}\n                </DialogTrigger>\n\n                <DialogContent className=\"max-w-screen max-h-screen w-screen h-screen p-0 border-0 bg-background\">\n                    <DialogTitle className=\"sr-only\">\n                        Generate Changelog from GitHub - Step {currentStep} of 3\n                    </DialogTitle>\n                    {/* Redirecting Overlay */}\n                    <AnimatePresence>\n                        {isRedirecting && (\n                            <motion.div\n                                initial={{opacity: 0}}\n                                animate={{opacity: 1}}\n                                exit={{opacity: 0}}\n                                className=\"absolute inset-0 z-50 bg-background/95 backdrop-blur-sm flex items-center justify-center\"\n                            >\n                                <div className=\"text-center space-y-6\">\n                                    <div className=\"space-y-2\">\n                                        <h3 className=\"text-2xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent\">\n                                            Creating Your Changelog\n                                        </h3>\n                                        <p className=\"text-muted-foreground\">\n                                            Preparing your content...\n                                        </p>\n                                    </div>\n                                    <motion.div\n                                        animate={{scale: [1, 1.1, 1]}}\n                                        transition={{duration: 2, repeat: Infinity}}\n                                        className=\"flex items-center justify-center gap-2\"\n                                    >\n                                        <motion.div\n                                            animate={{y: [0, -10, 0]}}\n                                            transition={{duration: 0.6, repeat: Infinity, delay: 0}}\n                                            className=\"w-2 h-2 bg-blue-500 rounded-full\"\n                                        />\n                                        <motion.div\n                                            animate={{y: [0, -10, 0]}}\n                                            transition={{duration: 0.6, repeat: Infinity, delay: 0.2}}\n                                            className=\"w-2 h-2 bg-purple-500 rounded-full\"\n                                        />\n                                        <motion.div\n                                            animate={{y: [0, -10, 0]}}\n                                            transition={{duration: 0.6, repeat: Infinity, delay: 0.4}}\n                                            className=\"w-2 h-2 bg-green-500 rounded-full\"\n                                        />\n                                    </motion.div>\n                                </div>\n                            </motion.div>\n                        )}\n                    </AnimatePresence>\n\n                    <div className=\"flex h-full\">\n                        {/* Sidebar */}\n                        <div className=\"w-80 border-r bg-muted/20 flex flex-col\">\n                            {/* Header */}\n                            <div className=\"p-6 border-b\">\n                                <div className=\"flex items-center gap-3 mb-4\">\n                                    <div className=\"p-2 bg-primary/10 rounded-lg\">\n                                        <Github className=\"h-5 w-5 text-primary\"/>\n                                    </div>\n                                    <div>\n                                        <h1 className=\"text-lg font-bold\">GitHub Generator</h1>\n                                        <p className=\"text-sm text-muted-foreground\">Transform commits into\n                                            changelog</p>\n                                    </div>\n                                </div>\n\n                                {/* Progress Steps */}\n                                <div className=\"space-y-3\">\n                                    {[\n                                        {step: 1, label: 'Source', icon: GitBranch},\n                                        {step: 2, label: 'Options', icon: Sparkles},\n                                        {step: 3, label: 'Results', icon: BookOpen}\n                                    ].map(({step, label, icon: Icon}) => (\n                                        <div\n                                            key={step}\n                                            className={`flex items-center gap-3 p-2 rounded-lg transition-all ${\n                                                currentStep === step\n                                                    ? 'bg-primary/10 text-primary'\n                                                    : currentStep > step\n                                                        ? 'text-green-600'\n                                                        : 'text-muted-foreground'\n                                            }`}\n                                        >\n                                            <div\n                                                className={`w-8 h-8 rounded-full flex items-center justify-center border-2 transition-all ${\n                                                    currentStep > step\n                                                        ? 'bg-green-100 border-green-500 text-green-600'\n                                                        : currentStep === step\n                                                            ? 'border-primary bg-primary/10 text-primary'\n                                                            : 'border-muted-foreground/30 text-muted-foreground'\n                                                }`}>\n                                                {currentStep > step ? (\n                                                    <Check className=\"h-4 w-4\"/>\n                                                ) : (\n                                                    <Icon className=\"h-4 w-4\"/>\n                                                )}\n                                            </div>\n                                            <div className=\"flex-1\">\n                                                <div className=\"font-medium text-sm\">{label}</div>\n                                            </div>\n                                        </div>\n                                    ))}\n                                </div>\n                            </div>\n\n                            {/* Current Step Info */}\n                            <ScrollArea className=\"flex-1\">\n                                <div className=\"p-6\">\n                                    <AnimatePresence mode=\"wait\">\n                                        {currentStep === 1 && (\n                                            <motion.div\n                                                key=\"step1-info\"\n                                                initial={{opacity: 0, y: 10}}\n                                                animate={{opacity: 1, y: 0}}\n                                                exit={{opacity: 0, y: -10}}\n                                                className=\"space-y-4\"\n                                            >\n                                                <h3 className=\"font-semibold text-primary\">Choose Source Method</h3>\n                                                <div className=\"space-y-3 text-sm text-muted-foreground\">\n                                                    <div className=\"p-3 bg-background rounded-lg border\">\n                                                        <div className=\"font-medium text-foreground mb-1\">Recent Commits\n                                                        </div>\n                                                        <div>Analyze commits from the last N days</div>\n                                                    </div>\n                                                    <div className=\"p-3 bg-background rounded-lg border\">\n                                                        <div className=\"font-medium text-foreground mb-1\">Between Tags</div>\n                                                        <div>Compare changes between releases</div>\n                                                    </div>\n                                                    <div className=\"p-3 bg-background rounded-lg border\">\n                                                        <div className=\"font-medium text-foreground mb-1\">Between Commits\n                                                        </div>\n                                                        <div>Specify exact commit range</div>\n                                                    </div>\n                                                </div>\n                                            </motion.div>\n                                        )}\n\n                                        {currentStep === 2 && (\n                                            <motion.div\n                                                key=\"step2-info\"\n                                                initial={{opacity: 0, y: 10}}\n                                                animate={{opacity: 1, y: 0}}\n                                                exit={{opacity: 0, y: -10}}\n                                                className=\"space-y-4\"\n                                            >\n                                                <h3 className=\"font-semibold text-primary\">Configure Options</h3>\n                                                <div className=\"space-y-3\">\n                                                    <div className=\"p-3 bg-background rounded-lg border\">\n                                                        <div className=\"font-medium text-foreground mb-2\">Current Settings\n                                                        </div>\n                                                        <div className=\"space-y-2 text-sm text-muted-foreground\">\n                                                            <div className=\"flex justify-between\">\n                                                                <span>AI Enhancement:</span>\n                                                                <span\n                                                                    className={options.useAI ? 'text-green-600' : 'text-orange-600'}>\n                                                                    {options.useAI ? 'Enabled' : 'Disabled'}\n                                                                </span>\n                                                            </div>\n                                                            <div className=\"flex justify-between\">\n                                                                <span>Code Analysis:</span>\n                                                                <span\n                                                                    className={options.includeCodeAnalysis ? 'text-green-600' : 'text-muted-foreground'}>\n                                                                    {options.includeCodeAnalysis ? 'Yes' : 'No'}\n                                                                </span>\n                                                            </div>\n                                                            <div className=\"flex justify-between\">\n                                                                <span>Group by Type:</span>\n                                                                <span\n                                                                    className={options.groupByType ? 'text-green-600' : 'text-muted-foreground'}>\n                                                                    {options.groupByType ? 'Yes' : 'No'}\n                                                                </span>\n                                                            </div>\n                                                        </div>\n                                                    </div>\n                                                </div>\n                                            </motion.div>\n                                        )}\n\n                                        {currentStep === 3 && result && (\n                                            <motion.div\n                                                key=\"step3-info\"\n                                                initial={{opacity: 0, y: 10}}\n                                                animate={{opacity: 1, y: 0}}\n                                                exit={{opacity: 0, y: -10}}\n                                                className=\"space-y-4\"\n                                            >\n                                                <h3 className=\"font-semibold text-primary\">Generation Complete</h3>\n                                                <div className=\"space-y-3\">\n                                                    <div className=\"grid grid-cols-2 gap-3\">\n                                                        <div className=\"text-center p-3 bg-primary/5 rounded-lg\">\n                                                            <div\n                                                                className=\"text-xl font-bold text-primary\">{result.changelog?.commitsCount || 0}</div>\n                                                            <div className=\"text-xs text-muted-foreground\">Commits</div>\n                                                        </div>\n                                                        <div\n                                                            className=\"text-center p-3 bg-green-50 dark:bg-green-950/20 rounded-lg\">\n                                                            <div\n                                                                className=\"text-xl font-bold text-green-600\">{result.changelog?.entriesCount || 0}</div>\n                                                            <div className=\"text-xs text-muted-foreground\">Entries</div>\n                                                        </div>\n                                                    </div>\n\n                                                    {result.changelog?.version && (\n                                                        <div\n                                                            className=\"text-center p-3 bg-blue-50 dark:bg-blue-950/20 rounded-lg\">\n                                                            <div\n                                                                className=\"text-sm font-semibold text-blue-600\">{result.changelog.version}</div>\n                                                            <div className=\"text-xs text-muted-foreground\">Suggested\n                                                                Version\n                                                            </div>\n                                                        </div>\n                                                    )}\n\n                                                    <div className=\"space-y-2 text-xs text-muted-foreground\">\n                                                        {result.metadata?.hasCodeAnalysis && (\n                                                            <div className=\"flex items-center gap-2\">\n                                                                <Database className=\"h-3 w-3 text-green-600\"/>\n                                                                <span\n                                                                    className=\"text-green-600\">Deep analysis enabled</span>\n                                                            </div>\n                                                        )}\n                                                        <div className=\"flex items-center gap-2\">\n                                                            <Clock className=\"h-3 w-3\"/>\n                                                            <span>Generated just now</span>\n                                                        </div>\n                                                    </div>\n                                                </div>\n                                            </motion.div>\n                                        )}\n                                    </AnimatePresence>\n                                </div>\n                            </ScrollArea>\n\n                            {/* Footer Actions */}\n                            <div className=\"p-6 border-t space-y-3\">\n                                {currentStep < 3 && (\n                                    <Button\n                                        onClick={() => {\n                                            if (currentStep === 2) {\n                                                generateChangelog();\n                                            } else {\n                                                setCurrentStep(currentStep + 1);\n                                            }\n                                        }}\n                                        disabled={isGenerateDisabled()}\n                                        className=\"w-full gap-2\"\n                                        size=\"lg\"\n                                    >\n                                        {currentStep === 2 ? (\n                                            isLoading ? (\n                                                <>\n                                                    <Loader2 className=\"h-4 w-4 animate-spin\"/>\n                                                    Generating...\n                                                </>\n                                            ) : (\n                                                <>\n                                                    <Zap className=\"h-4 w-4\"/>\n                                                    Generate Changelog\n                                                </>\n                                            )\n                                        ) : (\n                                            <>\n                                                Continue\n                                                <ArrowRight className=\"h-4 w-4\"/>\n                                            </>\n                                        )}\n                                    </Button>\n                                )}\n\n                                {currentStep === 3 && result && (\n                                    <Button\n                                        onClick={handleUseChangelog}\n                                        className=\"w-full gap-2\"\n                                        disabled={isRedirecting}\n                                        size=\"lg\"\n                                    >\n                                        {isRedirecting ? (\n                                            <>\n                                                <Loader2 className=\"h-4 w-4 animate-spin\"/>\n                                                Creating...\n                                            </>\n                                        ) : (\n                                            <>\n                                                <BookOpen className=\"h-4 w-4\"/>\n                                                Use Changelog\n                                            </>\n                                        )}\n                                    </Button>\n                                )}\n\n                                {currentStep > 1 && (\n                                    <Button\n                                        variant=\"outline\"\n                                        onClick={() => setCurrentStep(currentStep - 1)}\n                                        className=\"w-full gap-2\"\n                                    >\n                                        <ArrowLeft className=\"h-4 w-4\"/>\n                                        Back\n                                    </Button>\n                                )}\n                            </div>\n                        </div>\n\n                        {/* Main Content */}\n                        <div className=\"flex-1 flex flex-col min-h-0\">\n                            {/* Error Alert */}\n                            <AnimatePresence>\n                                {error && (\n                                    <motion.div\n                                        initial={{opacity: 0, height: 0}}\n                                        animate={{opacity: 1, height: 'auto'}}\n                                        exit={{opacity: 0, height: 0}}\n                                        className=\"p-4 border-b bg-destructive/5 flex-shrink-0\"\n                                    >\n                                        <Alert variant=\"destructive\" className=\"border-destructive/30\">\n                                            <AlertCircle className=\"h-4 w-4\"/>\n                                            <AlertDescription>{error}</AlertDescription>\n                                        </Alert>\n                                    </motion.div>\n                                )}\n                            </AnimatePresence>\n\n                            {/* Step Content */}\n                            <ScrollArea className=\"flex-1\">\n                                <div className=\"p-8\">\n                                    <AnimatePresence mode=\"wait\">\n                                        {/* Step 1: Source Selection */}\n                                        {currentStep === 1 && (\n                                            <motion.div\n                                                key=\"step1\"\n                                                initial={{opacity: 0, x: 20}}\n                                                animate={{opacity: 1, x: 0}}\n                                                exit={{opacity: 0, x: -20}}\n                                                className=\"space-y-8\"\n                                            >\n                                                <div>\n                                                    <h2 className=\"text-2xl font-bold mb-2\">Choose Your Source</h2>\n                                                    <p className=\"text-muted-foreground\">Select how to gather commits for\n                                                        your changelog</p>\n                                                </div>\n\n                                                <div className=\"grid grid-cols-3 gap-6\">\n                                                    {[\n                                                        {\n                                                            value: 'recent' as const,\n                                                            title: 'Recent Commits',\n                                                            icon: Calendar,\n                                                            description: 'Last N days of commits'\n                                                        },\n                                                        {\n                                                            value: 'between_tags' as const,\n                                                            title: 'Between Tags',\n                                                            icon: Tag,\n                                                            description: 'Compare two releases'\n                                                        },\n                                                        {\n                                                            value: 'between_commits' as const,\n                                                            title: 'Between Commits',\n                                                            icon: GitBranch,\n                                                            description: 'Specific commit range'\n                                                        }\n                                                    ].map((method) => {\n                                                        const Icon = method.icon;\n                                                        const isSelected = options.method === method.value;\n\n                                                        return (\n                                                            <motion.div\n                                                                key={method.value}\n                                                                whileHover={{scale: 1.02}}\n                                                                whileTap={{scale: 0.98}}\n                                                            >\n                                                                <Card\n                                                                    className={`cursor-pointer transition-all h-32 ${\n                                                                        isSelected\n                                                                            ? 'border-primary bg-primary/5 shadow-lg'\n                                                                            : 'hover:border-primary/30 hover:shadow-md'\n                                                                    }`}\n                                                                    onClick={() => updateOptions({method: method.value})}\n                                                                >\n                                                                    <CardContent\n                                                                        className=\"p-4 h-full flex flex-col items-center justify-center text-center\">\n                                                                        <div\n                                                                            className=\"p-3 rounded-full mb-3 bg-primary/10\">\n                                                                            <Icon className=\"h-6 w-6 text-primary\"/>\n                                                                        </div>\n                                                                        <div\n                                                                            className=\"font-semibold mb-1\">{method.title}</div>\n                                                                        <div\n                                                                            className=\"text-sm text-muted-foreground\">{method.description}</div>\n                                                                        {isSelected && (\n                                                                            <Check className=\"h-5 w-5 text-primary mt-2\"/>\n                                                                        )}\n                                                                    </CardContent>\n                                                                </Card>\n                                                            </motion.div>\n                                                        );\n                                                    })}\n                                                </div>\n\n                                                {/* Method Configuration */}\n                                                <div>\n                                                    <AnimatePresence mode=\"wait\">\n                                                        {options.method === 'recent' && (\n                                                            <motion.div\n                                                                key=\"recent\"\n                                                                initial={{opacity: 0, y: 20}}\n                                                                animate={{opacity: 1, y: 0}}\n                                                                exit={{opacity: 0, y: -20}}\n                                                            >\n                                                                <Card>\n                                                                    <CardHeader>\n                                                                        <CardTitle className=\"flex items-center gap-2\">\n                                                                            <Clock className=\"h-5 w-5\"/>\n                                                                            Days to look back\n                                                                        </CardTitle>\n                                                                    </CardHeader>\n                                                                    <CardContent className=\"space-y-6\">\n                                                                        <div\n                                                                            className=\"text-center p-8 bg-muted/50 rounded-lg\">\n                                                                            <span\n                                                                                className=\"text-6xl font-bold text-primary\">{options.daysBack}</span>\n                                                                            <span\n                                                                                className=\"text-2xl text-muted-foreground ml-2\">days</span>\n                                                                        </div>\n                                                                        <Slider\n                                                                            value={[options.daysBack]}\n                                                                            onValueChange={(value) => updateOptions({daysBack: value[0]})}\n                                                                            min={1}\n                                                                            max={365}\n                                                                            step={1}\n                                                                            className=\"w-full\"\n                                                                        />\n                                                                        <div\n                                                                            className=\"flex justify-between text-sm text-muted-foreground\">\n                                                                            <span>1 day</span>\n                                                                            <span>365 days</span>\n                                                                        </div>\n                                                                    </CardContent>\n                                                                </Card>\n                                                            </motion.div>\n                                                        )}\n\n                                                        {options.method === 'between_tags' && (\n                                                            <motion.div\n                                                                key=\"tags\"\n                                                                initial={{opacity: 0, y: 20}}\n                                                                animate={{opacity: 1, y: 0}}\n                                                                exit={{opacity: 0, y: -20}}\n                                                            >\n                                                                <Card>\n                                                                    <CardHeader>\n                                                                        <CardTitle className=\"flex items-center gap-2\">\n                                                                            <Tag className=\"h-5 w-5\"/>\n                                                                            Select Tag Range\n                                                                        </CardTitle>\n                                                                    </CardHeader>\n                                                                    <CardContent>\n                                                                        {isFetchingTags ? (\n                                                                            <div\n                                                                                className=\"flex items-center justify-center py-12\">\n                                                                                <div className=\"text-center\">\n                                                                                    <Loader2\n                                                                                        className=\"h-8 w-8 animate-spin mx-auto mb-4 text-primary\"/>\n                                                                                    <p className=\"text-muted-foreground\">Loading\n                                                                                        tags and releases...</p>\n                                                                                </div>\n                                                                            </div>\n                                                                        ) : tags.length === 0 && releases.length === 0 ? (\n                                                                            <Alert>\n                                                                                <AlertDescription>\n                                                                                    No tags or releases found in the\n                                                                                    repository. Create some tags or releases\n                                                                                    first.\n                                                                                </AlertDescription>\n                                                                            </Alert>\n                                                                        ) : (\n                                                                            <div className=\"grid grid-cols-2 gap-6\">\n                                                                                <div className=\"space-y-2\">\n                                                                                    <Label className=\"text-base\">From\n                                                                                        Tag/Release</Label>\n                                                                                    <Select\n                                                                                        value={options.fromRef}\n                                                                                        onValueChange={(value) => updateOptions({fromRef: value})}\n                                                                                    >\n                                                                                        <SelectTrigger className=\"h-12\">\n                                                                                            <SelectValue\n                                                                                                placeholder=\"Select starting point\"/>\n                                                                                        </SelectTrigger>\n                                                                                        <SelectContent>\n                                                                                            {releases.map(release => (\n                                                                                                <SelectItem\n                                                                                                    key={`release-from-${release.tagName}`}\n                                                                                                    value={release.tagName}>\n                                                                                                    📦 {release.name} ({release.tagName})\n                                                                                                </SelectItem>\n                                                                                            ))}\n                                                                                            {tags.map(tag => (\n                                                                                                <SelectItem\n                                                                                                    key={`tag-from-${tag.name}`}\n                                                                                                    value={tag.name}>\n                                                                                                    🏷️ {tag.name}\n                                                                                                </SelectItem>\n                                                                                            ))}\n                                                                                        </SelectContent>\n                                                                                    </Select>\n                                                                                </div>\n                                                                                <div className=\"space-y-2\">\n                                                                                    <Label className=\"text-base\">To\n                                                                                        Tag/Release</Label>\n                                                                                    <Select\n                                                                                        value={options.toRef}\n                                                                                        onValueChange={(value) => updateOptions({toRef: value})}\n                                                                                    >\n                                                                                        <SelectTrigger className=\"h-12\">\n                                                                                            <SelectValue\n                                                                                                placeholder=\"Select ending point\"/>\n                                                                                        </SelectTrigger>\n                                                                                        <SelectContent>\n                                                                                            <SelectItem value=\"HEAD\">🔥\n                                                                                                Latest (HEAD)</SelectItem>\n                                                                                            {releases.map(release => (\n                                                                                                <SelectItem\n                                                                                                    key={`release-to-${release.tagName}`}\n                                                                                                    value={release.tagName}>\n                                                                                                    📦 {release.name} ({release.tagName})\n                                                                                                </SelectItem>\n                                                                                            ))}\n                                                                                            {tags.map(tag => (\n                                                                                                <SelectItem\n                                                                                                    key={`tag-to-${tag.name}`}\n                                                                                                    value={tag.name}>\n                                                                                                    🏷️ {tag.name}\n                                                                                                </SelectItem>\n                                                                                            ))}\n                                                                                        </SelectContent>\n                                                                                    </Select>\n                                                                                </div>\n                                                                            </div>\n                                                                        )}\n                                                                    </CardContent>\n                                                                </Card>\n                                                            </motion.div>\n                                                        )}\n\n                                                        {options.method === 'between_commits' && (\n                                                            <motion.div\n                                                                key=\"commits\"\n                                                                initial={{opacity: 0, y: 20}}\n                                                                animate={{opacity: 1, y: 0}}\n                                                                exit={{opacity: 0, y: -20}}\n                                                            >\n                                                                <Card>\n                                                                    <CardHeader>\n                                                                        <CardTitle className=\"flex items-center gap-2\">\n                                                                            <GitBranch className=\"h-5 w-5\"/>\n                                                                            Specify Commit Range\n                                                                        </CardTitle>\n                                                                    </CardHeader>\n                                                                    <CardContent className=\"space-y-4\">\n                                                                        <div className=\"grid grid-cols-2 gap-6\">\n                                                                            <div className=\"space-y-2\">\n                                                                                <Label htmlFor=\"fromCommit\"\n                                                                                       className=\"text-base\">From\n                                                                                    Commit/Branch</Label>\n                                                                                <Input\n                                                                                    id=\"fromCommit\"\n                                                                                    value={options.fromRef}\n                                                                                    onChange={(e) => updateOptions({fromRef: e.target.value})}\n                                                                                    placeholder=\"abc123... or branch-name\"\n                                                                                    className=\"h-12\"\n                                                                                />\n                                                                            </div>\n                                                                            <div className=\"space-y-2\">\n                                                                                <Label htmlFor=\"toCommit\"\n                                                                                       className=\"text-base\">To\n                                                                                    Commit/Branch</Label>\n                                                                                <Input\n                                                                                    id=\"toCommit\"\n                                                                                    value={options.toRef}\n                                                                                    onChange={(e) => updateOptions({toRef: e.target.value})}\n                                                                                    placeholder=\"def456... or main\"\n                                                                                    className=\"h-12\"\n                                                                                />\n                                                                            </div>\n                                                                        </div>\n                                                                        <p className=\"text-sm text-muted-foreground\">\n                                                                            Use commit SHAs (full or short) or branch names.\n                                                                            Leave &ldquo;To&rdquo; as &ldquo;main&rdquo; or &ldquo;HEAD&rdquo; for\n                                                                            latest.\n                                                                        </p>\n                                                                    </CardContent>\n                                                                </Card>\n                                                            </motion.div>\n                                                        )}\n                                                    </AnimatePresence>\n                                                </div>\n                                            </motion.div>\n                                        )}\n\n                                        {/* Step 2: AI & Options */}\n                                        {currentStep === 2 && (\n                                            <motion.div\n                                                key=\"step2\"\n                                                initial={{opacity: 0, x: 20}}\n                                                animate={{opacity: 1, x: 0}}\n                                                exit={{opacity: 0, x: -20}}\n                                                className=\"space-y-8\"\n                                            >\n                                                <div>\n                                                    <h2 className=\"text-2xl font-bold mb-2\">Configure Options</h2>\n                                                    <p className=\"text-muted-foreground\">Customize how your changelog will\n                                                        be generated</p>\n                                                </div>\n\n                                                <div className=\"grid grid-cols-2 gap-8\">\n                                                    {/* AI Enhancement */}\n                                                    <Card>\n                                                        <CardHeader>\n                                                            <CardTitle className=\"flex items-center gap-2\">\n                                                                <Brain className=\"h-5 w-5\"/>\n                                                                AI Enhancement\n                                                            </CardTitle>\n                                                            <CardDescription>\n                                                                Improve commit messages and categorization with AI\n                                                            </CardDescription>\n                                                        </CardHeader>\n                                                        <CardContent className=\"space-y-6\">\n                                                            <div\n                                                                className=\"flex items-center justify-between p-4 border rounded-lg\">\n                                                                <div className=\"space-y-1\">\n                                                                    <Label className=\"text-base font-medium\">Enable AI\n                                                                        Processing</Label>\n                                                                    <p className=\"text-sm text-muted-foreground\">\n                                                                        Uses advanced AI to enhance commit analysis\n                                                                    </p>\n                                                                    {!aiSettings.enableAIAssistant && (\n                                                                        <Badge variant=\"secondary\" className=\"text-xs\">\n                                                                            AI assistant not configured\n                                                                        </Badge>\n                                                                    )}\n                                                                </div>\n                                                                <Switch\n                                                                    checked={options.useAI}\n                                                                    disabled={!aiSettings.enableAIAssistant || !aiSettings.aiApiKey}\n                                                                    onCheckedChange={(checked) => updateOptions({useAI: checked})}\n                                                                    className=\"scale-125\"\n                                                                />\n                                                            </div>\n\n                                                            {options.useAI && aiSettings.enableAIAssistant && (\n                                                                <motion.div\n                                                                    initial={{opacity: 0, height: 0}}\n                                                                    animate={{opacity: 1, height: 'auto'}}\n                                                                    className=\"space-y-4\"\n                                                                >\n                                                                    <Separator/>\n\n                                                                    <div\n                                                                        className=\"flex items-center justify-between p-4 border rounded-lg\">\n                                                                        <div className=\"space-y-1\">\n                                                                            <Label\n                                                                                className=\"text-base font-medium flex items-center gap-2\">\n                                                                                <Code2 className=\"h-4 w-4\"/>\n                                                                                Deep Code Analysis\n                                                                            </Label>\n                                                                            <p className=\"text-sm text-muted-foreground\">\n                                                                                Analyze file changes for better\n                                                                                categorization\n                                                                            </p>\n                                                                        </div>\n                                                                        <Switch\n                                                                            checked={options.includeCodeAnalysis}\n                                                                            onCheckedChange={(checked) => updateOptions({includeCodeAnalysis: checked})}\n                                                                            className=\"scale-125\"\n                                                                        />\n                                                                    </div>\n\n                                                                    {options.includeCodeAnalysis && (\n                                                                        <motion.div\n                                                                            initial={{opacity: 0, height: 0}}\n                                                                            animate={{opacity: 1, height: 'auto'}}\n                                                                            className=\"p-4 bg-muted/30 rounded-lg space-y-4\"\n                                                                        >\n                                                                            <Label className=\"text-base\">Maximum Commits to\n                                                                                Analyze</Label>\n                                                                            <div className=\"space-y-3\">\n                                                                                <div\n                                                                                    className=\"flex items-center justify-between\">\n                                                                                    <span\n                                                                                        className=\"text-2xl font-bold text-primary\">{options.maxCommitsToAnalyze}</span>\n                                                                                    <Badge variant=\"outline\">\n                                                                                        {options.maxCommitsToAnalyze <= 10 ? 'Fast' :\n                                                                                            options.maxCommitsToAnalyze <= 30 ? 'Balanced' : 'Thorough'}\n                                                                                    </Badge>\n                                                                                </div>\n                                                                                <Slider\n                                                                                    value={[options.maxCommitsToAnalyze]}\n                                                                                    onValueChange={(value) => updateOptions({maxCommitsToAnalyze: value[0]})}\n                                                                                    min={5}\n                                                                                    max={100}\n                                                                                    step={5}\n                                                                                    className=\"w-full\"\n                                                                                />\n                                                                                <div\n                                                                                    className=\"flex justify-between text-xs text-muted-foreground\">\n                                                                                    <span>5 (faster)</span>\n                                                                                    <span>100 (more detailed)</span>\n                                                                                </div>\n                                                                            </div>\n                                                                            <Alert icon={<Activity className=\"h-4 w-4\"/>}>\n                                                                                <AlertDescription>\n                                                                                    Higher values provide more detail but\n                                                                                    take longer to process.\n                                                                                </AlertDescription>\n                                                                            </Alert>\n                                                                        </motion.div>\n                                                                    )}\n                                                                </motion.div>\n                                                            )}\n\n                                                            {!aiSettings.enableAIAssistant && (\n                                                                <Alert>\n                                                                    <Info className=\"h-4 w-4\"/>\n                                                                    <AlertDescription>\n                                                                        AI assistant is not configured. Contact your\n                                                                        administrator to enable AI features.\n                                                                    </AlertDescription>\n                                                                </Alert>\n                                                            )}\n                                                        </CardContent>\n                                                    </Card>\n\n                                                    {/* Formatting Options */}\n                                                    <Card>\n                                                        <CardHeader>\n                                                            <CardTitle className=\"flex items-center gap-2\">\n                                                                <FileText className=\"h-5 w-5\"/>\n                                                                Format Settings\n                                                            </CardTitle>\n                                                            <CardDescription>\n                                                                Customize the changelog structure and style\n                                                            </CardDescription>\n                                                        </CardHeader>\n                                                        <CardContent className=\"space-y-6\">\n                                                            <div className=\"space-y-4\">\n                                                                <div\n                                                                    className=\"flex items-center justify-between p-4 border rounded-lg\">\n                                                                    <div className=\"space-y-1\">\n                                                                        <Label className=\"text-base font-medium\">Group by\n                                                                            Type</Label>\n                                                                        <p className=\"text-sm text-muted-foreground\">\n                                                                            Organize into Features, Bug Fixes, etc.\n                                                                        </p>\n                                                                    </div>\n                                                                    <Switch\n                                                                        checked={options.groupByType}\n                                                                        onCheckedChange={(checked) => updateOptions({groupByType: checked})}\n                                                                        className=\"scale-125\"\n                                                                    />\n                                                                </div>\n\n                                                                <div\n                                                                    className=\"flex items-center justify-between p-4 border rounded-lg\">\n                                                                    <div className=\"space-y-1\">\n                                                                        <Label className=\"text-base font-medium\">Include\n                                                                            Commit Links</Label>\n                                                                        <p className=\"text-sm text-muted-foreground\">\n                                                                            Add clickable GitHub commit links\n                                                                        </p>\n                                                                    </div>\n                                                                    <Switch\n                                                                        checked={options.includeCommitLinks}\n                                                                        onCheckedChange={(checked) => updateOptions({includeCommitLinks: checked})}\n                                                                        className=\"scale-125\"\n                                                                    />\n                                                                </div>\n                                                            </div>\n\n                                                            <div\n                                                                className=\"p-4 bg-green-50 dark:bg-green-950/20 rounded-lg\">\n                                                                <div className=\"flex items-start gap-3\">\n                                                                    <Check className=\"h-5 w-5 text-green-600 mt-0.5\"/>\n                                                                    <div className=\"space-y-1\">\n                                                                        <p className=\"font-medium text-green-900 dark:text-green-100\">Active\n                                                                            Features</p>\n                                                                        <div\n                                                                            className=\"text-sm text-green-700 dark:text-green-300 space-y-1\">\n                                                                            {options.groupByType &&\n                                                                                <p>✓ Grouped by change type</p>}\n                                                                            {options.includeCommitLinks &&\n                                                                                <p>✓ Clickable commit links</p>}\n                                                                            {options.useAI &&\n                                                                                <p>✓ AI-enhanced descriptions</p>}\n                                                                            {options.includeCodeAnalysis &&\n                                                                                <p>✓ Deep code analysis</p>}\n                                                                            {!options.groupByType && !options.includeCommitLinks && !options.useAI && !options.includeCodeAnalysis && (\n                                                                                <p className=\"text-muted-foreground\">Basic\n                                                                                    changelog generation</p>\n                                                                            )}\n                                                                        </div>\n                                                                    </div>\n                                                                </div>\n                                                            </div>\n                                                        </CardContent>\n                                                    </Card>\n                                                </div>\n                                            </motion.div>\n                                        )}\n\n                                        {/* Step 3: Results - FIXED SCROLLING SECTION */}\n                                        {currentStep === 3 && result && (\n                                            <motion.div\n                                                key=\"step3\"\n                                                initial={{opacity: 0, x: 20}}\n                                                animate={{opacity: 1, x: 0}}\n                                                exit={{opacity: 0, x: -20}}\n                                                className=\"space-y-6\"\n                                            >\n                                                <div>\n                                                    <h2 className=\"text-2xl font-bold mb-2\">Generated Changelog</h2>\n                                                    <p className=\"text-muted-foreground\">Your changelog content is ready to\n                                                        use</p>\n                                                </div>\n\n                                                <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6 h-[calc(100vh-12rem)]\">\n                                                    {/* Content Preview */}\n                                                    <Card className=\"lg:col-span-2 flex flex-col h-full\">\n                                                        <CardHeader className=\"flex-shrink-0\">\n                                                            <div className=\"flex items-center justify-between\">\n                                                                <CardTitle>Changelog Content</CardTitle>\n                                                                <div className=\"flex gap-2\">\n                                                                    <Tooltip>\n                                                                        <TooltipTrigger asChild>\n                                                                            <Button\n                                                                                variant=\"outline\"\n                                                                                size=\"sm\"\n                                                                                onClick={copyToClipboard}\n                                                                                className=\"gap-2\"\n                                                                            >\n                                                                                {copied ? (\n                                                                                    <>\n                                                                                        <Check className=\"h-4 w-4\"/>\n                                                                                        Copied!\n                                                                                    </>\n                                                                                ) : (\n                                                                                    <>\n                                                                                        <Copy className=\"h-4 w-4\"/>\n                                                                                        Copy\n                                                                                    </>\n                                                                                )}\n                                                                            </Button>\n                                                                        </TooltipTrigger>\n                                                                        <TooltipContent>Copy to clipboard</TooltipContent>\n                                                                    </Tooltip>\n                                                                </div>\n                                                            </div>\n                                                        </CardHeader>\n                                                        <CardContent className=\"flex-1 p-3 relative overflow-hidden\">\n                                                            <ScrollArea className=\"h-full w-full\">\n                                                                <div className=\"p-3\">\n                                                                    <pre className=\"whitespace-pre-wrap font-mono text-sm leading-relaxed text-foreground bg-muted/30 p-4 rounded-lg\">\n                                                                        {result.changelog?.content || 'Generated content will appear here...'}\n                                                                    </pre>\n                                                                </div>\n                                                            </ScrollArea>\n                                                            <div className=\"absolute bottom-6 right-6\">\n                                                                <Badge variant=\"secondary\" className=\"text-xs\">\n                                                                    {result.changelog?.content?.length || 0} chars\n                                                                </Badge>\n                                                            </div>\n                                                        </CardContent>\n                                                    </Card>\n\n                                                    {/* Entries Preview - PROPERLY SCROLLABLE */}\n                                                    <Card className=\"flex flex-col h-full\">\n                                                        <CardHeader className=\"flex-shrink-0\">\n                                                            <CardTitle className=\"text-lg\">Entry Breakdown</CardTitle>\n                                                            <CardDescription>\n                                                                Overview of generated entries\n                                                            </CardDescription>\n                                                        </CardHeader>\n                                                        <CardContent className=\"flex-1 overflow-hidden p-0\">\n                                                            {result.changelog?.entries && result.changelog.entries.length > 0 ? (\n                                                                <ScrollArea className=\"h-full w-full\">\n                                                                    <div className=\"p-4 space-y-3\">\n                                                                        {result.changelog.entries.map((entry, index) => (\n                                                                            <div key={index}\n                                                                                 className=\"p-3 border rounded-lg space-y-2 bg-background\">\n                                                                                <div\n                                                                                    className=\"flex items-center justify-between\">\n                                                                                    <Badge variant=\"outline\"\n                                                                                           className=\"text-xs\">{entry.category}</Badge>\n                                                                                    <span\n                                                                                        className=\"text-xs text-muted-foreground\">#{entry.commit.slice(0, 7)}</span>\n                                                                                </div>\n                                                                                <p className=\"text-sm font-medium\">{entry.description}</p>\n                                                                                {entry.impact && (\n                                                                                    <p className=\"text-xs text-muted-foreground\">\n                                                                                        {entry.impact}\n                                                                                    </p>\n                                                                                )}\n                                                                                {entry.files && entry.files.length > 0 && (\n                                                                                    <div className=\"flex flex-wrap gap-1 mt-2\">\n                                                                                        {entry.files.slice(0, 3).map((file, fileIndex) => (\n                                                                                            <Badge key={fileIndex}\n                                                                                                   variant=\"secondary\"\n                                                                                                   className=\"text-xs px-1 py-0\">\n                                                                                                {file.split('/').pop()}\n                                                                                            </Badge>\n                                                                                        ))}\n                                                                                        {entry.files.length > 3 && (\n                                                                                            <Badge variant=\"secondary\"\n                                                                                                   className=\"text-xs px-1 py-0\">\n                                                                                                +{entry.files.length - 3} more\n                                                                                            </Badge>\n                                                                                        )}\n                                                                                    </div>\n                                                                                )}\n                                                                            </div>\n                                                                        ))}\n                                                                    </div>\n                                                                </ScrollArea>\n                                                            ) : (\n                                                                <div className=\"flex-1 flex items-center justify-center p-6\">\n                                                                    <p className=\"text-sm text-muted-foreground text-center\">\n                                                                        No detailed entries available\n                                                                    </p>\n                                                                </div>\n                                                            )}\n                                                        </CardContent>\n                                                    </Card>\n                                                </div>\n                                            </motion.div>\n                                        )}\n                                    </AnimatePresence>\n                                </div>\n                            </ScrollArea>\n                        </div>\n                    </div>\n                </DialogContent>\n            </Dialog>\n        </TooltipProvider>\n    );\n}"
  },
  {
    "path": "components/github/GitHubIntegrationSettings.tsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport {\n    Settings,\n    TestTube,\n    CheckCircle,\n    XCircle,\n    Loader2,\n    Eye,\n    EyeOff,\n    Save,\n    Trash2,\n    Link,\n    RefreshCw,\n    ExternalLink\n} from 'lucide-react';\nimport {SiGithub} from '@icons-pack/react-simple-icons';\nimport { appInfo } from '@/lib/app-info';\n\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport { Switch } from '@/components/ui/switch';\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Badge } from '@/components/ui/badge';\nimport { Separator } from '@/components/ui/separator';\nimport { Alert, AlertDescription } from '@/components/ui/alert';\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';\nimport {\n    Tooltip,\n    TooltipContent,\n    TooltipProvider,\n    TooltipTrigger,\n} from '@/components/ui/tooltip';\n\ninterface GitHubIntegration {\n    enabled: boolean;\n    repositoryUrl: string;\n    defaultBranch: string;\n    autoGenerate: boolean;\n    includeBreakingChanges: boolean;\n    includeFixes: boolean;\n    includeFeatures: boolean;\n    includeChores: boolean;\n    customCommitTypes: string[];\n    lastSyncAt: string | null;\n    lastCommitSha: string | null;\n    hasAccessToken: boolean;\n}\n\ninterface RepositoryInfo {\n    name: string;\n    fullName: string;\n    description: string;\n    private: boolean;\n    defaultBranch: string;\n    language: string;\n    stargazersCount: number;\n    forksCount: number;\n}\n\ninterface TestResult {\n    success: boolean;\n    repository?: RepositoryInfo;\n    user?: {\n        login: string;\n        id: number;\n        avatarUrl: string;\n    };\n    error?: string;\n}\n\n// Default settings for when no integration exists\nconst DEFAULT_SETTINGS: GitHubIntegration = {\n    enabled: false,\n    repositoryUrl: '',\n    defaultBranch: 'main',\n    autoGenerate: false,\n    includeBreakingChanges: true,\n    includeFixes: true,\n    includeFeatures: true,\n    includeChores: false,\n    customCommitTypes: ['docs', 'style', 'refactor', 'perf'],\n    lastSyncAt: null,\n    lastCommitSha: null,\n    hasAccessToken: false\n};\n\nexport default function GitHubIntegrationSettings({ projectId, projectName }: { projectId: string; projectName: string }) {\n    // State management\n    const [settings, setSettings] = useState<GitHubIntegration>(DEFAULT_SETTINGS);\n    const [accessToken, setAccessToken] = useState('');\n    const [showToken, setShowToken] = useState(false);\n    const [isLoading, setIsLoading] = useState(true);\n    const [isSaving, setIsSaving] = useState(false);\n    const [isTesting, setIsTesting] = useState(false);\n    const [isDeleting, setIsDeleting] = useState(false);\n    const [testResult, setTestResult] = useState<TestResult | null>(null);\n    const [error, setError] = useState<string | null>(null);\n    const [success, setSuccess] = useState<string | null>(null);\n\n    // Load current settings\n    useEffect(() => {\n        loadSettings();\n    }, [projectId]);\n\n    const loadSettings = async () => {\n        try {\n            setIsLoading(true);\n            setError(null);\n\n            const response = await fetch(`/api/projects/${projectId}/integrations/github`);\n\n            if (response.status === 404) {\n                // No integration configured yet - use defaults\n                setSettings(DEFAULT_SETTINGS);\n                return;\n            }\n\n            if (!response.ok) {\n                throw new Error('Failed to load GitHub integration settings');\n            }\n\n            const data = await response.json();\n\n            // Handle case where response is null (no integration)\n            if (!data) {\n                setSettings(DEFAULT_SETTINGS);\n                return;\n            }\n\n            // Merge with defaults to ensure all fields are present\n            setSettings({\n                ...DEFAULT_SETTINGS,\n                ...data\n            });\n        } catch (err) {\n            console.error('Error loading GitHub settings:', err);\n            setError(err instanceof Error ? err.message : 'Failed to load settings');\n            // Set defaults on error too\n            setSettings(DEFAULT_SETTINGS);\n        } finally {\n            setIsLoading(false);\n        }\n    };\n\n    // Test GitHub connection\n    const testConnection = async () => {\n        if (!settings.repositoryUrl || !accessToken) {\n            setError('Repository URL and access token are required');\n            return;\n        }\n\n        try {\n            setIsTesting(true);\n            setTestResult(null);\n            setError(null);\n\n            const response = await fetch(`/api/projects/${projectId}/integrations/github/test`, {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({\n                    repositoryUrl: settings.repositoryUrl,\n                    accessToken\n                })\n            });\n\n            const result = await response.json();\n            setTestResult(result);\n\n            if (result.success && result.repository) {\n                // Update default branch from repository\n                setSettings(prev => ({\n                    ...prev,\n                    defaultBranch: result.repository!.defaultBranch\n                }));\n            }\n\n        } catch (err) {\n            console.error('Test connection error:', err);\n            setTestResult({\n                success: false,\n                error: err instanceof Error ? err.message : 'Test failed'\n            });\n        } finally {\n            setIsTesting(false);\n        }\n    };\n\n    // Save settings\n    const saveSettings = async () => {\n        if (!settings.repositoryUrl.trim()) {\n            setError('Repository URL is required');\n            return;\n        }\n\n        if (!accessToken.trim() && !settings.hasAccessToken) {\n            setError('Access token is required');\n            return;\n        }\n\n        try {\n            setIsSaving(true);\n            setError(null);\n\n            const payload = {\n                ...settings,\n                ...(accessToken.trim() && { accessToken: accessToken.trim() }) // Only include if provided\n            };\n\n            console.log('Saving GitHub integration with payload:', {\n                ...payload,\n                accessToken: payload.accessToken ? '[REDACTED]' : undefined\n            });\n\n            const response = await fetch(`/api/projects/${projectId}/integrations/github`, {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify(payload)\n            });\n\n            if (!response.ok) {\n                const errorData = await response.json();\n                console.error('Save failed:', errorData);\n                throw new Error(errorData.error || errorData.details || 'Failed to save settings');\n            }\n\n            const updatedSettings = await response.json();\n            setSettings({\n                ...DEFAULT_SETTINGS,\n                ...updatedSettings\n            });\n            setAccessToken(''); // Clear the input\n            setSuccess('GitHub integration configured successfully!');\n\n            // Clear success message after 3 seconds\n            setTimeout(() => setSuccess(null), 3000);\n\n        } catch (err) {\n            console.error('Save settings error:', err);\n            setError(err instanceof Error ? err.message : 'Failed to save settings');\n        } finally {\n            setIsSaving(false);\n        }\n    };\n\n    // Delete integration\n    const deleteIntegration = async () => {\n        try {\n            setIsDeleting(true);\n            setError(null);\n\n            const response = await fetch(`/api/projects/${projectId}/integrations/github`, {\n                method: 'DELETE'\n            });\n\n            if (!response.ok) {\n                const errorData = await response.json();\n                throw new Error(errorData.error || 'Failed to delete integration');\n            }\n\n            // Reset to default state\n            setSettings(DEFAULT_SETTINGS);\n            setAccessToken('');\n            setTestResult(null);\n            setSuccess('GitHub integration removed successfully!');\n\n            // Clear success message after 3 seconds\n            setTimeout(() => setSuccess(null), 3000);\n\n        } catch (err) {\n            console.error('Delete integration error:', err);\n            setError(err instanceof Error ? err.message : 'Failed to delete integration');\n        } finally {\n            setIsDeleting(false);\n        }\n    };\n\n    // Handle settings changes safely\n    const updateSettings = (updates: Partial<GitHubIntegration>) => {\n        setSettings(prev => ({\n            ...prev,\n            ...updates\n        }));\n    };\n\n    // Generate GitHub token creation URL with pre-filled parameters\n    const getGitHubTokenUrl = (): string => {\n        const description = `${appInfo.name} GitHub Integration: Project ${projectName}`;\n        const scopes = 'repo';\n        const encodedDescription = encodeURIComponent(description);\n        return `https://github.com/settings/tokens/new?description=${encodedDescription}&scopes=${scopes}`;\n    };\n\n    const openGitHubTokenCreation = () => {\n        window.open(getGitHubTokenUrl(), '_blank');\n    };\n\n    if (isLoading) {\n        return (\n            <div className=\"flex items-center justify-center p-8\">\n                <Loader2 className=\"h-8 w-8 animate-spin\" />\n            </div>\n        );\n    }\n\n    return (\n        <div className=\"space-y-6\">\n            {/* Header */}\n            <div className=\"flex items-center gap-3\">\n                <div className=\"p-2 bg-primary/10 rounded-lg\">\n                    <SiGithub className=\"h-5 w-5 text-primary\" />\n                </div>\n                <div>\n                    <h2 className=\"text-xl font-semibold\">GitHub Integration</h2>\n                    <p className=\"text-sm text-muted-foreground\">\n                        Generate changelog content from your GitHub repository commits\n                    </p>\n                </div>\n            </div>\n\n            {/* Alerts */}\n            <AnimatePresence>\n                {error && (\n                    <motion.div\n                        initial={{ opacity: 0, y: -10 }}\n                        animate={{ opacity: 1, y: 0 }}\n                        exit={{ opacity: 0, y: -10 }}\n                    >\n                        <Alert variant=\"destructive\">\n                            <AlertDescription>{error}</AlertDescription>\n                        </Alert>\n                    </motion.div>\n                )}\n\n                {success && (\n                    <motion.div\n                        initial={{ opacity: 0, y: -10 }}\n                        animate={{ opacity: 1, y: 0 }}\n                        exit={{ opacity: 0, y: -10 }}\n                    >\n                        <Alert className=\"border-green-200 bg-green-50 text-green-900 dark:border-green-800 dark:bg-green-950/30 dark:text-green-400\">\n                            <AlertDescription>{success}</AlertDescription>\n                        </Alert>\n                    </motion.div>\n                )}\n            </AnimatePresence>\n\n            <Tabs defaultValue=\"setup\" className=\"w-full\">\n                <TabsList className=\"grid w-full grid-cols-2\">\n                    <TabsTrigger value=\"setup\">Setup & Configuration</TabsTrigger>\n                    <TabsTrigger value=\"preferences\">Content Preferences</TabsTrigger>\n                </TabsList>\n\n                <TabsContent value=\"setup\" className=\"space-y-6\">\n                    {/* Repository Configuration */}\n                    <Card>\n                        <CardHeader>\n                            <CardTitle className=\"flex items-center gap-2\">\n                                <Link className=\"h-4 w-4\" />\n                                Repository Configuration\n                            </CardTitle>\n                            <CardDescription>\n                                Connect your GitHub repository to generate changelog content from commits\n                            </CardDescription>\n                        </CardHeader>\n                        <CardContent className=\"space-y-4\">\n                            {/* Repository URL */}\n                            <div className=\"space-y-2\">\n                                <Label htmlFor=\"repositoryUrl\">Repository URL</Label>\n                                <Input\n                                    id=\"repositoryUrl\"\n                                    placeholder=\"https://github.com/username/repository\"\n                                    value={settings.repositoryUrl || ''}\n                                    onChange={(e) => updateSettings({ repositoryUrl: e.target.value })}\n                                />\n                                <p className=\"text-xs text-muted-foreground\">\n                                    Enter the full GitHub repository URL (e.g., https://github.com/owner/repo)\n                                </p>\n                            </div>\n\n                            {/* Access Token */}\n                            <div className=\"space-y-2\">\n                                <Label htmlFor=\"accessToken\">Personal Access Token</Label>\n                                <div className=\"flex gap-2\">\n                                    <div className=\"relative flex-1\">\n                                        <Input\n                                            id=\"accessToken\"\n                                            type={showToken ? \"text\" : \"password\"}\n                                            placeholder={settings.hasAccessToken ? \"••••••••••••••••\" : \"ghp_xxxxxxxxxxxxxxxxxxxx\"}\n                                            value={accessToken}\n                                            onChange={(e) => setAccessToken(e.target.value)}\n                                        />\n                                        <Button\n                                            type=\"button\"\n                                            variant=\"ghost\"\n                                            size=\"icon\"\n                                            className=\"absolute right-0 top-0 h-full px-3\"\n                                            onClick={() => setShowToken(!showToken)}\n                                        >\n                                            {showToken ? <EyeOff className=\"h-4 w-4\" /> : <Eye className=\"h-4 w-4\" />}\n                                        </Button>\n                                    </div>\n                                    <TooltipProvider>\n                                        <Tooltip>\n                                            <TooltipTrigger asChild>\n                                                <Button\n                                                    variant=\"outline\"\n                                                    onClick={openGitHubTokenCreation}\n                                                    title=\"Create a new GitHub personal access token with pre-filled parameters\"\n                                                >\n                                                    <ExternalLink className=\"h-4 w-4\" />\n                                                </Button>\n                                            </TooltipTrigger>\n                                            <TooltipContent>Create Token on GitHub</TooltipContent>\n                                        </Tooltip>\n                                        <Tooltip>\n                                            <TooltipTrigger asChild>\n                                                <Button\n                                                    variant=\"outline\"\n                                                    onClick={testConnection}\n                                                    disabled={isTesting || !settings.repositoryUrl || !accessToken}\n                                                >\n                                                    {isTesting ? (\n                                                        <Loader2 className=\"h-4 w-4 animate-spin\" />\n                                                    ) : (\n                                                        <TestTube className=\"h-4 w-4\" />\n                                                    )}\n                                                </Button>\n                                            </TooltipTrigger>\n                                            <TooltipContent>Test Connection</TooltipContent>\n                                        </Tooltip>\n                                    </TooltipProvider>\n                                </div>\n                                <p className=\"text-xs text-muted-foreground\">\n                                    Generate a Personal Access Token in GitHub Settings → Developer settings → Personal access tokens.\n                                    Requires &apos;repo&apos; scope for private repositories or &apos;public_repo&apos; for public ones.\n                                </p>\n                                {settings.hasAccessToken && !accessToken && (\n                                    <p className=\"text-xs text-green-600\">\n                                        ✓ Access token is configured. Leave empty to keep existing token.\n                                    </p>\n                                )}\n                            </div>\n\n                            {/* Test Results */}\n                            <AnimatePresence>\n                                {testResult && (\n                                    <motion.div\n                                        initial={{ opacity: 0, height: 0 }}\n                                        animate={{ opacity: 1, height: 'auto' }}\n                                        exit={{ opacity: 0, height: 0 }}\n                                    >\n                                        <Card className={testResult.success\n                                            ? 'border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950/30'\n                                            : 'border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950/30'\n                                        }>\n                                            <CardContent className=\"p-4\">\n                                                {testResult.success ? (\n                                                    <div className=\"space-y-3\">\n                                                        <div className=\"flex items-center gap-2 text-green-700 dark:text-green-400\">\n                                                            <CheckCircle className=\"h-4 w-4\" />\n                                                            <span className=\"font-medium\">Connection successful!</span>\n                                                        </div>\n                                                        {testResult.repository && (\n                                                            <div className=\"space-y-2 text-sm\">\n                                                                <div className=\"grid grid-cols-2 gap-4\">\n                                                                    <div>\n                                                                        <span className=\"font-medium\">Repository:</span> {testResult.repository.fullName}\n                                                                    </div>\n                                                                    <div>\n                                                                        <span className=\"font-medium\">Default Branch:</span> {testResult.repository.defaultBranch}\n                                                                    </div>\n                                                                    <div>\n                                                                        <span className=\"font-medium\">Language:</span> {testResult.repository.language || 'Not specified'}\n                                                                    </div>\n                                                                    <div>\n                                                                        <span className=\"font-medium\">Visibility:</span> {testResult.repository.private ? 'Private' : 'Public'}\n                                                                    </div>\n                                                                </div>\n                                                                {testResult.repository.description && (\n                                                                    <p className=\"text-muted-foreground\">{testResult.repository.description}</p>\n                                                                )}\n                                                            </div>\n                                                        )}\n                                                    </div>\n                                                ) : (\n                                                    <div className=\"flex items-center gap-2 text-red-700 dark:text-red-400\">\n                                                        <XCircle className=\"h-4 w-4\" />\n                                                        <span>{testResult.error || 'Connection failed'}</span>\n                                                    </div>\n                                                )}\n                                            </CardContent>\n                                        </Card>\n                                    </motion.div>\n                                )}\n                            </AnimatePresence>\n\n                            {/* Default Branch */}\n                            <div className=\"space-y-2\">\n                                <Label htmlFor=\"defaultBranch\">Default Branch</Label>\n                                <Input\n                                    id=\"defaultBranch\"\n                                    placeholder=\"main\"\n                                    value={settings.defaultBranch || 'main'}\n                                    onChange={(e) => updateSettings({ defaultBranch: e.target.value })}\n                                />\n                                <p className=\"text-xs text-muted-foreground\">\n                                    The default branch to use for commit analysis (usually &apos;main&apos; or &apos;master&apos;)\n                                </p>\n                            </div>\n\n                            {/* Enable Integration */}\n                            <div className=\"flex items-center justify-between p-4 border rounded-lg\">\n                                <div className=\"space-y-1\">\n                                    <Label htmlFor=\"enabled\">Enable GitHub Integration</Label>\n                                    <p className=\"text-sm text-muted-foreground\">\n                                        Allow this project to generate changelog content from GitHub commits\n                                    </p>\n                                </div>\n                                <Switch\n                                    id=\"enabled\"\n                                    checked={settings.enabled}\n                                    onCheckedChange={(checked) => updateSettings({ enabled: checked })}\n                                />\n                            </div>\n                        </CardContent>\n                    </Card>\n\n                    {/* Status */}\n                    {settings.hasAccessToken && (\n                        <Card>\n                            <CardHeader>\n                                <CardTitle className=\"flex items-center gap-2\">\n                                    <Settings className=\"h-4 w-4\" />\n                                    Integration Status\n                                </CardTitle>\n                            </CardHeader>\n                            <CardContent>\n                                <div className=\"space-y-3\">\n                                    <div className=\"flex items-center justify-between\">\n                                        <span className=\"text-sm\">Status</span>\n                                        <Badge variant={settings.enabled ? \"default\" : \"secondary\"}>\n                                            {settings.enabled ? \"Enabled\" : \"Disabled\"}\n                                        </Badge>\n                                    </div>\n                                    {settings.lastSyncAt && (\n                                        <div className=\"flex items-center justify-between\">\n                                            <span className=\"text-sm\">Last Sync</span>\n                                            <span className=\"text-sm text-muted-foreground\">\n                                                {new Date(settings.lastSyncAt).toLocaleString()}\n                                            </span>\n                                        </div>\n                                    )}\n                                    {settings.lastCommitSha && (\n                                        <div className=\"flex items-center justify-between\">\n                                            <span className=\"text-sm\">Last Commit</span>\n                                            <code className=\"text-xs bg-muted px-2 py-1 rounded\">\n                                                {settings.lastCommitSha.substring(0, 7)}\n                                            </code>\n                                        </div>\n                                    )}\n                                </div>\n                            </CardContent>\n                        </Card>\n                    )}\n                </TabsContent>\n\n                <TabsContent value=\"preferences\" className=\"space-y-6\">\n                    {/* Content Generation Preferences */}\n                    <Card>\n                        <CardHeader>\n                            <CardTitle>Content Generation Preferences</CardTitle>\n                            <CardDescription>\n                                Configure which types of commits to include when generating changelog content\n                            </CardDescription>\n                        </CardHeader>\n                        <CardContent className=\"space-y-4\">\n                            {/* Commit Type Toggles */}\n                            <div className=\"grid grid-cols-2 gap-4\">\n                                <div className=\"flex items-center justify-between p-3 border rounded-lg\">\n                                    <div>\n                                        <Label>Breaking Changes</Label>\n                                        <p className=\"text-xs text-muted-foreground\">Include breaking changes</p>\n                                    </div>\n                                    <Switch\n                                        checked={settings.includeBreakingChanges}\n                                        onCheckedChange={(checked) => updateSettings({ includeBreakingChanges: checked })}\n                                    />\n                                </div>\n\n                                <div className=\"flex items-center justify-between p-3 border rounded-lg\">\n                                    <div>\n                                        <Label>Features</Label>\n                                        <p className=\"text-xs text-muted-foreground\">feat: new features</p>\n                                    </div>\n                                    <Switch\n                                        checked={settings.includeFeatures}\n                                        onCheckedChange={(checked) => updateSettings({ includeFeatures: checked })}\n                                    />\n                                </div>\n\n                                <div className=\"flex items-center justify-between p-3 border rounded-lg\">\n                                    <div>\n                                        <Label>Bug Fixes</Label>\n                                        <p className=\"text-xs text-muted-foreground\">fix: bug fixes</p>\n                                    </div>\n                                    <Switch\n                                        checked={settings.includeFixes}\n                                        onCheckedChange={(checked) => updateSettings({ includeFixes: checked })}\n                                    />\n                                </div>\n\n                                <div className=\"flex items-center justify-between p-3 border rounded-lg\">\n                                    <div>\n                                        <Label>Maintenance</Label>\n                                        <p className=\"text-xs text-muted-foreground\">chore: maintenance tasks</p>\n                                    </div>\n                                    <Switch\n                                        checked={settings.includeChores}\n                                        onCheckedChange={(checked) => updateSettings({ includeChores: checked })}\n                                    />\n                                </div>\n                            </div>\n\n                            <Separator />\n\n                            {/* Custom Commit Types */}\n                            <div className=\"space-y-3\">\n                                <Label>Additional Commit Types</Label>\n                                <div className=\"flex flex-wrap gap-2\">\n                                    {['docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci'].map((type) => (\n                                        <Badge\n                                            key={type}\n                                            variant={settings.customCommitTypes.includes(type) ? \"default\" : \"outline\"}\n                                            className=\"cursor-pointer\"\n                                            onClick={() => {\n                                                const newCustomTypes = settings.customCommitTypes.includes(type)\n                                                    ? settings.customCommitTypes.filter(t => t !== type)\n                                                    : [...settings.customCommitTypes, type];\n                                                updateSettings({ customCommitTypes: newCustomTypes });\n                                            }}\n                                        >\n                                            {type}\n                                        </Badge>\n                                    ))}\n                                </div>\n                                <p className=\"text-xs text-muted-foreground\">\n                                    Click to toggle inclusion of these conventional commit types\n                                </p>\n                            </div>\n                        </CardContent>\n                    </Card>\n                </TabsContent>\n            </Tabs>\n\n            {/* Action Buttons */}\n            <div className=\"flex items-center justify-between\">\n                <div>\n                    {settings.hasAccessToken && (\n                        <Button\n                            variant=\"destructive\"\n                            onClick={deleteIntegration}\n                            disabled={isDeleting}\n                        >\n                            {isDeleting ? (\n                                <Loader2 className=\"h-4 w-4 animate-spin mr-2\" />\n                            ) : (\n                                <Trash2 className=\"h-4 w-4 mr-2\" />\n                            )}\n                            Remove Integration\n                        </Button>\n                    )}\n                </div>\n\n                <div className=\"flex items-center gap-3\">\n                    <Button\n                        variant=\"outline\"\n                        onClick={loadSettings}\n                        disabled={isLoading}\n                    >\n                        <RefreshCw className=\"h-4 w-4 mr-2\" />\n                        Refresh\n                    </Button>\n\n                    <Button\n                        onClick={saveSettings}\n                        disabled={isSaving}\n                    >\n                        {isSaving ? (\n                            <Loader2 className=\"h-4 w-4 animate-spin mr-2\" />\n                        ) : (\n                            <Save className=\"h-4 w-4 mr-2\" />\n                        )}\n                        Save Settings\n                    </Button>\n                </div>\n            </div>\n        </div>\n    );\n}"
  },
  {
    "path": "components/loading-spinner.tsx",
    "content": "'use client'\n\nimport { Loader2 } from 'lucide-react'\n\nexport function LoadingSpinner() {\n    return (\n        <div className=\"container flex h-screen w-screen flex-col items-center justify-center\">\n            <div className=\"flex flex-col items-center justify-center space-y-4\">\n                <Loader2 className=\"h-10 w-10 animate-spin text-primary\" />\n                <p className=\"text-muted-foreground\">Loading...</p>\n            </div>\n        </div>\n    )\n}"
  },
  {
    "path": "components/markdown-editor/MarkdownEditor.tsx",
    "content": "// components/markdown-editor/MarkdownEditor.tsx\n\n'use client';\n\nimport React, {useState, useEffect, useRef, useCallback, useMemo} from 'react';\nimport {useDebounce} from 'use-debounce';\nimport {Badge} from '@/components/ui/badge';\nimport {\n    Bold,\n    Italic,\n    Link,\n    List,\n    ListOrdered,\n    Quote,\n    Code,\n    Heading1,\n    Heading2,\n    Heading3,\n    Image,\n    Zap\n} from 'lucide-react';\n\n// Import existing components\nimport MarkdownToolbar, {ToolbarGroup, ToolbarDropdown} from '@/components/markdown-editor/MarkdownToolbar';\n\n// Import our markdown renderer with custom extensions\nimport {renderMarkdown} from '@/lib/services/core/markdown/useCustomExtensions';\n\n// Import AI integration\nimport useAIAssistant from '@/hooks/useAIAssistant';\nimport {AICompletionType} from '@/lib/utils/ai/types';\nimport AIAssistantPanel from '@/components/markdown-editor/ai/AIAssistantPanel';\n\n// Import CUM modals\nimport {CUMButtonModal, CUMAlertModal, CUMEmbedModal, CUMTableModal} from '@/components/markdown-editor/modals';\nimport {useCUMModals} from '@/components/markdown-editor/hooks/useCUMModals';\n\nexport interface MarkdownEditorProps {\n    initialValue?: string;\n    onChange?: (value: string) => void;\n    onSave?: (value: string) => void;\n    onExport?: (value: string) => void;\n    placeholder?: string;\n    className?: string;\n    height?: string;\n    autoFocus?: boolean;\n    readOnly?: boolean;\n    enableAI?: boolean;\n    enableCUM?: boolean;\n    aiApiKey?: string;\n    maxLength?: number;\n}\n\nexport const MarkdownEditor: React.FC<MarkdownEditorProps> = ({\n                                                                  initialValue = '',\n                                                                  onChange,\n                                                                  onSave,\n                                                                  onExport,\n                                                                  placeholder = 'Start writing your markdown...',\n                                                                  className = '',\n                                                                  height = '400px',\n                                                                  autoFocus = false,\n                                                                  readOnly = false,\n                                                                  enableAI = false,\n                                                                  enableCUM = process.env.NEXT_PUBLIC_ENABLE_CUM !== 'false',\n                                                                  aiApiKey,\n                                                                  maxLength\n                                                              }) => {\n    // Core editor state\n    const [content, setContent] = useState(initialValue);\n    const [view, setView] = useState<'edit' | 'preview' | 'split'>('edit');\n    const [history, setHistory] = useState<string[]>([initialValue]);\n    const [historyIndex, setHistoryIndex] = useState(0);\n    const [wordCount, setWordCount] = useState(0);\n    const [charCount, setCharCount] = useState(0);\n    const [isSaved, setIsSaved] = useState(true);\n\n    const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n    // AI integration - ALWAYS call the hook\n    const ai = useAIAssistant({apiKey: aiApiKey});\n\n    // CUM modals state\n    const {modals, openModal, closeModal} = useCUMModals();\n\n    // Initialize content\n    useEffect(() => {\n        if (initialValue !== content) {\n            setContent(initialValue);\n            setHistory([initialValue]);\n            setHistoryIndex(0);\n        }\n    }, [initialValue, content]);\n\n    // Update metrics\n    useEffect(() => {\n        const words = content.trim() ? content.trim().split(/\\s+/).length : 0;\n        const chars = content.length;\n        setWordCount(words);\n        setCharCount(chars);\n    }, [content]);\n\n    // Auto focus\n    useEffect(() => {\n        if (autoFocus && textareaRef.current) {\n            textareaRef.current.focus();\n        }\n    }, [autoFocus]);\n\n    // Debounced content for preview rendering (300ms delay)\n    const [debouncedContent] = useDebounce(content, 300);\n\n    // History management with debouncing\n    const addToHistory = useCallback((newContent: string) => {\n        setHistory(prev => {\n            const newHistory = prev.slice(0, historyIndex + 1);\n            newHistory.push(newContent);\n            if (newHistory.length > 50) newHistory.shift();\n            return newHistory;\n        });\n        setHistoryIndex(prev => Math.min(prev + 1, 49));\n    }, [historyIndex]);\n\n    // Debounced history additions (2 seconds - only add to history when user pauses)\n    const [debouncedContentForHistory] = useDebounce(content, 2000);\n    useEffect(() => {\n        if (debouncedContentForHistory && debouncedContentForHistory !== history[historyIndex]) {\n            addToHistory(debouncedContentForHistory);\n        }\n    }, [debouncedContentForHistory, historyIndex, history, addToHistory]);\n\n    // Content change handler (no longer adds to history immediately)\n    const handleContentChange = useCallback((newContent: string) => {\n        setContent(newContent);\n        setIsSaved(false);\n        onChange?.(newContent);\n    }, [onChange]);\n\n    // Undo/Redo\n    const canUndo = historyIndex > 0;\n    const canRedo = historyIndex < history.length - 1;\n\n    const handleUndo = useCallback(() => {\n        if (canUndo) {\n            const newIndex = historyIndex - 1;\n            const newContent = history[newIndex];\n            setContent(newContent);\n            setHistoryIndex(newIndex);\n            onChange?.(newContent);\n        }\n    }, [canUndo, historyIndex, history, onChange]);\n\n    const handleRedo = useCallback(() => {\n        if (canRedo) {\n            const newIndex = historyIndex + 1;\n            const newContent = history[newIndex];\n            setContent(newContent);\n            setHistoryIndex(newIndex);\n            onChange?.(newContent);\n        }\n    }, [canRedo, historyIndex, history, onChange]);\n\n    // Text manipulation helpers\n    const insertAtCursor = useCallback((text: string) => {\n        const textarea = textareaRef.current;\n        if (!textarea) return;\n\n        const start = textarea.selectionStart;\n        const end = textarea.selectionEnd;\n        const newContent = content.slice(0, start) + text + content.slice(end);\n\n        handleContentChange(newContent);\n\n        setTimeout(() => {\n            textarea.selectionStart = textarea.selectionEnd = start + text.length;\n            textarea.focus();\n        }, 0);\n    }, [content, handleContentChange]);\n\n    const wrapSelection = useCallback((prefix: string, suffix: string) => {\n        const textarea = textareaRef.current;\n        if (!textarea) return;\n\n        const start = textarea.selectionStart;\n        const end = textarea.selectionEnd;\n        const selectedText = content.slice(start, end);\n        const replacement = prefix + selectedText + suffix;\n\n        const newContent = content.slice(0, start) + replacement + content.slice(end);\n        handleContentChange(newContent);\n\n        setTimeout(() => {\n            textarea.selectionStart = start + prefix.length;\n            textarea.selectionEnd = start + prefix.length + selectedText.length;\n            textarea.focus();\n        }, 0);\n    }, [content, handleContentChange]);\n\n    // Basic markdown action handlers\n    const handleBold = useCallback(() => wrapSelection('**', '**'), [wrapSelection]);\n    const handleItalic = useCallback(() => wrapSelection('*', '*'), [wrapSelection]);\n    const handleCode = useCallback(() => wrapSelection('`', '`'), [wrapSelection]);\n    const handleLink = useCallback(() => wrapSelection('[', '](url)'), [wrapSelection]);\n    const handleImage = useCallback(() => wrapSelection('![', '](url)'), [wrapSelection]);\n    const handleQuote = useCallback(() => insertAtCursor('\\n> '), [insertAtCursor]);\n    const handleBulletList = useCallback(() => insertAtCursor('\\n- '), [insertAtCursor]);\n    const handleNumberedList = useCallback(() => insertAtCursor('\\n1. '), [insertAtCursor]);\n    const handleHeading1 = useCallback(() => insertAtCursor('\\n# '), [insertAtCursor]);\n    const handleHeading2 = useCallback(() => insertAtCursor('\\n## '), [insertAtCursor]);\n    const handleHeading3 = useCallback(() => insertAtCursor('\\n### '), [insertAtCursor]);\n\n    // CUM modal handlers\n    const handleCUMButton = useCallback(() => openModal('button'), [openModal]);\n    const handleCUMAlert = useCallback(() => openModal('alert'), [openModal]);\n    const handleCUMEmbed = useCallback(() => openModal('embed'), [openModal]);\n    const handleCUMTable = useCallback(() => openModal('table'), [openModal]);\n\n    // Modal insertion handler\n    const handleModalInsert = useCallback((markdown: string) => {\n        insertAtCursor(`\\n${markdown}\\n`);\n    }, [insertAtCursor]);\n\n    // Save handler\n    const handleSave = useCallback(() => {\n        onSave?.(content);\n        setIsSaved(true);\n    }, [content, onSave]);\n\n    // Export handler\n    const handleExport = useCallback(() => {\n        onExport?.(content);\n    }, [content, onExport]);\n\n    // AI helper functions\n    const getContextForAI = useCallback(() => {\n        const textarea = textareaRef.current;\n        if (!textarea) return content;\n\n        const start = textarea.selectionStart;\n        const end = textarea.selectionEnd;\n\n        if (start !== end) {\n            return content.slice(start, end);\n        }\n\n        const lines = content.split('\\n');\n        const position = start;\n        let currentPos = 0;\n        let lineIndex = 0;\n\n        for (let i = 0; i < lines.length; i++) {\n            if (currentPos + lines[i].length >= position) {\n                lineIndex = i;\n                break;\n            }\n            currentPos += lines[i].length + 1;\n        }\n\n        const startLine = Math.max(0, lineIndex - 2);\n        const endLine = Math.min(lines.length, lineIndex + 3);\n        return lines.slice(startLine, endLine).join('\\n');\n    }, [content]);\n\n    const handleApplyAIContent = useCallback((text: string) => {\n        const textarea = textareaRef.current;\n        if (!textarea) return;\n\n        const start = textarea.selectionStart;\n        const end = textarea.selectionEnd;\n\n        let newContent;\n        let newCursorPos;\n\n        if (start !== end) {\n            newContent = content.slice(0, start) + text + content.slice(end);\n            newCursorPos = start + text.length;\n        } else {\n            newContent = content.slice(0, start) + text + content.slice(start);\n            newCursorPos = start + text.length;\n        }\n\n        handleContentChange(newContent);\n\n        setTimeout(() => {\n            textarea.focus();\n            textarea.selectionStart = newCursorPos;\n            textarea.selectionEnd = newCursorPos;\n        }, 0);\n    }, [content, handleContentChange]);\n\n    const handleGenerateAI = useCallback(() => {\n        if (!enableAI || !ai) return;\n\n        const contextText = getContextForAI();\n\n        if (!contextText.trim()) {\n            return;\n        }\n\n        ai.generateCompletion({\n            type: ai.state.completionType,\n            content: contextText,\n            customPrompt: ai.state.customPrompt,\n            options: {\n                temperature: ai.state.temperature\n            }\n        }).catch(error => {\n            console.error('Error generating AI content:', error);\n        });\n    }, [enableAI, ai, getContextForAI]);\n\n    // Keyboard shortcuts\n    const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n        // Save shortcut: Ctrl+S\n        if (e.ctrlKey && e.key === 's') {\n            e.preventDefault();\n            handleSave();\n        }\n\n        // Bold: Ctrl+B\n        if (e.ctrlKey && e.key === 'b') {\n            e.preventDefault();\n            handleBold();\n        }\n\n        // Italic: Ctrl+I\n        if (e.ctrlKey && e.key === 'i') {\n            e.preventDefault();\n            handleItalic();\n        }\n\n        // Link: Ctrl+K\n        if (e.ctrlKey && e.key === 'k') {\n            e.preventDefault();\n            handleLink();\n        }\n\n        // Undo: Ctrl+Z\n        if (e.ctrlKey && !e.shiftKey && e.key === 'z') {\n            e.preventDefault();\n            handleUndo();\n        }\n\n        // Redo: Ctrl+Shift+Z or Ctrl+Y\n        if ((e.ctrlKey && e.shiftKey && e.key === 'z') ||\n            (e.ctrlKey && e.key === 'y')) {\n            e.preventDefault();\n            handleRedo();\n        }\n\n        // Headings: Ctrl+1, Ctrl+2, Ctrl+3\n        if (e.ctrlKey && e.key === '1') {\n            e.preventDefault();\n            handleHeading1();\n        }\n        if (e.ctrlKey && e.key === '2') {\n            e.preventDefault();\n            handleHeading2();\n        }\n        if (e.ctrlKey && e.key === '3') {\n            e.preventDefault();\n            handleHeading3();\n        }\n\n        // AI Assistant: Alt+A\n        if (e.altKey && e.key === 'a' && enableAI && ai) {\n            e.preventDefault();\n            ai.openAssistant(AICompletionType.COMPLETE);\n        }\n    }, [\n        handleSave, handleUndo, handleRedo,\n        handleBold, handleItalic, handleLink,\n        handleHeading1, handleHeading2, handleHeading3,\n        enableAI, ai\n    ]);\n\n    // Render markdown (engine has built-in LRU caching, so no manual memoization needed)\n    // Still using useMemo for React optimization to prevent re-renders\n    const renderedHtml = useMemo(() => {\n        return renderMarkdown(debouncedContent);\n    }, [debouncedContent]);\n\n    // Create clean toolbar structure\n    const toolbarGroups: ToolbarGroup[] = [\n        {\n            name: 'Formatting',\n            actions: [\n                {\n                    icon: <Bold size={16}/>,\n                    label: 'Bold',\n                    onClick: handleBold,\n                    shortcut: 'Ctrl+B',\n                },\n                {\n                    icon: <Italic size={16}/>,\n                    label: 'Italic',\n                    onClick: handleItalic,\n                    shortcut: 'Ctrl+I',\n                },\n                {\n                    icon: <Code size={16}/>,\n                    label: 'Code',\n                    onClick: handleCode,\n                },\n                {\n                    icon: <Link size={16}/>,\n                    label: 'Link',\n                    onClick: handleLink,\n                    shortcut: 'Ctrl+K',\n                },\n            ],\n        },\n        {\n            name: 'Structure',\n            actions: [\n                {\n                    icon: <List size={16}/>,\n                    label: 'Bullet List',\n                    onClick: handleBulletList,\n                },\n                {\n                    icon: <ListOrdered size={16}/>,\n                    label: 'Numbered List',\n                    onClick: handleNumberedList,\n                },\n                {\n                    icon: <Quote size={16}/>,\n                    label: 'Blockquote',\n                    onClick: handleQuote,\n                },\n                {\n                    icon: <Image size={16}/>,\n                    label: 'Image',\n                    onClick: handleImage,\n                },\n            ],\n        },\n    ];\n\n    // Create toolbar dropdowns with CUM extensions\n    const toolbarDropdowns: ToolbarDropdown[] = [\n        {\n            name: 'Headings',\n            icon: <Heading2 size={16}/>,\n            actions: [\n                {\n                    icon: <Heading1 size={16}/>,\n                    label: 'Heading 1',\n                    onClick: handleHeading1,\n                    shortcut: 'Ctrl+1',\n                },\n                {\n                    icon: <Heading2 size={16}/>,\n                    label: 'Heading 2',\n                    onClick: handleHeading2,\n                    shortcut: 'Ctrl+2',\n                },\n                {\n                    icon: <Heading3 size={16}/>,\n                    label: 'Heading 3',\n                    onClick: handleHeading3,\n                    shortcut: 'Ctrl+3',\n                },\n            ],\n        },\n    ];\n\n    // Add CUM Extensions dropdown if enabled\n    if (enableCUM) {\n        toolbarDropdowns.push({\n            name: 'CUM Extensions',\n            icon: <Zap size={16}/>,\n            actions: [\n                {\n                    icon: <Zap size={16}/>,\n                    label: 'Button',\n                    onClick: handleCUMButton,\n                },\n                {\n                    icon: <Zap size={16}/>,\n                    label: 'Alert',\n                    onClick: handleCUMAlert,\n                },\n                {\n                    icon: <Zap size={16}/>,\n                    label: 'Embed',\n                    onClick: handleCUMEmbed,\n                },\n                {\n                    icon: <Zap size={16}/>,\n                    label: 'Table',\n                    onClick: handleCUMTable,\n                },\n            ],\n        });\n    }\n\n    return (\n        <div className={`flex flex-col border rounded-md shadow-sm bg-background ${className}`}>\n            {/* Toolbar */}\n            <MarkdownToolbar\n                groups={toolbarGroups}\n                dropdowns={toolbarDropdowns}\n                canUndo={canUndo}\n                canRedo={canRedo}\n                onUndo={handleUndo}\n                onRedo={handleRedo}\n                onSave={onSave ? handleSave : undefined}\n                onExport={onExport ? handleExport : undefined}\n                viewMode={view}\n                onViewModeChange={(mode: 'edit' | 'preview' | 'split') => setView(mode)}\n                onAIAssist={enableAI && ai ? () => ai.openAssistant?.(AICompletionType.COMPLETE) : undefined}\n                enableAI={enableAI}\n                onBold={handleBold}\n                onItalic={handleItalic}\n                onLink={handleLink}\n                onHeading1={handleHeading1}\n                onHeading2={handleHeading2}\n                onHeading3={handleHeading3}\n                onBulletList={handleBulletList}\n                onNumberedList={handleNumberedList}\n                onQuote={handleQuote}\n                onCode={handleCode}\n                onImage={handleImage}\n            />\n\n            {/* Editor content */}\n            <div className=\"flex flex-1\" style={{height}}>\n                {/* Edit mode */}\n                {(view === 'edit' || view === 'split') && (\n                    <div className={`flex flex-col ${view === 'split' ? 'w-1/2 border-r' : 'w-full'}`}>\n            <textarea\n                ref={textareaRef}\n                value={content}\n                onChange={(e) => handleContentChange(e.target.value)}\n                onKeyDown={handleKeyDown}\n                placeholder={placeholder}\n                className=\"w-full h-full p-4 font-mono text-sm border-0 bg-background focus-visible:ring-0 focus-visible:outline-none resize-none\"\n                readOnly={readOnly}\n                maxLength={maxLength}\n            />\n                    </div>\n                )}\n\n                {/* Preview mode */}\n                {(view === 'preview' || view === 'split') && (\n                    <div className={`flex flex-col overflow-auto ${view === 'split' ? 'w-1/2' : 'w-full'}`}>\n                        <div className=\"flex-1 p-4\">\n                            <div\n                                className=\"prose max-w-none prose-img:my-4 prose-headings:mt-6 prose-headings:mb-4 prose-p:mb-4 prose-pre:my-4 prose-blockquote:my-4\"\n                                dangerouslySetInnerHTML={{__html: renderedHtml}}\n                                suppressHydrationWarning\n                            />\n                        </div>\n                    </div>\n                )}\n            </div>\n\n            {/* Status bar with your updated styling */}\n            <div\n                className=\"flex items-center justify-between h-6 px-3 py-1 text-xs text-muted-foreground border-t bg-muted/20\">\n                <div className=\"flex items-center gap-4\">\n                    <span>{wordCount} words</span>\n                    <span>{charCount} characters</span>\n                    {maxLength && (\n                        <Badge variant={charCount > maxLength * 0.9 ? 'destructive' : 'secondary'}>\n                            {charCount}/{maxLength}\n                        </Badge>\n                    )}\n                    {!isSaved && <Badge variant=\"outline\">Unsaved</Badge>}\n                </div>\n\n                <div className=\"flex items-center gap-2\">\n                    {enableCUM && (\n                        <Badge variant=\"outline\" className=\"text-xs\">\n                            CUM Enabled\n                        </Badge>\n                    )}\n                    <span className=\"capitalize\">{view} mode</span>\n                </div>\n            </div>\n\n            {/* CUM Modals */}\n            {enableCUM && (\n                <>\n                    <CUMButtonModal\n                        isOpen={modals.buttonModal}\n                        onClose={() => closeModal('button')}\n                        onInsert={handleModalInsert}\n                    />\n                    <CUMAlertModal\n                        isOpen={modals.alertModal}\n                        onClose={() => closeModal('alert')}\n                        onInsert={handleModalInsert}\n                    />\n                    <CUMEmbedModal\n                        isOpen={modals.embedModal}\n                        onClose={() => closeModal('embed')}\n                        onInsert={handleModalInsert}\n                    />\n                    <CUMTableModal\n                        isOpen={modals.tableModal}\n                        onClose={() => closeModal('table')}\n                        onInsert={handleModalInsert}\n                    />\n                </>\n            )}\n\n            {/* AI Assistant Panel */}\n            {enableAI && ai && (\n                <AIAssistantPanel\n                    isVisible={ai.state.isVisible}\n                    content={getContextForAI()}\n                    completionType={ai.state.completionType}\n                    customPrompt={ai.state.customPrompt}\n                    isLoading={ai.state.isLoading}\n                    generatedResult={ai.state.lastResult}\n                    error={ai.state.error}\n                    temperature={ai.state.temperature}\n                    onTemperatureChange={ai.setTemperature}\n                    onClose={ai.closeAssistant}\n                    onCompletionTypeChange={ai.setCompletionType}\n                    onCustomPromptChange={ai.setCustomPrompt}\n                    onGenerate={handleGenerateAI}\n                    onApply={handleApplyAIContent}\n                    onRegenerate={handleGenerateAI}\n                    isApiKeyValid={ai.state.apiKeyValid}\n                    onSetApiKey={ai.setApiKey}\n                />\n            )}\n        </div>\n    );\n};"
  },
  {
    "path": "components/markdown-editor/MarkdownEditorArea.tsx",
    "content": "'use client';\n\nimport React, { useRef, useEffect, forwardRef } from 'react';\n\n/**\n * Editor area props\n */\nexport interface MarkdownEditorAreaProps {\n    value: string;\n    onChange: (value: string) => void;\n    onSelect?: (\n        selectionStart: number,\n        selectionEnd: number,\n        selectedText: string\n    ) => void;\n    onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;\n    placeholder?: string;\n    resizable?: boolean;\n    minRows?: number;\n    maxRows?: number;\n    className?: string;\n    disabled?: boolean;\n    autoFocus?: boolean;\n    onBold?: (text: string) => void;\n    onItalic?: (text: string) => void;\n    onLink?: (text: string) => void;\n    onCode?: (text: string) => void;\n    onQuote?: (text: string) => void;\n    onBulletList?: (text: string) => void;\n    onNumberedList?: (text: string) => void;\n    onHeading?: (text: string, level: number) => void;\n}\n\n/**\n * Simple and reliable Markdown editor text area component\n */\nconst MarkdownEditorArea = forwardRef<HTMLTextAreaElement, MarkdownEditorAreaProps>(\n    ({\n         value,\n         onChange,\n         onSelect,\n         onKeyDown,\n         placeholder = 'Start writing...',\n         resizable = false,\n         minRows = 5,\n         className = '',\n         disabled = false,\n         autoFocus = false,\n     }, ref) => {\n        const internalRef = useRef<HTMLTextAreaElement | null>(null);\n\n        // Handle ref forwarding\n        useEffect(() => {\n            if (!ref) return;\n\n            if (typeof ref === 'function') {\n                ref(internalRef.current);\n            } else {\n                ref.current = internalRef.current;\n            }\n        }, [ref]);\n\n        // Handle text change\n        const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n            onChange(e.target.value);\n        };\n\n        // Handle selection changes\n        const handleSelect = (e: React.SyntheticEvent<HTMLTextAreaElement>) => {\n            const target = e.target as HTMLTextAreaElement;\n            const start = target.selectionStart;\n            const end = target.selectionEnd;\n            const selectedText = value.substring(start, end);\n\n            if (onSelect) {\n                onSelect(start, end, selectedText);\n            }\n        };\n\n        // Apply styles\n        const textareaStyle: React.CSSProperties = {\n            resize: resizable ? 'vertical' : 'none',\n            overflow: 'auto',\n            lineHeight: '1.5',\n            minHeight: resizable ? '100px' : undefined,\n            maxHeight: resizable ? '800px' : undefined,\n            height: resizable ? 'auto' : undefined,\n            width: '100%',\n            tabSize: 2,\n        };\n\n        return (\n            <textarea\n                ref={internalRef}\n                value={value}\n                onChange={handleChange}\n                onSelect={handleSelect}\n                onKeyDown={onKeyDown}\n                placeholder={placeholder}\n                rows={minRows}\n                style={textareaStyle}\n                disabled={disabled}\n                autoFocus={autoFocus}\n                className={`w-full p-4 font-mono text-sm border-0 bg-background focus-visible:ring-0 focus-visible:outline-none focus:border-0 ${className}`}\n                spellCheck=\"false\"\n            />\n        );\n    }\n);\n\n// Display name for React DevTools\nMarkdownEditorArea.displayName = 'MarkdownEditorArea';\n\nexport default MarkdownEditorArea;"
  },
  {
    "path": "components/markdown-editor/MarkdownPreview.tsx",
    "content": "// components/markdown-editor/MarkdownPreview.tsx\n\n'use client';\n\nimport React, { useState, useEffect } from 'react';\nimport { renderMarkdown, renderMarkdownStreamed } from '@/lib/services/core/markdown/useCustomExtensions';\n\ninterface MarkdownPreviewProps {\n    content: string;\n    className?: string;\n}\n\nexport const MarkdownPreview: React.FC<MarkdownPreviewProps> = ({\n                                                                    content,\n                                                                    className = ''\n                                                                }) => {\n    const [html, setHtml] = useState('');\n\n    useEffect(() => {\n        const wordCount = content.trim() ? content.trim().split(/\\s+/).length : 0;\n\n        const renderContent = async () => {\n            if (wordCount > 10000) {\n                try {\n                    const rendered = await renderMarkdownStreamed(content);\n                    setHtml(rendered);\n                } catch (error) {\n                    console.error('Streaming render failed:', error);\n                    setHtml(renderMarkdown(content));\n                }\n            } else {\n                setHtml(renderMarkdown(content));\n            }\n        };\n\n        renderContent();\n    }, [content]);\n\n    return (\n        <div\n            className={`prose max-w-none prose-img:my-4 prose-headings:mt-6 prose-headings:mb-4 prose-p:mb-4 prose-pre:my-4 prose-blockquote:my-4 ${className}`}\n            dangerouslySetInnerHTML={{ __html: html }}\n            suppressHydrationWarning\n        />\n    );\n};"
  },
  {
    "path": "components/markdown-editor/MarkdownToolbar.tsx",
    "content": "// components/markdown-editor/MarkdownToolbar.tsx\n\n'use client';\n\nimport React, {memo, useState} from 'react';\nimport {\n    Bold,\n    ChevronDown,\n    Code,\n    FileDown,\n    Heading1,\n    Heading2,\n    Heading3,\n    Image,\n    Italic,\n    Link,\n    List,\n    ListOrdered,\n    Menu,\n    Quote,\n    RotateCcw,\n    RotateCw,\n    Save,\n    Sparkles,\n} from 'lucide-react';\n\nimport {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger,} from '@/components/ui/tooltip';\n\nimport {\n    DropdownMenu,\n    DropdownMenuContent,\n    DropdownMenuItem,\n    DropdownMenuLabel,\n    DropdownMenuSeparator,\n    DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\n\nimport {Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger,} from '@/components/ui/sheet';\n\nimport {Tabs, TabsList, TabsTrigger,} from '@/components/ui/tabs';\n\nimport {Button} from '@/components/ui/button';\nimport {Separator} from '@/components/ui/separator';\nimport {ScrollArea} from '@/components/ui/scroll-area';\n\nexport interface ToolbarAction {\n    icon: React.ReactNode;\n    label: string;\n    onClick: () => void;\n    shortcut?: string;\n    isActive?: boolean;\n    variant?: 'default' | 'outline' | 'ghost';\n}\n\nexport interface ToolbarGroup {\n    name: string;\n    actions: ToolbarAction[];\n}\n\nexport interface ToolbarDropdown {\n    name: string;\n    icon: React.ReactNode;\n    actions: ToolbarAction[];\n}\n\nexport interface MarkdownToolbarProps {\n    groups?: ToolbarGroup[];\n    dropdowns?: ToolbarDropdown[];\n    canUndo?: boolean;\n    canRedo?: boolean;\n    onUndo?: () => void;\n    onRedo?: () => void;\n    onSave?: () => void;\n    onExport?: () => void;\n    viewMode?: 'edit' | 'preview' | 'split';\n    onViewModeChange?: (mode: 'edit' | 'preview' | 'split') => void;\n    onAIAssist?: () => void;\n    enableAI?: boolean;\n    className?: string;\n\n    // Additional handlers for specific actions\n    onBold?: () => void;\n    onItalic?: () => void;\n    onLink?: () => void;\n    onHeading1?: () => void;\n    onHeading2?: () => void;\n    onHeading3?: () => void;\n    onBulletList?: () => void;\n    onNumberedList?: () => void;\n    onQuote?: () => void;\n    onCode?: () => void;\n    onImage?: () => void;\n}\n\n// Create stable action component to prevent re-renders\nconst ToolbarAction = memo(({action, isMobile = false}: { action: ToolbarAction; isMobile?: boolean }) => {\n    if (isMobile) {\n        return (\n            <Button\n                variant={action.variant || (action.isActive ? 'default' : 'ghost')}\n                onClick={action.onClick}\n                className=\"w-full justify-start h-14 text-left font-normal hover:bg-accent/50 transition-colors\"\n                type=\"button\"\n            >\n                <span className=\"mr-4 text-muted-foreground\">{action.icon}</span>\n                <span className=\"flex-1 text-foreground\">{action.label}</span>\n            </Button>\n        );\n    }\n\n    return (\n        <TooltipProvider>\n            <Tooltip>\n                <TooltipTrigger asChild>\n                    <Button\n                        variant={action.variant || (action.isActive ? 'default' : 'ghost')}\n                        size=\"icon\"\n                        onClick={action.onClick}\n                        className=\"h-8 w-8\"\n                        type=\"button\"\n                    >\n                        {action.icon}\n                    </Button>\n                </TooltipTrigger>\n                <TooltipContent>\n                    <div className=\"flex justify-between w-full\">\n                        <span>{action.label}</span>\n                        {action.shortcut && (\n                            <span className=\"text-muted-foreground ml-4\">{action.shortcut}</span>\n                        )}\n                    </div>\n                </TooltipContent>\n            </Tooltip>\n        </TooltipProvider>\n    );\n});\nToolbarAction.displayName = 'ToolbarAction';\n\n// Create stable dropdown component to prevent re-renders\nconst ToolbarDropdownComponent = memo(({dropdown, isMobile = false}: {\n    dropdown: ToolbarDropdown;\n    isMobile?: boolean\n}) => {\n    if (isMobile) {\n        return (\n            <div className=\"space-y-3\">\n                <div className=\"flex items-center space-x-3 px-4\">\n                    <div className=\"p-2 bg-muted rounded-lg\">\n                        {dropdown.icon}\n                    </div>\n                    <h3 className=\"font-semibold text-base text-foreground\">{dropdown.name}</h3>\n                </div>\n                <div className=\"space-y-1 px-2\">\n                    {dropdown.actions.map((action, actionIndex) => (\n                        <ToolbarAction\n                            key={`mobile-dropdown-action-${actionIndex}`}\n                            action={action}\n                            isMobile={true}\n                        />\n                    ))}\n                </div>\n            </div>\n        );\n    }\n\n    return (\n        <DropdownMenu>\n            <TooltipProvider>\n                <Tooltip>\n                    <TooltipTrigger asChild>\n                        <DropdownMenuTrigger asChild>\n                            <Button variant=\"ghost\" size=\"icon\" className=\"h-8 w-8\" type=\"button\">\n                                {dropdown.icon}\n                            </Button>\n                        </DropdownMenuTrigger>\n                    </TooltipTrigger>\n                    <TooltipContent>{dropdown.name}</TooltipContent>\n                </Tooltip>\n            </TooltipProvider>\n\n            <DropdownMenuContent align=\"start\">\n                <DropdownMenuLabel>{dropdown.name}</DropdownMenuLabel>\n                <DropdownMenuSeparator/>\n                {dropdown.actions.map((action, actionIndex) => (\n                    <DropdownMenuItem\n                        key={`dropdown-action-${actionIndex}`}\n                        onClick={action.onClick}\n                    >\n                        <span className=\"mr-2\">{action.icon}</span>\n                        <span>{action.label}</span>\n                        {action.shortcut && (\n                            <span className=\"ml-auto text-xs text-muted-foreground\">\n                                {action.shortcut}\n                            </span>\n                        )}\n                    </DropdownMenuItem>\n                ))}\n            </DropdownMenuContent>\n        </DropdownMenu>\n    );\n});\nToolbarDropdownComponent.displayName = 'ToolbarDropdownComponent';\n\n// Mobile toolbar sheet component\nconst MobileToolbarSheet = memo(({\n                                     groups,\n                                     dropdowns,\n                                     canUndo,\n                                     canRedo,\n                                     onUndo,\n                                     onRedo,\n                                     onSave,\n                                     onExport,\n                                     onAIAssist,\n                                     enableAI,\n                                     onBold,\n                                     onItalic,\n                                     onLink,\n                                     onHeading1,\n                                     onHeading2,\n                                     onHeading3,\n                                     onBulletList,\n                                     onNumberedList,\n                                     onQuote,\n                                     onCode,\n                                     onImage,\n                                 }: Omit<MarkdownToolbarProps, 'className' | 'viewMode' | 'onViewModeChange'>) => {\n    const [isOpen, setIsOpen] = useState(false);\n\n    const handleActionClick = (originalOnClick: () => void) => {\n        return () => {\n            originalOnClick();\n            setIsOpen(false);\n        };\n    };\n\n    // Default formatting actions\n    const defaultFormatGroup: ToolbarGroup = {\n        name: 'Formatting',\n        actions: [\n            {\n                icon: <Bold size={16}/>,\n                label: 'Bold',\n                onClick: handleActionClick(onBold || (() => {\n                })),\n            },\n            {\n                icon: <Italic size={16}/>,\n                label: 'Italic',\n                onClick: handleActionClick(onItalic || (() => {\n                })),\n            },\n            {\n                icon: <Link size={16}/>,\n                label: 'Link',\n                onClick: handleActionClick(onLink || (() => {\n                })),\n            },\n        ],\n    };\n\n    // Default heading actions\n    const defaultHeadingGroup: ToolbarGroup = {\n        name: 'Headings',\n        actions: [\n            {\n                icon: <Heading1 size={16}/>,\n                label: 'Heading 1',\n                onClick: handleActionClick(onHeading1 || (() => {\n                })),\n            },\n            {\n                icon: <Heading2 size={16}/>,\n                label: 'Heading 2',\n                onClick: handleActionClick(onHeading2 || (() => {\n                })),\n            },\n            {\n                icon: <Heading3 size={16}/>,\n                label: 'Heading 3',\n                onClick: handleActionClick(onHeading3 || (() => {\n                })),\n            },\n        ],\n    };\n\n    // Default list actions\n    const defaultListGroup: ToolbarGroup = {\n        name: 'Lists & Quotes',\n        actions: [\n            {\n                icon: <List size={16}/>,\n                label: 'Bullet List',\n                onClick: handleActionClick(onBulletList || (() => {\n                })),\n            },\n            {\n                icon: <ListOrdered size={16}/>,\n                label: 'Numbered List',\n                onClick: handleActionClick(onNumberedList || (() => {\n                })),\n            },\n            {\n                icon: <Quote size={16}/>,\n                label: 'Blockquote',\n                onClick: handleActionClick(onQuote || (() => {\n                })),\n            },\n        ],\n    };\n\n    // Default insert actions\n    const defaultInsertGroup: ToolbarGroup = {\n        name: 'Insert',\n        actions: [\n            {\n                icon: <Code size={16}/>,\n                label: 'Inline Code',\n                onClick: handleActionClick(onCode || (() => {\n                })),\n            },\n            {\n                icon: <Image size={16}/>,\n                label: 'Image',\n                onClick: handleActionClick(onImage || (() => {\n                })),\n            },\n        ],\n    };\n\n    // Combine with provided groups or use defaults\n    const allGroups = groups && groups.length > 0 ? groups : [\n        defaultFormatGroup,\n        defaultHeadingGroup,\n        defaultListGroup,\n        defaultInsertGroup,\n    ];\n\n    // Combine with provided dropdowns or use defaults\n    const allDropdowns = dropdowns && dropdowns.length > 0 ? dropdowns : [];\n\n    return (\n        <Sheet open={isOpen} onOpenChange={setIsOpen}>\n            <SheetTrigger asChild>\n                <Button variant=\"ghost\" size=\"icon\" className=\"md:hidden h-8 w-8\">\n                    <Menu size={16}/>\n                </Button>\n            </SheetTrigger>\n            <SheetContent side=\"left\" className=\"w-80 flex flex-col\">\n                <SheetHeader className=\"flex-shrink-0\">\n                    <SheetTitle>Markdown Tools</SheetTitle>\n                    <SheetDescription>\n                        Format your text and insert content elements\n                    </SheetDescription>\n                </SheetHeader>\n\n                <ScrollArea className=\"flex-1 mt-6\">\n                    <div className=\"space-y-6 pr-4\">\n                        {/* History */}\n                        <div className=\"space-y-3\">\n                            <h3 className=\"font-semibold text-base text-foreground\">History</h3>\n                            <div className=\"flex gap-2\">\n                                <Button\n                                    variant=\"outline\"\n                                    onClick={handleActionClick(onUndo || (() => {\n                                    }))}\n                                    disabled={!canUndo}\n                                    className=\"flex-1\"\n                                >\n                                    <RotateCcw size={16} className=\"mr-2\"/>\n                                    Undo\n                                </Button>\n                                <Button\n                                    variant=\"outline\"\n                                    onClick={handleActionClick(onRedo || (() => {\n                                    }))}\n                                    disabled={!canRedo}\n                                    className=\"flex-1\"\n                                >\n                                    <RotateCw size={16} className=\"mr-2\"/>\n                                    Redo\n                                </Button>\n                            </div>\n                        </div>\n\n                        {/* Groups */}\n                        {allGroups.map((group, groupIndex) => (\n                            <div key={`mobile-group-${groupIndex}`} className=\"space-y-3\">\n                                <div className=\"flex items-center space-x-3 px-4\">\n                                    <div className=\"p-2 bg-muted rounded-lg\">\n                                        {group.actions[0]?.icon}\n                                    </div>\n                                    <h3 className=\"font-semibold text-base text-foreground\">{group.name}</h3>\n                                </div>\n                                <div className=\"space-y-1 px-2\">\n                                    {group.actions.map((action, actionIndex) => (\n                                        <ToolbarAction\n                                            key={`mobile-action-${actionIndex}`}\n                                            action={{\n                                                ...action,\n                                                onClick: handleActionClick(action.onClick)\n                                            }}\n                                            isMobile={true}\n                                        />\n                                    ))}\n                                </div>\n                            </div>\n                        ))}\n\n                        {/* Dropdowns */}\n                        {allDropdowns.map((dropdown, dropdownIndex) => (\n                            <ToolbarDropdownComponent\n                                key={`mobile-dropdown-${dropdownIndex}`}\n                                dropdown={{\n                                    ...dropdown,\n                                    actions: dropdown.actions.map(action => ({\n                                        ...action,\n                                        onClick: handleActionClick(action.onClick)\n                                    }))\n                                }}\n                                isMobile={true}\n                            />\n                        ))}\n\n                        {/* AI Assistant */}\n                        {enableAI && onAIAssist && (\n                            <div className=\"space-y-3\">\n                                <div className=\"flex items-center space-x-3 px-4\">\n                                    <div className=\"p-2 bg-muted rounded-lg\">\n                                        <Sparkles size={16}/>\n                                    </div>\n                                    <h3 className=\"font-semibold text-base text-foreground\">AI Assistant</h3>\n                                </div>\n                                <div className=\"px-2\">\n                                    <Button\n                                        variant=\"outline\"\n                                        onClick={handleActionClick(onAIAssist)}\n                                        className=\"w-full justify-start h-14 text-left font-normal hover:bg-accent/50 transition-colors\"\n                                    >\n                                        <Sparkles size={16} className=\"mr-4 text-muted-foreground\"/>\n                                        <span className=\"flex-1 text-foreground\">Open AI Assistant</span>\n                                    </Button>\n                                </div>\n                            </div>\n                        )}\n\n                        {/* Save & Export */}\n                        {(onSave || onExport) && (\n                            <div className=\"space-y-3\">\n                                <h3 className=\"font-semibold text-base text-foreground\">Actions</h3>\n                                <div className=\"space-y-2 px-2\">\n                                    {onSave && (\n                                        <Button\n                                            variant=\"outline\"\n                                            onClick={handleActionClick(onSave)}\n                                            className=\"w-full justify-start h-14 text-left font-normal hover:bg-accent/50 transition-colors\"\n                                        >\n                                            <Save size={16} className=\"mr-4 text-muted-foreground\"/>\n                                            <span className=\"flex-1 text-foreground\">Save</span>\n                                        </Button>\n                                    )}\n                                    {onExport && (\n                                        <Button\n                                            variant=\"outline\"\n                                            onClick={handleActionClick(onExport)}\n                                            className=\"w-full justify-start h-14 text-left font-normal hover:bg-accent/50 transition-colors\"\n                                        >\n                                            <FileDown size={16} className=\"mr-4 text-muted-foreground\"/>\n                                            <span className=\"flex-1 text-foreground\">Export</span>\n                                        </Button>\n                                    )}\n                                </div>\n                            </div>\n                        )}\n                    </div>\n                </ScrollArea>\n            </SheetContent>\n        </Sheet>\n    );\n});\n\nMobileToolbarSheet.displayName = 'MobileToolbarSheet';\n\n// Main toolbar component\nconst MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({\n                                                             groups = [],\n                                                             dropdowns = [],\n                                                             canUndo = false,\n                                                             canRedo = false,\n                                                             onUndo,\n                                                             onRedo,\n                                                             onSave,\n                                                             onExport,\n                                                             viewMode = 'edit',\n                                                             onViewModeChange,\n                                                             onAIAssist,\n                                                             enableAI = false,\n                                                             className = '',\n                                                             onBold,\n                                                             onItalic,\n                                                             onLink,\n                                                             onHeading1,\n                                                             onHeading2,\n                                                             onHeading3,\n                                                             onBulletList,\n                                                             onNumberedList,\n                                                             onQuote,\n                                                             onCode,\n                                                             onImage,\n                                                         }) => {\n    return (\n        <div className={`flex items-center justify-between p-2 border-b bg-muted/10 ${className}`}>\n            {/* Left side - Tools */}\n            <div className=\"flex items-center space-x-1\">\n                {/* Mobile toolbar */}\n                <MobileToolbarSheet\n                    groups={groups}\n                    dropdowns={dropdowns}\n                    canUndo={canUndo}\n                    canRedo={canRedo}\n                    onUndo={onUndo}\n                    onRedo={onRedo}\n                    onSave={onSave}\n                    onExport={onExport}\n                    onAIAssist={onAIAssist}\n                    enableAI={enableAI}\n                    onBold={onBold}\n                    onItalic={onItalic}\n                    onLink={onLink}\n                    onHeading1={onHeading1}\n                    onHeading2={onHeading2}\n                    onHeading3={onHeading3}\n                    onBulletList={onBulletList}\n                    onNumberedList={onNumberedList}\n                    onQuote={onQuote}\n                    onCode={onCode}\n                    onImage={onImage}\n                />\n\n                {/* Desktop toolbar */}\n                <div className=\"hidden sm:flex items-center w-full\">\n                    <div className=\"flex items-center space-x-1\">\n                        {/* Undo/Redo */}\n                        <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            onClick={onUndo}\n                            disabled={!canUndo}\n                            className=\"h-8 w-8\"\n                            title=\"Undo (Ctrl+Z)\"\n                        >\n                            <RotateCcw size={16}/>\n                        </Button>\n                        <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            onClick={onRedo}\n                            disabled={!canRedo}\n                            className=\"h-8 w-8\"\n                            title=\"Redo (Ctrl+Shift+Z)\"\n                        >\n                            <RotateCw size={16}/>\n                        </Button>\n\n                        <Separator orientation=\"vertical\" className=\"mx-1 h-6\"/>\n\n                        {/* Text formatting */}\n                        <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            onClick={onBold}\n                            className=\"h-8 w-8\"\n                            title=\"Bold (Ctrl+B)\"\n                        >\n                            <Bold size={16}/>\n                        </Button>\n                        <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            onClick={onItalic}\n                            className=\"h-8 w-8\"\n                            title=\"Italic (Ctrl+I)\"\n                        >\n                            <Italic size={16}/>\n                        </Button>\n                        <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            onClick={onLink}\n                            className=\"h-8 w-8\"\n                            title=\"Link (Ctrl+K)\"\n                        >\n                            <Link size={16}/>\n                        </Button>\n\n                        <Separator orientation=\"vertical\" className=\"mx-1 h-6\"/>\n\n                        {/* Headings */}\n                        <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            onClick={onHeading1}\n                            className=\"h-8 w-8\"\n                            title=\"Heading 1 (Ctrl+1)\"\n                        >\n                            <Heading1 size={16}/>\n                        </Button>\n                        <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            onClick={onHeading2}\n                            className=\"h-8 w-8\"\n                            title=\"Heading 2 (Ctrl+2)\"\n                        >\n                            <Heading2 size={16}/>\n                        </Button>\n                        <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            onClick={onHeading3}\n                            className=\"h-8 w-8\"\n                            title=\"Heading 3 (Ctrl+3)\"\n                        >\n                            <Heading3 size={16}/>\n                        </Button>\n\n                        <Separator orientation=\"vertical\" className=\"mx-1 h-6\"/>\n\n                        {/* Lists and quotes */}\n                        <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            onClick={onBulletList}\n                            className=\"h-8 w-8\"\n                            title=\"Bullet List\"\n                        >\n                            <List size={16}/>\n                        </Button>\n                        <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            onClick={onNumberedList}\n                            className=\"h-8 w-8\"\n                            title=\"Numbered List\"\n                        >\n                            <ListOrdered size={16}/>\n                        </Button>\n                        <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            onClick={onQuote}\n                            className=\"h-8 w-8\"\n                            title=\"Blockquote\"\n                        >\n                            <Quote size={16}/>\n                        </Button>\n\n                        <Separator orientation=\"vertical\" className=\"mx-1 h-6\"/>\n\n                        {/* Code and images */}\n                        <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            onClick={onCode}\n                            className=\"h-8 w-8\"\n                            title=\"Inline Code\"\n                        >\n                            <Code size={16}/>\n                        </Button>\n                        <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            onClick={onImage}\n                            className=\"h-8 w-8\"\n                            title=\"Image\"\n                        >\n                            <Image size={16}/>\n                        </Button>\n\n                        {/* Dropdowns (including CUM Extensions) */}\n                        {dropdowns.length > 0 && (\n                            <>\n                                <Separator orientation=\"vertical\" className=\"mx-1 h-6\"/>\n                                {dropdowns.map((dropdown, index) => (\n                                    <DropdownMenu key={index}>\n                                        <DropdownMenuTrigger asChild>\n                                            <Button\n                                                variant=\"ghost\"\n                                                size=\"sm\"\n                                                className=\"h-8 gap-1\"\n                                                title={dropdown.name}\n                                            >\n                                                {dropdown.icon}\n                                                <ChevronDown size={12}/>\n                                            </Button>\n                                        </DropdownMenuTrigger>\n                                        <DropdownMenuContent align=\"start\">\n                                            <DropdownMenuLabel>{dropdown.name}</DropdownMenuLabel>\n                                            <DropdownMenuSeparator/>\n                                            {dropdown.actions.map((action, actionIndex) => (\n                                                <DropdownMenuItem\n                                                    key={actionIndex}\n                                                    onClick={action.onClick}\n                                                >\n                                                    <span className=\"mr-2\">{action.icon}</span>\n                                                    <span>{action.label}</span>\n                                                    {action.shortcut && (\n                                                        <span className=\"ml-auto text-xs text-muted-foreground\">\n                                                        {action.shortcut}\n                                                    </span>\n                                                    )}\n                                                </DropdownMenuItem>\n                                            ))}\n                                        </DropdownMenuContent>\n                                    </DropdownMenu>\n                                ))}\n                            </>\n                        )}\n                    </div>\n                </div>\n            </div>\n\n            {/* Right side - View modes */}\n            <div className=\"flex items-center\">\n                {/* AI Assistant button (if enabled) */}\n                {enableAI && onAIAssist && (\n                    <>\n                        <Button\n                            variant=\"outline\"\n                            size=\"sm\"\n                            onClick={onAIAssist}\n                            className=\"h-8 gap-1 text-primary\"\n                            title=\"AI Assistant (Alt+A)\"\n                        >\n                            <Sparkles size={15}/>\n                            <span>AI</span>\n                        </Button>\n                        <Separator orientation=\"vertical\" className=\"mx-1 h-6\"/>\n                    </>\n                )}\n\n                {/* Save & Export */}\n                {onSave && (\n                    <Button\n                        variant=\"ghost\"\n                        size=\"icon\"\n                        onClick={onSave}\n                        className=\"h-8 w-8\"\n                        title=\"Save (Ctrl+S)\"\n                    >\n                        <Save size={16}/>\n                    </Button>\n                )}\n                {onExport && (\n                    <Button\n                        variant=\"ghost\"\n                        size=\"icon\"\n                        onClick={onExport}\n                        className=\"h-8 w-8\"\n                        title=\"Export\"\n                    >\n                        <FileDown size={16}/>\n                    </Button>\n                )}\n\n                <Separator orientation=\"vertical\" className=\"mx-1 h-6\"/>\n\n                {/* View mode tabs */}\n                <Tabs\n                    value={viewMode}\n                    onValueChange={(value) => onViewModeChange?.(value as 'edit' | 'preview' | 'split')}\n                    className=\"w-auto\"\n                >\n                    <TabsList className=\"grid w-full grid-cols-3 h-8\">\n                        <TabsTrigger value=\"edit\" className=\"h-6 px-3 text-xs\">\n                            Edit\n                        </TabsTrigger>\n                        <TabsTrigger value=\"split\" className=\"h-6 px-3 text-xs\">\n                            Split\n                        </TabsTrigger>\n                        <TabsTrigger value=\"preview\" className=\"h-6 px-3 text-xs\">\n                            Preview\n                        </TabsTrigger>\n                    </TabsList>\n                </Tabs>\n            </div>\n        </div>\n    );\n};\n\nexport default MarkdownToolbar;"
  },
  {
    "path": "components/markdown-editor/RenderMarkdown.tsx",
    "content": "'use client';\n\nimport React, {useMemo} from 'react';\nimport {renderMarkdown} from '@/lib/services/core/markdown/useCustomExtensions';\nimport {useDebounce} from \"use-debounce\";\n\ninterface RenderMarkdownProps {\n    children: string;\n    className?: string;\n    enableCUM?: boolean;\n}\n\nexport const RenderMarkdown: React.FC<RenderMarkdownProps> = ({\n                                                                  children = '',\n                                                                  className = ''\n                                                              }) => {\n    // Use our new markdown renderer\n    const [debouncedContent] = useDebounce(children || '', 300);\n\n    const renderedHtml = useMemo(() => {\n        const content = debouncedContent || '';\n        return renderMarkdown(content);\n    }, [debouncedContent]);\n\n    return (\n        <div\n            className={`prose max-w-none prose-img:my-4 prose-headings:mt-6 prose-headings:mb-4 prose-p:mb-4 prose-pre:my-4 prose-blockquote:my-4 ${className}`}\n            dangerouslySetInnerHTML={{__html: renderedHtml}}\n            suppressHydrationWarning\n        />\n    );\n};"
  },
  {
    "path": "components/markdown-editor/StatusBar.tsx",
    "content": "'use client';\n\nimport React from 'react';\nimport { Info, Clock, Check, AlertCircle } from 'lucide-react';\nimport { useTimezone } from '@/hooks/use-timezone';\nimport {\n    Tooltip,\n    TooltipContent,\n    TooltipProvider,\n    TooltipTrigger,\n} from '@/components/ui/tooltip';\n\nexport interface StatusBarProps {\n    /**\n     * Number of words in the document\n     */\n    wordCount: number;\n\n    /**\n     * Number of characters in the document\n     */\n    charCount: number;\n\n    /**\n     * Number of lines in the document\n     */\n    lineCount?: number;\n\n    /**\n     * Current cursor position (line, column)\n     */\n    cursorPosition?: {\n        line: number;\n        column: number;\n    };\n\n    /**\n     * Reading time in minutes\n     */\n    readingTime?: number;\n\n    /**\n     * Whether the document has been saved\n     */\n    isSaved?: boolean;\n\n    /**\n     * Status message to display\n     */\n    statusMessage?: string;\n\n    /**\n     * Last saved timestamp\n     */\n    lastSaved?: Date;\n\n    /**\n     * Additional CSS classes\n     */\n    className?: string;\n}\n\n/**\n * Time formatter for last saved timestamp\n */\nfunction formatSaveTime(date: Date, timezone = 'UTC'): string {\n    const now = new Date();\n    const diff = now.getTime() - date.getTime();\n\n    // If less than a minute ago\n    if (diff < 60000) {\n        return 'just now';\n    }\n\n    // If less than an hour ago\n    if (diff < 3600000) {\n        const minutes = Math.floor(diff / 60000);\n        return `${minutes} ${minutes === 1 ? 'minute' : 'minutes'} ago`;\n    }\n\n    // Format time for today\n    if (date.getDate() === now.getDate() &&\n        date.getMonth() === now.getMonth() &&\n        date.getFullYear() === now.getFullYear()) {\n        return `today at ${date.toLocaleString('en-US', {\n            hour: 'numeric',\n            minute: 'numeric',\n            hour12: true,\n            timeZone: timezone,\n        })}`;\n    }\n\n    // Format for other days\n    return date.toLocaleString('en-US', {\n        month: 'short',\n        day: 'numeric',\n        hour: 'numeric',\n        minute: 'numeric',\n        hour12: true,\n        timeZone: timezone,\n    });\n}\n\n/**\n * Status bar component for markdown editor\n */\nexport default function StatusBar({\n                                      wordCount,\n                                      charCount,\n                                      lineCount,\n                                      cursorPosition,\n                                      readingTime,\n                                      isSaved,\n                                      statusMessage,\n                                      lastSaved,\n                                      className = '',\n                                  }: StatusBarProps) {\n    const timezone = useTimezone();\n\n    return (\n        <div className={`flex items-center justify-between h-6 px-3 py-1 text-xs text-muted-foreground border-t bg-muted/20 ${className}`}>\n            {/* Left side metrics */}\n            <div className=\"flex items-center space-x-3\">\n                {/* Word count */}\n                <TooltipProvider>\n                    <Tooltip>\n                        <TooltipTrigger asChild>\n                            <div className=\"flex items-center\">\n                                <span className=\"font-medium\">{wordCount}</span>\n                                <span className=\"ml-1\">words</span>\n                            </div>\n                        </TooltipTrigger>\n                        <TooltipContent side=\"top\">\n                            <p>Word count</p>\n                        </TooltipContent>\n                    </Tooltip>\n                </TooltipProvider>\n\n                {/* Character count */}\n                <TooltipProvider>\n                    <Tooltip>\n                        <TooltipTrigger asChild>\n                            <div className=\"flex items-center\">\n                                <span className=\"font-medium\">{charCount}</span>\n                                <span className=\"ml-1\">characters</span>\n                            </div>\n                        </TooltipTrigger>\n                        <TooltipContent side=\"top\">\n                            <p>Character count</p>\n                        </TooltipContent>\n                    </Tooltip>\n                </TooltipProvider>\n\n                {/* Line count if available */}\n                {lineCount !== undefined && (\n                    <TooltipProvider>\n                        <Tooltip>\n                            <TooltipTrigger asChild>\n                                <div className=\"flex items-center\">\n                                    <span className=\"font-medium\">{lineCount}</span>\n                                    <span className=\"ml-1\">lines</span>\n                                </div>\n                            </TooltipTrigger>\n                            <TooltipContent side=\"top\">\n                                <p>Line count</p>\n                            </TooltipContent>\n                        </Tooltip>\n                    </TooltipProvider>\n                )}\n\n                {/* Reading time if available */}\n                {readingTime !== undefined && (\n                    <TooltipProvider>\n                        <Tooltip>\n                            <TooltipTrigger asChild>\n                                <div className=\"flex items-center\">\n                                    <Clock className=\"w-3 h-3 mr-1\" />\n                                    <span className=\"font-medium\">{readingTime}</span>\n                                    <span className=\"ml-1\">min read</span>\n                                </div>\n                            </TooltipTrigger>\n                            <TooltipContent side=\"top\">\n                                <p>Estimated reading time</p>\n                            </TooltipContent>\n                        </Tooltip>\n                    </TooltipProvider>\n                )}\n            </div>\n\n            {/* Center - status message */}\n            {statusMessage && (\n                <div className=\"absolute left-1/2 transform -translate-x-1/2 flex items-center\">\n                    {isSaved !== undefined && (\n                        <>\n                            {isSaved ? (\n                                <Check className=\"w-3 h-3 mr-1 text-green-500\" />\n                            ) : (\n                                <AlertCircle className=\"w-3 h-3 mr-1 text-amber-500\" />\n                            )}\n                        </>\n                    )}\n                    <span>{statusMessage}</span>\n                </div>\n            )}\n\n            {/* Right side - cursor position and saved status */}\n            <div className=\"flex items-center space-x-3\">\n                {/* Cursor position if available */}\n                {cursorPosition && (\n                    <div className=\"flex items-center\">\n                        <span>Ln {cursorPosition.line}, Col {cursorPosition.column}</span>\n                    </div>\n                )}\n\n                {/* Last saved timestamp if available */}\n                {lastSaved && (\n                    <TooltipProvider>\n                        <Tooltip>\n                            <TooltipTrigger asChild>\n                                <div className=\"flex items-center\">\n                                    {isSaved && <Check className=\"w-3 h-3 mr-1 text-green-500\" />}\n                                    <span>Saved {formatSaveTime(lastSaved, timezone)}</span>\n                                </div>\n                            </TooltipTrigger>\n                            <TooltipContent side=\"top\">\n                                <p>\n                                    Last saved: {lastSaved.toLocaleString('en-US', {\n                                    month: 'short',\n                                    day: 'numeric',\n                                    year: 'numeric',\n                                    hour: 'numeric',\n                                    minute: 'numeric',\n                                    second: 'numeric',\n                                    hour12: true\n                                })}\n                                </p>\n                            </TooltipContent>\n                        </Tooltip>\n                    </TooltipProvider>\n                )}\n\n                {/* If no lastSaved but we have an isSaved state */}\n                {!lastSaved && isSaved !== undefined && (\n                    <div className=\"flex items-center\">\n                        {isSaved ? (\n                            <>\n                                <Check className=\"w-3 h-3 mr-1 text-green-500\" />\n                                <span>Saved</span>\n                            </>\n                        ) : (\n                            <>\n                                <AlertCircle className=\"w-3 h-3 mr-1 text-amber-500\" />\n                                <span>Unsaved changes</span>\n                            </>\n                        )}\n                    </div>\n                )}\n\n                {/* Help button */}\n                <TooltipProvider>\n                    <Tooltip>\n                        <TooltipTrigger asChild>\n                            <button className=\"focus:outline-none\">\n                                <Info className=\"w-3.5 h-3.5 text-muted-foreground/80 hover:text-muted-foreground\" />\n                            </button>\n                        </TooltipTrigger>\n                        <TooltipContent side=\"top\" align=\"end\">\n                            <div className=\"space-y-1 max-w-xs\">\n                                <p className=\"font-medium\">Keyboard Shortcuts</p>\n                                <div className=\"text-xs grid grid-cols-2 gap-x-4 gap-y-1\">\n                                    <span>Bold</span>\n                                    <span className=\"font-mono\">Ctrl+B</span>\n                                    <span>Italic</span>\n                                    <span className=\"font-mono\">Ctrl+I</span>\n                                    <span>Link</span>\n                                    <span className=\"font-mono\">Ctrl+K</span>\n                                    <span>Heading 1-3</span>\n                                    <span className=\"font-mono\">Ctrl+1,2,3</span>\n                                    <span>Save</span>\n                                    <span className=\"font-mono\">Ctrl+S</span>\n                                    <span>AI Assistant</span>\n                                    <span className=\"font-mono\">Alt+A</span>\n                                </div>\n                            </div>\n                        </TooltipContent>\n                    </Tooltip>\n                </TooltipProvider>\n            </div>\n        </div>\n    );\n}"
  },
  {
    "path": "components/markdown-editor/ai/AIAssistantPanel.tsx",
    "content": "'use client';\n\nimport React, { useState, useRef, useEffect } from 'react';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport {\n    Bot,\n    X,\n    Sparkles,\n    Loader2,\n    ArrowRight,\n    Copy,\n    Check,\n    RefreshCw,\n    ChevronRight,\n    Lightbulb,\n    Settings,\n    Wand2,\n} from 'lucide-react';\n\nimport { AICompletionType, AIEditorResult } from '@/lib/utils/ai/types';\nimport { getCompletionTypeDescription } from '@/lib/utils/ai/prompts';\n\nimport { Button } from '@/components/ui/button';\nimport { Textarea } from '@/components/ui/textarea';\nimport { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';\nimport { Slider } from '@/components/ui/slider';\nimport { ScrollArea } from '@/components/ui/scroll-area';\nimport { Card, CardContent } from '@/components/ui/card';\nimport { Badge } from '@/components/ui/badge';\nimport {\n    Tooltip,\n    TooltipContent,\n    TooltipProvider,\n    TooltipTrigger,\n} from '@/components/ui/tooltip';\nimport {\n    Collapsible,\n    CollapsibleContent,\n    CollapsibleTrigger,\n} from '@/components/ui/collapsible';\nimport { cn } from '@/lib/utils';\n\nexport interface AIAssistantPanelProps {\n    /**\n     * Whether the panel is visible\n     */\n    isVisible: boolean;\n\n    /**\n     * Current selection or content to process\n     */\n    content: string;\n\n    /**\n     * Selected AI completion type\n     */\n    completionType: AICompletionType;\n\n    /**\n     * Custom prompt (for custom completion type)\n     */\n    customPrompt: string;\n\n    /**\n     * Is currently generating content\n     */\n    isLoading: boolean;\n\n    /**\n     * Is currently regenerating content\n     */\n    isRegenerating?: boolean;\n\n    /**\n     * Generated result\n     */\n    generatedResult?: AIEditorResult | null;\n\n    /**\n     * Error message if generation failed\n     */\n    error?: Error | null;\n\n    /**\n     * Handler for closing the panel\n     */\n    onClose: () => void;\n\n    /**\n     * Handler for changing completion type\n     */\n    onCompletionTypeChange: (type: AICompletionType) => void;\n\n    /**\n     * Handler for changing custom prompt\n     */\n    onCustomPromptChange: (prompt: string) => void;\n\n    /**\n     * Handler for changing temperature\n     */\n    onTemperatureChange?: (temperature: number) => void;\n\n    /**\n     * Current temperature value\n     */\n    temperature?: number;\n\n    /**\n     * Handler for generating content\n     */\n    onGenerate: () => void;\n\n    /**\n     * Handler for applying generated content\n     */\n    onApply: (text: string) => void;\n\n    /**\n     * Handler for regenerating content\n     */\n    onRegenerate?: () => void;\n\n    /**\n     * Handler for setting API key\n     */\n    onSetApiKey?: (key: string) => void;\n\n    /**\n     * Is API key valid\n     */\n    isApiKeyValid?: boolean | null;\n}\n\n/**\n * Redesigned AI Assistant Panel component with improved UX\n */\nexport default function AIAssistantPanel({\n                                             isVisible,\n                                             content,\n                                             completionType,\n                                             customPrompt,\n                                             isLoading,\n                                             isRegenerating = false,\n                                             generatedResult,\n                                             error,\n                                             onClose,\n                                             onCompletionTypeChange,\n                                             onCustomPromptChange,\n                                             onTemperatureChange,\n                                             temperature = 0.7,\n                                             onGenerate,\n                                             onApply,\n                                             onRegenerate,\n                                             onSetApiKey,\n                                             isApiKeyValid = null,\n                                         }: AIAssistantPanelProps) {\n    // State for API key input\n    const [apiKey, setApiKey] = useState('');\n\n    // State for copy button\n    const [copied, setCopied] = useState(false);\n\n    // State for advanced settings and suggestions\n    const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);\n    const [showSuggestions, setShowSuggestions] = useState(false);\n\n    // Refs for content and result\n    const contentPreviewRef = useRef<HTMLDivElement>(null);\n    const resultRef = useRef<HTMLDivElement>(null);\n\n    // Truncate text for display\n    const truncateContent = (text: string, maxLength: number = 200): string => {\n        if (!text || text.length <= maxLength) return text || '';\n        return text.substring(0, maxLength) + '...';\n    };\n\n    // Copy generated text to clipboard\n    const handleCopy = () => {\n        if (generatedResult?.text) {\n            navigator.clipboard.writeText(generatedResult.text);\n            setCopied(true);\n            setTimeout(() => setCopied(false), 2000);\n        }\n    };\n\n    // Scroll elements into view when they mount\n    useEffect(() => {\n        if (isVisible && resultRef.current && generatedResult) {\n            resultRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });\n        }\n    }, [isVisible, generatedResult]);\n\n    // Reset copy state when result changes\n    useEffect(() => {\n        setCopied(false);\n    }, [generatedResult]);\n\n    // Sample writing suggestions based on content\n    const getSuggestions = () => {\n        // These would normally be generated dynamically based on content\n        return [\n            { type: AICompletionType.IMPROVE, label: 'Improve writing style' },\n            { type: AICompletionType.EXPAND, label: 'Add more details' },\n            { type: AICompletionType.SUMMARIZE, label: 'Create a summary' },\n            { type: AICompletionType.FIX_GRAMMAR, label: 'Fix grammar and spelling' }\n        ];\n    };\n\n    // Animation variants\n    const panelVariants = {\n        hidden: { opacity: 0, x: '100%' },\n        visible: { opacity: 1, x: 0 },\n        exit: { opacity: 0, x: '100%' },\n    };\n\n    return (\n        <AnimatePresence>\n            {isVisible && (\n                <motion.div\n                    className=\"fixed inset-0 bg-black/30 dark:bg-black/60 backdrop-blur-[2px] z-50 flex items-center justify-end overflow-hidden\"\n                    initial={{ opacity: 0 }}\n                    animate={{ opacity: 1 }}\n                    exit={{ opacity: 0 }}\n                    onClick={onClose}\n                >\n                    <motion.div\n                        className=\"w-full sm:w-[450px] lg:w-[520px] bg-background border-l border-t sm:border rounded-tl-lg sm:rounded-lg shadow-2xl overflow-hidden sm:mr-6 sm:my-6 max-h-[90vh] flex flex-col h-[75vh]\"\n                        variants={panelVariants}\n                        initial=\"hidden\"\n                        animate=\"visible\"\n                        exit=\"exit\"\n                        transition={{ type: \"spring\", damping: 30, stiffness: 300 }}\n                        onClick={(e) => e.stopPropagation()}\n                    >\n                        {/* Header */}\n                        <div className=\"flex items-center justify-between p-4 border-b bg-muted/40\">\n                            <div className=\"flex items-center gap-2.5\">\n                                <div className=\"bg-primary/15 text-primary p-1.5 rounded-md\">\n                                    <Bot className=\"w-5 h-5\" />\n                                </div>\n                                <div>\n                                    <h3 className=\"font-semibold text-base\">AI Assistant</h3>\n                                    <p className=\"text-xs text-muted-foreground\">Enhance your writing with AI</p>\n                                </div>\n                            </div>\n                            <TooltipProvider>\n                                <Tooltip>\n                                    <TooltipTrigger asChild>\n                                        <Button\n                                            variant=\"ghost\"\n                                            size=\"icon\"\n                                            onClick={onClose}\n                                            className=\"h-8 w-8 rounded-full hover:bg-destructive/10 hover:text-destructive\"\n                                        >\n                                            <X className=\"w-4 h-4\" />\n                                        </Button>\n                                    </TooltipTrigger>\n                                    <TooltipContent side=\"left\">Close</TooltipContent>\n                                </Tooltip>\n                            </TooltipProvider>\n                        </div>\n\n                        {/* Content */}\n                        <ScrollArea className=\"flex-grow\">\n                            <div className=\"p-5 space-y-6\">\n                                {/* API key input if not valid */}\n                                {isApiKeyValid === false && onSetApiKey && (\n                                    <Card className=\"bg-muted/30 border-orange-200 dark:border-orange-800\">\n                                        <CardContent className=\"pt-4 pb-3\">\n                                            <h4 className=\"text-sm font-medium mb-2 flex items-center\">\n                                                <span className=\"text-orange-500 mr-2\">⚠</span>\n                                                Enter API Key\n                                            </h4>\n                                            <p className=\"text-xs text-muted-foreground mb-3\">\n                                                Your API key is required for AI features. It will be stored only in your browser.\n                                            </p>\n                                            <div className=\"flex gap-2\">\n                                                <input\n                                                    type=\"password\"\n                                                    value={apiKey}\n                                                    onChange={(e) => setApiKey(e.target.value)}\n                                                    placeholder=\"sk-...\"\n                                                    className=\"flex-grow p-2 text-sm rounded-md border focus:ring-1 focus:ring-primary/70 focus:border-primary/70\"\n                                                />\n                                                <Button\n                                                    size=\"sm\"\n                                                    onClick={() => onSetApiKey(apiKey)}\n                                                    className=\"gap-1\"\n                                                >\n                                                    <Wand2 className=\"w-3.5 h-3.5\" />\n                                                    <span>Save</span>\n                                                </Button>\n                                            </div>\n                                        </CardContent>\n                                    </Card>\n                                )}\n\n                                {/* Action selection */}\n                                <div className=\"space-y-3\">\n                                    <div className=\"flex items-center justify-between\">\n                                        <h4 className=\"text-sm font-medium\">Select AI action</h4>\n                                        <Button\n                                            variant=\"ghost\"\n                                            size=\"sm\"\n                                            className={cn(\n                                                \"h-7 px-2.5 text-xs rounded-full transition-colors\",\n                                                showSuggestions && \"bg-primary/10 text-primary hover:bg-primary/15\"\n                                            )}\n                                            onClick={() => setShowSuggestions(!showSuggestions)}\n                                        >\n                                            <Lightbulb className=\"w-3.5 h-3.5 mr-1.5\" />\n                                            <span>Suggestions</span>\n                                        </Button>\n                                    </div>\n\n                                    <AnimatePresence>\n                                        {showSuggestions && (\n                                            <motion.div\n                                                initial={{ height: 0, opacity: 0 }}\n                                                animate={{ height: 'auto', opacity: 1 }}\n                                                exit={{ height: 0, opacity: 0 }}\n                                                className=\"overflow-hidden\"\n                                            >\n                                                <div className=\"bg-muted/30 p-3.5 rounded-md border mb-3.5\">\n                                                    <h5 className=\"text-xs font-medium mb-2.5\">Suggestions based on your content</h5>\n                                                    <div className=\"flex flex-wrap gap-2\">\n                                                        {getSuggestions().map((suggestion, i) => (\n                                                            <Badge\n                                                                key={i}\n                                                                variant=\"outline\"\n                                                                className=\"cursor-pointer hover:bg-primary/10 hover:text-primary hover:border-primary/30 transition-colors py-1\"\n                                                                onClick={() => onCompletionTypeChange(suggestion.type)}\n                                                            >\n                                                                {suggestion.label}\n                                                            </Badge>\n                                                        ))}\n                                                    </div>\n                                                </div>\n                                            </motion.div>\n                                        )}\n                                    </AnimatePresence>\n\n                                    <Tabs\n                                        defaultValue={completionType}\n                                        value={completionType}\n                                        onValueChange={(value) => onCompletionTypeChange(value as AICompletionType)}\n                                        className=\"w-full\"\n                                    >\n                                        <TabsList className=\"w-full grid grid-cols-4 h-9\">\n                                            <TabsTrigger value={AICompletionType.COMPLETE} className=\"text-xs\">\n                                                Complete\n                                            </TabsTrigger>\n                                            <TabsTrigger value={AICompletionType.IMPROVE} className=\"text-xs\">\n                                                Improve\n                                            </TabsTrigger>\n                                            <TabsTrigger value={AICompletionType.SUMMARIZE} className=\"text-xs\">\n                                                Summarize\n                                            </TabsTrigger>\n                                            <TabsTrigger value={AICompletionType.CUSTOM} className=\"text-xs\">\n                                                Custom\n                                            </TabsTrigger>\n                                        </TabsList>\n                                        <div className=\"flex mt-2.5\">\n                                            <TabsList className=\"h-auto bg-transparent p-0\">\n                                                <TabsTrigger\n                                                    value={AICompletionType.EXPAND}\n                                                    className=\"text-xs mr-1.5 h-7 px-3 rounded-full border\"\n                                                    data-state={completionType === AICompletionType.EXPAND ? \"active\" : \"inactive\"}\n                                                >\n                                                    Expand\n                                                </TabsTrigger>\n                                                <TabsTrigger\n                                                    value={AICompletionType.REPHRASE}\n                                                    className=\"text-xs mr-1.5 h-7 px-3 rounded-full border\"\n                                                    data-state={completionType === AICompletionType.REPHRASE ? \"active\" : \"inactive\"}\n                                                >\n                                                    Rephrase\n                                                </TabsTrigger>\n                                                <TabsTrigger\n                                                    value={AICompletionType.FIX_GRAMMAR}\n                                                    className=\"text-xs h-7 px-3 rounded-full border\"\n                                                    data-state={completionType === AICompletionType.FIX_GRAMMAR ? \"active\" : \"inactive\"}\n                                                >\n                                                    Fix Grammar\n                                                </TabsTrigger>\n                                            </TabsList>\n                                        </div>\n                                    </Tabs>\n\n                                    <p className=\"text-xs text-muted-foreground\">\n                                        {getCompletionTypeDescription(completionType)}\n                                    </p>\n                                </div>\n\n                                {/* Custom prompt input */}\n                                {completionType === AICompletionType.CUSTOM && (\n                                    <div className=\"space-y-2\">\n                                        <label htmlFor=\"customPrompt\" className=\"text-sm font-medium\">\n                                            Custom instruction\n                                        </label>\n                                        <Textarea\n                                            id=\"customPrompt\"\n                                            placeholder=\"Describe what you want the AI to do...\"\n                                            value={customPrompt}\n                                            onChange={(e) => onCustomPromptChange(e.target.value)}\n                                            className=\"min-h-[80px] text-sm resize-none focus:ring-1 focus:ring-primary/70\"\n                                        />\n                                    </div>\n                                )}\n\n                                {/* Content preview */}\n                                <div className=\"space-y-2\">\n                                    <div className=\"flex items-center justify-between\">\n                                        <h4 className=\"text-sm font-medium flex items-center\">\n                                            Content to process\n                                        </h4>\n                                        <p className=\"text-xs text-muted-foreground bg-muted/50 px-2 py-0.5 rounded-full\">\n                                            {content.length} characters\n                                        </p>\n                                    </div>\n                                    <div\n                                        ref={contentPreviewRef}\n                                        className=\"p-3.5 bg-muted/30 rounded-md border text-sm max-h-32 overflow-y-auto break-words whitespace-pre-wrap\"\n                                    >\n                                        {content ? truncateContent(content, 300) : (\n                                            <span className=\"text-muted-foreground italic\">No text selected. AI will use the current cursor position.</span>\n                                        )}\n                                    </div>\n                                </div>\n\n                                {/* Advanced settings */}\n                                <Collapsible\n                                    open={showAdvancedSettings}\n                                    onOpenChange={setShowAdvancedSettings}\n                                    className=\"border rounded-md overflow-hidden transition-shadow\"\n                                >\n                                    <CollapsibleTrigger asChild>\n                                        <div\n                                            className={cn(\n                                                \"flex items-center justify-between p-3 cursor-pointer hover:bg-muted/40 transition-colors\",\n                                                showAdvancedSettings && \"bg-muted/30\"\n                                            )}\n                                        >\n                                            <div className=\"flex items-center gap-2\">\n                                                <Settings className=\"w-4 h-4 text-muted-foreground\" />\n                                                <span className=\"text-sm font-medium\">Advanced settings</span>\n                                            </div>\n                                            <ChevronRight\n                                                className={`w-4 h-4 transition-transform duration-200 ${showAdvancedSettings ? 'rotate-90' : ''}`}\n                                            />\n                                        </div>\n                                    </CollapsibleTrigger>\n                                    <CollapsibleContent>\n                                        <div className=\"p-4 border-t space-y-4 bg-muted/10\">\n                                            {/* Temperature setting */}\n                                            <div className=\"space-y-2\">\n                                                <div className=\"flex items-center justify-between\">\n                                                    <label htmlFor=\"temperature\" className=\"text-sm\">Temperature</label>\n                                                    <span className=\"text-xs font-mono bg-muted px-2 py-0.5 rounded-full\">\n                            {temperature.toFixed(1)}\n                          </span>\n                                                </div>\n                                                <div className=\"flex items-center gap-3 mt-3\">\n                                                    <span className=\"text-xs text-muted-foreground\">Precise</span>\n                                                    <Slider\n                                                        id=\"temperature\"\n                                                        min={0}\n                                                        max={1}\n                                                        step={0.1}\n                                                        value={[temperature]}\n                                                        onValueChange={(value) => onTemperatureChange?.(value[0])}\n                                                        className=\"flex-grow\"\n                                                    />\n                                                    <span className=\"text-xs text-muted-foreground\">Creative</span>\n                                                </div>\n                                                <p className=\"text-xs text-muted-foreground mt-2\">\n                                                    Higher values make output more creative but less predictable\n                                                </p>\n                                            </div>\n                                        </div>\n                                    </CollapsibleContent>\n                                </Collapsible>\n\n                                {/* Error message */}\n                                {error && (\n                                    <div className=\"p-4 bg-destructive/10 border-destructive/20 border rounded-md text-sm text-destructive\">\n                                        <p className=\"font-medium mb-1\">Error</p>\n                                        <p>{error.message}</p>\n                                    </div>\n                                )}\n\n                                {/* Generated content */}\n                                {generatedResult && (\n                                    <div ref={resultRef} className=\"space-y-2.5\">\n                                        <div className=\"flex items-center justify-between\">\n                                            <h4 className=\"text-sm font-medium flex items-center\">\n                                                Generated content\n                                                <span className=\"ml-2 inline-flex h-2 w-2 bg-emerald-400 rounded-full\"></span>\n                                            </h4>\n                                            <div className=\"flex gap-1.5\">\n                                                <TooltipProvider>\n                                                    <Tooltip>\n                                                        <TooltipTrigger asChild>\n                                                            <Button\n                                                                variant=\"ghost\"\n                                                                size=\"icon\"\n                                                                className=\"h-7 w-7 rounded-full hover:bg-primary/10\"\n                                                                onClick={handleCopy}\n                                                            >\n                                                                {copied ?\n                                                                    <Check className=\"h-3.5 w-3.5 text-emerald-500\" /> :\n                                                                    <Copy className=\"h-3.5 w-3.5\" />\n                                                                }\n                                                            </Button>\n                                                        </TooltipTrigger>\n                                                        <TooltipContent side=\"top\" align=\"center\">\n                                                            {copied ? \"Copied!\" : \"Copy to clipboard\"}\n                                                        </TooltipContent>\n                                                    </Tooltip>\n                                                </TooltipProvider>\n\n                                                {onRegenerate && (\n                                                    <TooltipProvider>\n                                                        <Tooltip>\n                                                            <TooltipTrigger asChild>\n                                                                <Button\n                                                                    variant=\"ghost\"\n                                                                    size=\"icon\"\n                                                                    className=\"h-7 w-7 rounded-full hover:bg-primary/10\"\n                                                                    onClick={onRegenerate}\n                                                                    disabled={isRegenerating}\n                                                                >\n                                                                    {isRegenerating ? (\n                                                                        <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />\n                                                                    ) : (\n                                                                        <RefreshCw className=\"h-3.5 w-3.5\" />\n                                                                    )}\n                                                                </Button>\n                                                            </TooltipTrigger>\n                                                            <TooltipContent side=\"top\" align=\"center\">\n                                                                {isRegenerating ? \"Regenerating...\" : \"Regenerate content\"}\n                                                            </TooltipContent>\n                                                        </Tooltip>\n                                                    </TooltipProvider>\n                                                )}\n                                            </div>\n                                        </div>\n                                        <div className=\"p-4 bg-primary/5 border border-primary/20 rounded-md text-sm max-h-[250px] overflow-y-auto break-words whitespace-pre-wrap shadow-sm\">\n                                            {generatedResult.text}\n                                        </div>\n\n                                        {/* Metadata */}\n                                        {generatedResult.metadata && (\n                                            <p className=\"text-xs text-muted-foreground flex items-center\">\n                        <span className=\"bg-muted/50 px-2 py-0.5 rounded-full mr-2\">\n                          {generatedResult.metadata.model}\n                        </span>\n                                                {generatedResult.usage && (\n                                                    <span className=\"bg-muted/50 px-2 py-0.5 rounded-full\">\n                            {generatedResult.usage.total_tokens} tokens\n                          </span>\n                                                )}\n                                            </p>\n                                        )}\n                                    </div>\n                                )}\n                            </div>\n                        </ScrollArea>\n\n                        {/* Footer with buttons */}\n                        <div className=\"p-4 border-t flex justify-between items-center gap-3 bg-muted/20\">\n                            <Button\n                                variant=\"outline\"\n                                onClick={onClose}\n                                disabled={isLoading}\n                                className=\"min-w-[80px]\"\n                            >\n                                Cancel\n                            </Button>\n\n                            {generatedResult ? (\n                                <Button\n                                    onClick={() => onApply(generatedResult.text)}\n                                    className=\"gap-1.5 px-4 bg-primary/90 hover:bg-primary\"\n                                >\n                                    <span>Insert</span>\n                                    <ArrowRight className=\"w-4 h-4\" />\n                                </Button>\n                            ) : (\n                                <Button\n                                    onClick={onGenerate}\n                                    disabled={isLoading || isApiKeyValid === false || (completionType === AICompletionType.CUSTOM && !customPrompt.trim())}\n                                    className=\"gap-1.5 min-w-[120px] bg-primary/90 hover:bg-primary\"\n                                >\n                                    {isLoading ? (\n                                        <>\n                                            <Loader2 className=\"w-4 h-4 animate-spin\" />\n                                            <span>Generating...</span>\n                                        </>\n                                    ) : (\n                                        <>\n                                            <Sparkles className=\"w-4 h-4\" />\n                                            <span>Generate</span>\n                                        </>\n                                    )}\n                                </Button>\n                            )}\n                        </div>\n                    </motion.div>\n                </motion.div>\n            )}\n        </AnimatePresence>\n    );\n}"
  },
  {
    "path": "components/markdown-editor/hooks/useCUMModals.ts",
    "content": "// components/markdown-editor/hooks/useCUMModals.ts\n\nimport { useState, useCallback } from 'react';\nimport { CUMExtensionType } from '@/components/markdown-editor/types/cum-extensions';\n\nexport interface CUMModalState {\n    buttonModal: boolean;\n    alertModal: boolean;\n    embedModal: boolean;\n    tableModal: boolean;\n}\n\nexport interface UseCUMModalsReturn {\n    modals: CUMModalState;\n    openModal: (type: CUMExtensionType) => void;\n    closeModal: (type: CUMExtensionType) => void;\n    closeAllModals: () => void;\n}\n\nexport function useCUMModals(): UseCUMModalsReturn {\n    const [modals, setModals] = useState<CUMModalState>({\n        buttonModal: false,\n        alertModal: false,\n        embedModal: false,\n        tableModal: false,\n    });\n\n    const openModal = useCallback((type: CUMExtensionType) => {\n        setModals(prev => ({\n            ...prev,\n            [`${type}Modal`]: true,\n        }));\n    }, []);\n\n    const closeModal = useCallback((type: CUMExtensionType) => {\n        setModals(prev => ({\n            ...prev,\n            [`${type}Modal`]: false,\n        }));\n    }, []);\n\n    const closeAllModals = useCallback(() => {\n        setModals({\n            buttonModal: false,\n            alertModal: false,\n            embedModal: false,\n            tableModal: false,\n        });\n    }, []);\n\n    return {\n        modals,\n        openModal,\n        closeModal,\n        closeAllModals,\n    };\n}"
  },
  {
    "path": "components/markdown-editor/index.ts",
    "content": "export {MarkdownEditor} from './MarkdownEditor';\nexport {MarkdownPreview} from './MarkdownPreview';\nexport type {MarkdownEditorProps} from './MarkdownEditor';"
  },
  {
    "path": "components/markdown-editor/modals/CUMAlertModal.tsx",
    "content": "// components/markdown-editor/modals/CUMAlertModal.tsx\n\n'use client';\n\nimport React, {useState} from 'react';\nimport {Button} from '@/components/ui/button';\nimport {Input} from '@/components/ui/input';\nimport {Label} from '@/components/ui/label';\nimport {Textarea} from '@/components/ui/textarea';\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogFooter,\n    DialogHeader,\n    DialogTitle,\n} from '@/components/ui/dialog';\nimport {Eye, AlertTriangle, Info, CheckCircle, XCircle, Lightbulb} from 'lucide-react';\nimport {CUMModalProps, CUMAlertConfig} from '@/components/markdown-editor/types/cum-extensions';\n\nconst alertTypes = [\n    {value: 'info', label: 'Info', icon: Info, color: 'bg-blue-50 border-blue-200 text-blue-800'},\n    {value: 'warning', label: 'Warning', icon: AlertTriangle, color: 'bg-yellow-50 border-yellow-200 text-yellow-800'},\n    {value: 'error', label: 'Error', icon: XCircle, color: 'bg-red-50 border-red-200 text-red-800'},\n    {value: 'success', label: 'Success', icon: CheckCircle, color: 'bg-green-50 border-green-200 text-green-800'},\n    {value: 'tip', label: 'Tip', icon: Lightbulb, color: 'bg-purple-50 border-purple-200 text-purple-800'},\n] as const;\n\nexport const CUMAlertModal: React.FC<CUMModalProps> = ({\n                                                           isOpen,\n                                                           onClose,\n                                                           onInsert,\n                                                       }) => {\n    const [config, setConfig] = useState<CUMAlertConfig>({\n        type: 'info',\n        title: '',\n        content: 'Your alert message goes here...',\n        dismissible: false,\n    });\n\n    const [preview, setPreview] = useState(false);\n\n    const handleInsert = () => {\n        const titleLine = config.title ? ` ${config.title}` : '';\n        const markdown = `:::${config.type}${titleLine}\\n${config.content}\\n:::`;\n\n        onInsert(markdown);\n        onClose();\n    };\n\n    const handleReset = () => {\n        setConfig({\n            type: 'info',\n            title: '',\n            content: 'Your alert message goes here...',\n            dismissible: false,\n        });\n    };\n\n    const selectedType = alertTypes.find(type => type.value === config.type);\n    const previewMarkdown = `:::${config.type}${config.title ? ` ${config.title}` : ''}\\n${config.content}\\n:::`;\n\n    return (\n        <Dialog open={isOpen} onOpenChange={onClose}>\n            <DialogContent className=\"sm:max-w-[600px]\">\n                <DialogHeader>\n                    <DialogTitle className=\"flex items-center gap-2\">\n                        <span className=\"text-2xl\">⚠️</span>\n                        Create Alert\n                    </DialogTitle>\n                    <DialogDescription>\n                        Create a styled alert box to highlight important information, warnings, or tips.\n                    </DialogDescription>\n                </DialogHeader>\n\n                <div className=\"grid gap-6 py-4\">\n                    {/* Alert Type */}\n                    <div className=\"grid gap-2\">\n                        <Label>Alert Type</Label>\n                        <div className=\"grid grid-cols-5 gap-2\">\n                            {alertTypes.map((type) => {\n                                const Icon = type.icon;\n                                return (\n                                    <Button\n                                        key={type.value}\n                                        variant={config.type === type.value ? 'default' : 'outline'}\n                                        size=\"sm\"\n                                        onClick={() => setConfig(prev => ({...prev, type: type.value}))}\n                                        className=\"flex flex-col gap-1 h-auto py-3\"\n                                    >\n                                        <Icon className=\"w-4 h-4\"/>\n                                        <span className=\"text-xs\">{type.label}</span>\n                                    </Button>\n                                );\n                            })}\n                        </div>\n                    </div>\n\n                    {/* Title (Optional) */}\n                    <div className=\"grid gap-2\">\n                        <Label htmlFor=\"alert-title\">Title (Optional)</Label>\n                        <Input\n                            id=\"alert-title\"\n                            value={config.title}\n                            onChange={(e) => setConfig(prev => ({...prev, title: e.target.value}))}\n                            placeholder=\"Leave empty to use default title...\"\n                        />\n                    </div>\n\n                    {/* Content */}\n                    <div className=\"grid gap-2\">\n                        <Label htmlFor=\"alert-content\">Alert Content</Label>\n                        <Textarea\n                            id=\"alert-content\"\n                            value={config.content}\n                            onChange={(e) => setConfig(prev => ({...prev, content: e.target.value}))}\n                            placeholder=\"Enter your alert message...\"\n                            rows={4}\n                        />\n                    </div>\n\n                    {/* Preview */}\n                    <div className=\"grid gap-2\">\n                        <div className=\"flex items-center justify-between\">\n                            <Label>Preview</Label>\n                            <Button\n                                variant=\"ghost\"\n                                size=\"sm\"\n                                onClick={() => setPreview(!preview)}\n                            >\n                                <Eye className=\"w-4 h-4 mr-1\"/>\n                                {preview ? 'Hide' : 'Show'} Markdown\n                            </Button>\n                        </div>\n\n                        {preview ? (\n                            <div className=\"p-3 bg-muted rounded-md font-mono text-sm whitespace-pre-wrap\">\n                                {previewMarkdown}\n                            </div>\n                        ) : (\n                            <div className={`border-l-4 p-4 rounded-md ${selectedType?.color}`}>\n                                <div className=\"flex items-center gap-2 font-medium mb-2\">\n                                    {selectedType && <selectedType.icon className=\"w-4 h-4\"/>}\n                                    {config.title || selectedType?.label}\n                                </div>\n                                <div className=\"text-sm\">\n                                    {config.content}\n                                </div>\n                            </div>\n                        )}\n                    </div>\n\n                    {/* Quick Templates */}\n                    <div className=\"grid gap-2\">\n                        <Label>Quick Templates</Label>\n                        <div className=\"grid grid-cols-2 gap-2\">\n                            <Button\n                                variant=\"outline\"\n                                size=\"sm\"\n                                onClick={() => setConfig(prev => ({\n                                    ...prev,\n                                    type: 'warning',\n                                    title: 'Breaking Change',\n                                    content: 'This update includes breaking changes. Please review the migration guide before upgrading.'\n                                }))}\n                            >\n                                Breaking Change\n                            </Button>\n                            <Button\n                                variant=\"outline\"\n                                size=\"sm\"\n                                onClick={() => setConfig(prev => ({\n                                    ...prev,\n                                    type: 'tip',\n                                    title: 'Pro Tip',\n                                    content: 'You can use keyboard shortcuts to speed up your workflow. Press Ctrl+K to open the command palette.'\n                                }))}\n                            >\n                                Pro Tip\n                            </Button>\n                            <Button\n                                variant=\"outline\"\n                                size=\"sm\"\n                                onClick={() => setConfig(prev => ({\n                                    ...prev,\n                                    type: 'info',\n                                    title: 'New Feature',\n                                    content: 'We\\'ve added a new feature that makes it easier to collaborate with your team.'\n                                }))}\n                            >\n                                New Feature\n                            </Button>\n                            <Button\n                                variant=\"outline\"\n                                size=\"sm\"\n                                onClick={() => setConfig(prev => ({\n                                    ...prev,\n                                    type: 'error',\n                                    title: 'Known Issue',\n                                    content: 'We\\'re aware of an issue that may affect some users. We\\'re working on a fix and will update this page when it\\'s resolved.'\n                                }))}\n                            >\n                                Known Issue\n                            </Button>\n                        </div>\n                    </div>\n                </div>\n\n                <DialogFooter>\n                    <Button variant=\"outline\" onClick={handleReset}>\n                        Reset\n                    </Button>\n                    <Button variant=\"outline\" onClick={onClose}>\n                        Cancel\n                    </Button>\n                    <Button onClick={handleInsert}>\n                        Insert Alert\n                    </Button>\n                </DialogFooter>\n            </DialogContent>\n        </Dialog>\n    );\n};"
  },
  {
    "path": "components/markdown-editor/modals/CUMButtonModal.tsx",
    "content": "'use client';\n\nimport React, {useState} from 'react';\nimport {Button} from '@/components/ui/button';\nimport {Input} from '@/components/ui/input';\nimport {Label} from '@/components/ui/label';\nimport {\n    Select,\n    SelectContent,\n    SelectItem,\n    SelectTrigger,\n    SelectValue,\n} from '@/components/ui/select';\nimport {\n    Dialog,\n    DialogContent,\n    DialogFooter,\n    DialogHeader,\n    DialogTitle,\n} from '@/components/ui/dialog';\nimport {Switch} from '@/components/ui/switch';\nimport {Badge} from '@/components/ui/badge';\nimport {Card, CardContent} from '@/components/ui/card';\nimport {Separator} from '@/components/ui/separator';\nimport {\n    Link,\n    Eye,\n    EyeOff,\n    RotateCcw,\n    Sparkles,\n    MousePointer,\n    ExternalLink\n} from 'lucide-react';\nimport {renderMarkdown} from '@/lib/services/core/markdown/useCustomExtensions';\nimport {CUMModalProps, CUMButtonConfig} from '@/components/markdown-editor/types/cum-extensions';\n\nexport const CUMButtonModal: React.FC<CUMModalProps> = ({\n                                                            isOpen,\n                                                            onClose,\n                                                            onInsert,\n                                                        }) => {\n    const [config, setConfig] = useState<CUMButtonConfig>({\n        text: 'Click Me',\n        url: 'https://example.com',\n        style: 'primary',\n        size: 'md',\n        target: '_blank',\n        disabled: false,\n    });\n\n    const [showPreview, setShowPreview] = useState(true);\n\n    const handleInsert = () => {\n        const options = [];\n        if (config.style !== 'primary') options.push(config.style);\n        if (config.size !== 'md') options.push(config.size);\n        if (config.disabled) options.push('disabled');\n        if (config.target !== '_blank') options.push('self');\n\n        const optionsString = options.length > 0 ? `{${options.join(',')}}` : '';\n        const markdown = `[button:${config.text}](${config.url})${optionsString}`;\n\n        onInsert(markdown);\n        onClose();\n    };\n\n    const handleReset = () => {\n        setConfig({\n            text: 'Click Me',\n            url: 'https://example.com',\n            style: 'primary',\n            size: 'md',\n            target: '_blank',\n            disabled: false,\n        });\n    };\n\n    const generateMarkdown = () => {\n        const options = [];\n        if (config.style !== 'primary') options.push(config.style);\n        if (config.size !== 'md') options.push(config.size);\n        if (config.disabled) options.push('disabled');\n        if (config.target !== '_blank') options.push('self');\n\n        const optionsString = options.length > 0 ? `{${options.join(',')}}` : '';\n        return `[button:${config.text}](${config.url})${optionsString}`;\n    };\n\n    const getStyleColor = (style: string) => {\n        const colors: Record<string, string> = {\n            default: 'bg-slate-100 text-slate-800',\n            primary: 'bg-blue-100 text-blue-800',\n            secondary: 'bg-gray-100 text-gray-800',\n            success: 'bg-green-100 text-green-800',\n            danger: 'bg-red-100 text-red-800',\n            outline: 'bg-purple-100 text-purple-800',\n            ghost: 'bg-orange-100 text-orange-800'\n        };\n        return colors[style] || colors.primary;\n    };\n\n    return (\n        <Dialog open={isOpen} onOpenChange={onClose}>\n            <DialogContent className=\"sm:max-w-[700px] max-h-[85vh] overflow-hidden\">\n                <DialogHeader className=\"space-y-3\">\n                    <DialogTitle className=\"flex items-center gap-3\">\n                        <div className=\"p-2 rounded-lg bg-blue-100 dark:bg-blue-900/30\">\n                            <MousePointer className=\"h-5 w-5 text-blue-600 dark:text-blue-400\"/>\n                        </div>\n                        <div>\n                            <div className=\"text-xl font-semibold\">Create Interactive Button</div>\n                            <div className=\"text-sm text-muted-foreground font-normal\">\n                                Design a clickable button with custom styling and behavior\n                            </div>\n                        </div>\n                    </DialogTitle>\n                </DialogHeader>\n\n                <div className=\"grid grid-cols-2 gap-6 py-4\">\n                    {/* Configuration Panel */}\n                    <div className=\"space-y-4\">\n                        <div className=\"space-y-3\">\n                            <div className=\"flex items-center gap-2\">\n                                <Sparkles className=\"h-4 w-4 text-blue-500\"/>\n                                <Label className=\"text-sm font-medium\">Content & Destination</Label>\n                            </div>\n\n                            <div className=\"space-y-3\">\n                                <div>\n                                    <Label htmlFor=\"button-text\" className=\"text-xs text-muted-foreground\">\n                                        Button Text\n                                    </Label>\n                                    <Input\n                                        id=\"button-text\"\n                                        value={config.text}\n                                        onChange={(e) => setConfig(prev => ({...prev, text: e.target.value}))}\n                                        placeholder=\"Enter button text...\"\n                                        className=\"h-9\"\n                                    />\n                                </div>\n\n                                <div>\n                                    <Label htmlFor=\"button-url\" className=\"text-xs text-muted-foreground\">\n                                        URL Destination\n                                    </Label>\n                                    <Input\n                                        id=\"button-url\"\n                                        type=\"url\"\n                                        value={config.url}\n                                        onChange={(e) => setConfig(prev => ({...prev, url: e.target.value}))}\n                                        placeholder=\"https://example.com\"\n                                        className=\"h-9\"\n                                    />\n                                </div>\n                            </div>\n                        </div>\n\n                        <Separator/>\n\n                        <div className=\"space-y-3\">\n                            <Label className=\"text-sm font-medium\">Appearance</Label>\n\n                            <div className=\"grid grid-cols-2 gap-3\">\n                                <div>\n                                    <Label className=\"text-xs text-muted-foreground\">Style</Label>\n                                    <Select\n                                        value={config.style}\n                                        onValueChange={(value) =>\n                                            setConfig(prev => ({...prev, style: value as CUMButtonConfig['style']}))\n                                        }\n                                    >\n                                        <SelectTrigger className=\"h-9\">\n                                            <SelectValue/>\n                                        </SelectTrigger>\n                                        <SelectContent>\n                                            <SelectItem value=\"default\">Default</SelectItem>\n                                            <SelectItem value=\"primary\">Primary</SelectItem>\n                                            <SelectItem value=\"secondary\">Secondary</SelectItem>\n                                            <SelectItem value=\"success\">Success</SelectItem>\n                                            <SelectItem value=\"danger\">Danger</SelectItem>\n                                            <SelectItem value=\"outline\">Outline</SelectItem>\n                                            <SelectItem value=\"ghost\">Ghost</SelectItem>\n                                        </SelectContent>\n                                    </Select>\n                                </div>\n\n                                <div>\n                                    <Label className=\"text-xs text-muted-foreground\">Size</Label>\n                                    <Select\n                                        value={config.size}\n                                        onValueChange={(value) =>\n                                            setConfig(prev => ({...prev, size: value as CUMButtonConfig['size']}))\n                                        }\n                                    >\n                                        <SelectTrigger className=\"h-9\">\n                                            <SelectValue/>\n                                        </SelectTrigger>\n                                        <SelectContent>\n                                            <SelectItem value=\"sm\">Small</SelectItem>\n                                            <SelectItem value=\"md\">Medium</SelectItem>\n                                            <SelectItem value=\"lg\">Large</SelectItem>\n                                        </SelectContent>\n                                    </Select>\n                                </div>\n                            </div>\n                        </div>\n\n                        <Separator/>\n\n                        <div className=\"space-y-3\">\n                            <Label className=\"text-sm font-medium\">Behavior</Label>\n\n                            <div className=\"space-y-3\">\n                                <div className=\"flex items-center justify-between\">\n                                    <div className=\"flex items-center gap-2\">\n                                        <ExternalLink className=\"h-3 w-3 text-muted-foreground\"/>\n                                        <Label htmlFor=\"new-tab\" className=\"text-xs\">Open in new tab</Label>\n                                    </div>\n                                    <Switch\n                                        id=\"new-tab\"\n                                        checked={config.target === '_blank'}\n                                        onCheckedChange={(checked) =>\n                                            setConfig(prev => ({...prev, target: checked ? '_blank' : '_self'}))\n                                        }\n                                    />\n                                </div>\n\n                                <div className=\"flex items-center justify-between\">\n                                    <div className=\"flex items-center gap-2\">\n                                        <div className=\"h-3 w-3 rounded-full border border-muted-foreground/30\"/>\n                                        <Label htmlFor=\"disabled\" className=\"text-xs\">Disabled state</Label>\n                                    </div>\n                                    <Switch\n                                        id=\"disabled\"\n                                        checked={config.disabled}\n                                        onCheckedChange={(checked) =>\n                                            setConfig(prev => ({...prev, disabled: checked}))\n                                        }\n                                    />\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n\n                    {/* Preview Panel */}\n                    <div className=\"space-y-4\">\n                        <div className=\"flex items-center justify-between\">\n                            <div className=\"flex items-center gap-2\">\n                                <Eye className=\"h-4 w-4 text-green-500\"/>\n                                <Label className=\"text-sm font-medium\">Live Preview</Label>\n                                <Badge variant=\"secondary\" className={getStyleColor(config.style)}>\n                                    {config.style}\n                                </Badge>\n                            </div>\n\n                            <Button\n                                variant=\"ghost\"\n                                size=\"sm\"\n                                onClick={() => setShowPreview(!showPreview)}\n                            >\n                                {showPreview ? <EyeOff className=\"h-3 w-3\"/> : <Eye className=\"h-3 w-3\"/>}\n                            </Button>\n                        </div>\n\n                        <Card className=\"border-2 border-dashed\">\n                            <CardContent className=\"p-6\">\n                                {showPreview ? (\n                                    <div className=\"space-y-4\">\n                                        <div className=\"text-xs text-muted-foreground text-center\">\n                                            Button Preview\n                                        </div>\n                                        <div\n                                            className=\"flex justify-center\"\n                                            dangerouslySetInnerHTML={{\n                                                __html: renderMarkdown(generateMarkdown())\n                                            }}\n                                        />\n                                    </div>\n                                ) : (\n                                    <div className=\"space-y-2\">\n                                        <div className=\"text-xs text-muted-foreground\">\n                                            Generated Markdown\n                                        </div>\n                                        <div className=\"p-3 bg-muted rounded-md font-mono text-xs break-all\">\n                                            {generateMarkdown()}\n                                        </div>\n                                    </div>\n                                )}\n                            </CardContent>\n                        </Card>\n\n                        <div className=\"text-xs text-muted-foreground space-y-1\">\n                            <div className=\"flex items-center gap-1\">\n                                <Link className=\"h-3 w-3\"/>\n                                <span>Target: {config.target === '_blank' ? 'New tab' : 'Same tab'}</span>\n                            </div>\n                            <div>Size: {config.size?.toUpperCase() || 'MD'}</div>\n                            {config.disabled && (\n                                <div className=\"text-amber-600 dark:text-amber-400\">\n                                    ⚠️ Button will be disabled\n                                </div>\n                            )}\n                        </div>\n                    </div>\n                </div>\n\n                <DialogFooter className=\"flex justify-between\">\n                    <Button\n                        variant=\"ghost\"\n                        onClick={handleReset}\n                        className=\"flex items-center gap-2\"\n                    >\n                        <RotateCcw className=\"h-3 w-3\"/>\n                        Reset\n                    </Button>\n\n                    <div className=\"flex gap-2\">\n                        <Button variant=\"outline\" onClick={onClose}>\n                            Cancel\n                        </Button>\n                        <Button\n                            onClick={handleInsert}\n                            className=\"flex items-center gap-2\"\n                        >\n                            <MousePointer className=\"h-3 w-3\"/>\n                            Insert Button\n                        </Button>\n                    </div>\n                </DialogFooter>\n            </DialogContent>\n        </Dialog>\n    );\n};"
  },
  {
    "path": "components/markdown-editor/modals/CUMEmbedModal.tsx",
    "content": "// components/markdown-editor/modals/CUMEmbedModal.tsx\n\n'use client';\n\nimport React, {useState, useEffect} from 'react';\nimport {Button} from '@/components/ui/button';\nimport {Input} from '@/components/ui/input';\nimport {Label} from '@/components/ui/label';\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogFooter,\n    DialogHeader,\n    DialogTitle,\n} from '@/components/ui/dialog';\nimport {Badge} from '@/components/ui/badge';\nimport {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs';\nimport {Switch} from '@/components/ui/switch';\nimport {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select';\nimport {Eye, Settings, Sparkles} from 'lucide-react';\n\nimport {\n    SiYoutube,\n    SiCodepen,\n    SiFigma,\n    SiX,\n    SiGithub,\n    SiGooglechrome,\n    SiVimeo,\n    SiSpotify,\n    SiCodesandbox\n} from '@icons-pack/react-simple-icons';\n\nimport {CUMModalProps} from '@/components/markdown-editor/types/cum-extensions';\n\n// Enhanced type definitions\ntype EmbedProvider = typeof embedProviders[number]['value'];\n\ninterface EmbedOptions {\n    width?: string;\n    height?: string;\n    autoplay?: boolean;\n    mute?: boolean;\n    loop?: boolean;\n    controls?: boolean;\n    start?: string;\n    theme?: 'light' | 'dark';\n    tab?: 'result' | 'html' | 'css' | 'js';\n    view?: 'preview' | 'editor' | 'split';\n\n    [key: string]: string | number | boolean | undefined;\n}\n\ninterface CUMEmbedConfig {\n    provider: EmbedProvider;\n    url: string;\n    options: EmbedOptions;\n}\n\nconst embedProviders = [\n    {\n        value: 'youtube',\n        label: 'YouTube',\n        icon: SiYoutube,\n        color: '#FF0000',\n        placeholder: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',\n        description: 'Embed YouTube videos',\n        supportedOptions: ['autoplay', 'mute', 'loop', 'controls', 'start', 'height'],\n        tips: [\n            'Works with youtube.com, youtu.be, and YouTube Shorts URLs',\n            'Video ID is automatically extracted',\n            'Creates responsive embedded player with 16:9 aspect ratio',\n            'Supports autoplay, mute, loop, and start time options'\n        ]\n    },\n    {\n        value: 'vimeo',\n        label: 'Vimeo',\n        icon: SiVimeo,\n        color: '#1AB7EA',\n        placeholder: 'https://vimeo.com/753580183',\n        description: 'Embed Vimeo videos',\n        supportedOptions: ['autoplay', 'mute', 'loop', 'height'],\n        tips: [\n            'High-quality video embedding',\n            'Works with all Vimeo video URLs',\n            'Responsive player with clean design',\n            'Supports autoplay and loop options'\n        ]\n    },\n    {\n        value: 'codepen',\n        label: 'CodePen',\n        icon: SiCodepen,\n        color: '#000000',\n        placeholder: 'https://codepen.io/jcoulterdesign/pen/abYNyLq',\n        description: 'Embed CodePen demos',\n        supportedOptions: ['height', 'theme', 'tab'],\n        tips: [\n            'Perfect for showcasing code examples',\n            'Interactive demos work in preview',\n            'Shows HTML, CSS, and JS tabs',\n            'Supports light and dark themes'\n        ]\n    },\n    {\n        value: 'codesandbox',\n        label: 'CodeSandbox',\n        icon: SiCodesandbox,\n        color: '#040404',\n        placeholder: 'https://codesandbox.io/p/sandbox/vite-hp9yj',\n        description: 'Embed CodeSandbox projects',\n        supportedOptions: ['height', 'view'],\n        tips: [\n            'Full development environment in browser',\n            'Perfect for React, Vue, and other frameworks',\n            'Live preview and file explorer',\n            'Supports editor and preview views'\n        ]\n    },\n    {\n        value: 'figma',\n        label: 'Figma',\n        icon: SiFigma,\n        color: '#F24E1E',\n        placeholder: 'https://www.figma.com/community/file/804111521375743796/figma-file-template',\n        description: 'Embed Figma designs',\n        supportedOptions: ['height'],\n        tips: [\n            'Share design prototypes and wireframes',\n            'Interactive prototypes work in embed',\n            'Viewers can inspect design elements',\n            'Perfect for design system documentation'\n        ]\n    },\n    {\n        value: 'spotify',\n        label: 'Spotify',\n        icon: SiSpotify,\n        color: '#1DB954',\n        placeholder: 'https://open.spotify.com/track/4iV5W9uYEdYUVa79Axb7Rh',\n        description: 'Embed Spotify tracks/playlists',\n        supportedOptions: ['height'],\n        tips: [\n            'Embed tracks, albums, playlists, and podcasts',\n            'Works with all Spotify open.spotify.com URLs',\n            'Includes play controls and track info',\n            'Great for music-related content'\n        ]\n    },\n    {\n        value: 'twitter',\n        label: 'Twitter/X',\n        icon: SiX,\n        color: '#1DA1F2',\n        placeholder: 'https://x.com/supernova3339/status/1947451911096992097',\n        description: 'Embed tweets',\n        supportedOptions: [],\n        tips: [\n            'Works with twitter.com and x.com URLs',\n            'Shows tweet preview card',\n            'Includes author and engagement info',\n            'Links to original tweet'\n        ]\n    },\n    {\n        value: 'github',\n        label: 'GitHub',\n        icon: SiGithub,\n        color: '#181717',\n        placeholder: 'https://github.com/supernova3339/changerawr',\n        description: 'Embed GitHub repos',\n        supportedOptions: [],\n        tips: [\n            'Shows repository information card',\n            'Displays repo name and description',\n            'Links to live repository',\n            'Perfect for open source showcases'\n        ]\n    },\n    {\n        value: 'generic',\n        label: 'Generic Link',\n        icon: SiGooglechrome,\n        color: '#4285F4',\n        placeholder: 'https://example.com',\n        description: 'Generic link embed',\n        supportedOptions: [],\n        tips: [\n            'Works with any HTTPS URL',\n            'Creates a preview card',\n            'Shows domain and link',\n            'Fallback for unsupported platforms'\n        ]\n    },\n] as const;\n\nconst optionDefinitions = {\n    autoplay: {label: 'Autoplay', type: 'boolean', description: 'Start playing automatically'},\n    mute: {label: 'Mute', type: 'boolean', description: 'Start muted (required for autoplay)'},\n    loop: {label: 'Loop', type: 'boolean', description: 'Loop the video continuously'},\n    controls: {label: 'Show Controls', type: 'boolean', description: 'Show player controls'},\n    start: {label: 'Start Time', type: 'number', description: 'Start time in seconds'},\n    height: {\n        label: 'Height',\n        type: 'select',\n        options: ['300', '400', '500', '600'],\n        description: 'Embed height in pixels'\n    },\n    theme: {label: 'Theme', type: 'select', options: ['light', 'dark'], description: 'Color theme'},\n    tab: {\n        label: 'Default Tab',\n        type: 'select',\n        options: ['result', 'html', 'css', 'js'],\n        description: 'Default CodePen tab'\n    },\n    view: {\n        label: 'View Mode',\n        type: 'select',\n        options: ['preview', 'editor', 'split'],\n        description: 'CodeSandbox view mode'\n    }\n} as const;\n\nexport const CUMEmbedModal: React.FC<CUMModalProps> = ({\n                                                           isOpen,\n                                                           onClose,\n                                                           onInsert,\n                                                       }) => {\n    const [config, setConfig] = useState<CUMEmbedConfig>({\n        provider: 'youtube',\n        url: '',\n        options: {},\n    });\n\n    const [urlValid, setUrlValid] = useState(false);\n\n    // Validate URL when it changes\n    useEffect(() => {\n        const provider = embedProviders.find(p => p.value === config.provider);\n        if (!provider || !config.url) {\n            setUrlValid(false);\n            return;\n        }\n\n        try {\n            const url = new URL(config.url);\n            let valid = false;\n\n            switch (config.provider) {\n                case 'youtube':\n                    valid = url.hostname.includes('youtube.com') || url.hostname.includes('youtu.be');\n                    break;\n                case 'vimeo':\n                    valid = url.hostname.includes('vimeo.com');\n                    break;\n                case 'codepen':\n                    valid = url.hostname.includes('codepen.io');\n                    break;\n                case 'codesandbox':\n                    valid = url.hostname.includes('codesandbox.io');\n                    break;\n                case 'figma':\n                    valid = url.hostname.includes('figma.com');\n                    break;\n                case 'spotify':\n                    valid = url.hostname.includes('open.spotify.com');\n                    break;\n                case 'twitter':\n                    valid = url.hostname.includes('twitter.com') || url.hostname.includes('x.com');\n                    break;\n                case 'github':\n                    valid = url.hostname.includes('github.com');\n                    break;\n                case 'generic':\n                    valid = url.protocol === 'http:' || url.protocol === 'https:';\n                    break;\n                default:\n                    valid = false;\n            }\n\n            setUrlValid(valid);\n        } catch {\n            setUrlValid(false);\n        }\n    }, [config.url, config.provider]);\n\n    const handleInsert = () => {\n        if (!urlValid) return;\n\n        // Build options string\n        const optionsArray: string[] = [];\n        Object.entries(config.options).forEach(([key, value]) => {\n            if (value !== undefined && value !== false && value !== '') {\n                if (typeof value === 'boolean') {\n                    optionsArray.push(`${key}:${value ? '1' : '0'}`);\n                } else {\n                    optionsArray.push(`${key}:${value}`);\n                }\n            }\n        });\n\n        const optionsString = optionsArray.length > 0 ? `{${optionsArray.join(',')}}` : '';\n        const markdown = `[embed:${config.provider}](${config.url})${optionsString}`;\n\n        onInsert(markdown);\n        onClose();\n    };\n\n    const handleReset = () => {\n        setConfig({\n            provider: 'youtube',\n            url: '',\n            options: {},\n        });\n    };\n\n    const handleQuickFill = (exampleUrl: string) => {\n        setConfig(prev => ({...prev, url: exampleUrl}));\n    };\n\n    const handleProviderChange = (newProvider: string) => {\n        // Type guard to ensure newProvider is a valid provider value\n        const isValidProvider = embedProviders.some(p => p.value === newProvider);\n        if (!isValidProvider) return;\n\n        setConfig(prev => ({\n            ...prev,\n            provider: newProvider as typeof embedProviders[number]['value'],\n            url: '',\n            options: {} // Reset options when changing provider\n        }));\n    };\n\n    const handleOptionChange = (optionKey: string, value: string | number | boolean) => {\n        setConfig(prev => ({\n            ...prev,\n            options: {\n                ...prev.options,\n                [optionKey]: value\n            }\n        }));\n    };\n\n    const selectedProvider = embedProviders.find(p => p.value === config.provider);\n\n    // Build preview Markdown\n    const optionsArray: string[] = [];\n    Object.entries(config.options).forEach(([key, value]) => {\n        if (value !== undefined && value !== false && value !== '') {\n            if (typeof value === 'boolean') {\n                optionsArray.push(`${key}:${value ? '1' : '0'}`);\n            } else {\n                optionsArray.push(`${key}:${value}`);\n            }\n        }\n    });\n    const optionsString = optionsArray.length > 0 ? `{${optionsArray.join(',')}}` : '';\n    const previewMarkdown = `[embed:${config.provider}](${config.url})${optionsString}`;\n\n    return (\n        <Dialog open={isOpen} onOpenChange={onClose}>\n            <DialogContent className=\"sm:max-w-[800px] max-h-[90vh] overflow-y-auto\">\n                <DialogHeader>\n                    <DialogTitle className=\"flex items-center gap-2\">\n                        <span className=\"text-2xl\">🎬</span>\n                        Create Embed\n                    </DialogTitle>\n                    <DialogDescription>\n                        Embed external content like videos, code demos, designs, and more into your markdown.\n                    </DialogDescription>\n                </DialogHeader>\n\n                <Tabs defaultValue=\"provider\" className=\"w-full\">\n                    <TabsList className=\"grid w-full grid-cols-3\">\n                        <TabsTrigger value=\"provider\">Provider</TabsTrigger>\n                        <TabsTrigger value=\"options\" disabled={!selectedProvider?.supportedOptions.length}>\n                            <Settings className=\"w-4 h-4 mr-1\"/>\n                            Options\n                        </TabsTrigger>\n                        <TabsTrigger value=\"preview\">\n                            <Eye className=\"w-4 h-4 mr-1\"/>\n                            Preview\n                        </TabsTrigger>\n                    </TabsList>\n\n                    <TabsContent value=\"provider\" className=\"space-y-6 mt-6\">\n                        {/* Provider Selection */}\n                        <div className=\"grid gap-3\">\n                            <Label>Content Provider</Label>\n                            <div className=\"grid grid-cols-2 gap-3\">\n                                {embedProviders.map((provider) => {\n                                    const Icon = provider.icon;\n                                    return (\n                                        <Button\n                                            key={provider.value}\n                                            variant={config.provider === provider.value ? 'default' : 'outline'}\n                                            size=\"sm\"\n                                            onClick={() => handleProviderChange(provider.value)}\n                                            className=\"flex items-center gap-3 h-auto py-4 px-4 justify-start\"\n                                        >\n                                            <Icon color={provider.color} size={20}/>\n                                            <div className=\"text-left\">\n                                                <div className=\"font-medium\">{provider.label}</div>\n                                                <div\n                                                    className=\"text-xs text-muted-foreground\">{provider.description}</div>\n                                            </div>\n                                        </Button>\n                                    );\n                                })}\n                            </div>\n                        </div>\n\n                        {/* URL Input */}\n                        <div className=\"grid gap-3\">\n                            <div className=\"flex items-center justify-between\">\n                                <Label htmlFor=\"embed-url\">Content URL</Label>\n                                {urlValid &&\n                                    <Badge variant=\"outline\" className=\"text-green-600 border-green-300\">✓ Valid\n                                        URL</Badge>}\n                                {config.url && !urlValid &&\n                                    <Badge variant=\"outline\" className=\"text-red-600 border-red-300\">✗ Invalid\n                                        URL</Badge>}\n                            </div>\n                            <Input\n                                id=\"embed-url\"\n                                type=\"url\"\n                                value={config.url}\n                                onChange={(e) => setConfig(prev => ({...prev, url: e.target.value}))}\n                                placeholder={selectedProvider?.placeholder}\n                                className={urlValid ? 'border-green-300 focus:border-green-500' : config.url && !urlValid ? 'border-red-300 focus:border-red-500' : ''}\n                            />\n                            {selectedProvider && (\n                                <div className=\"flex gap-2\">\n                                    <Button\n                                        variant=\"ghost\"\n                                        size=\"sm\"\n                                        onClick={() => handleQuickFill(selectedProvider.placeholder)}\n                                        className=\"text-xs\"\n                                    >\n                                        Use example URL\n                                    </Button>\n                                </div>\n                            )}\n                        </div>\n\n                        {/* Provider Tips */}\n                        {selectedProvider && (\n                            <div className=\"p-4 bg-muted/50 rounded-lg border\">\n                                <div className=\"flex items-center gap-2 mb-3\">\n                                    <selectedProvider.icon color={selectedProvider.color} size={16}/>\n                                    <span className=\"font-medium text-sm\">{selectedProvider.label} Tips</span>\n                                </div>\n                                <ul className=\"space-y-1 text-sm text-muted-foreground\">\n                                    {selectedProvider.tips.map((tip, index) => (\n                                        <li key={index} className=\"flex items-start gap-2\">\n                                            <span className=\"text-xs mt-0.5\">•</span>\n                                            <span>{tip}</span>\n                                        </li>\n                                    ))}\n                                </ul>\n                            </div>\n                        )}\n\n                        {/* Popular Examples */}\n                        <div className=\"grid gap-3\">\n                            <Label className=\"flex items-center gap-2\">\n                                <Sparkles className=\"w-4 h-4\"/>\n                                Popular Examples\n                            </Label>\n                            <div className=\"grid grid-cols-1 gap-2 text-sm\">\n                                <Button\n                                    variant=\"outline\"\n                                    size=\"sm\"\n                                    onClick={() => {\n                                        handleProviderChange('youtube');\n                                        handleQuickFill('https://www.youtube.com/watch?v=dQw4w9WgXcQ');\n                                    }}\n                                    className=\"justify-start h-auto py-2\"\n                                >\n                                    <SiYoutube color=\"#FF0000\" size={16} className=\"mr-2\"/>\n                                    Rick Astley - Never Gonna Give You Up\n                                </Button>\n                                <Button\n                                    variant=\"outline\"\n                                    size=\"sm\"\n                                    onClick={() => {\n                                        handleProviderChange('codepen');\n                                        handleQuickFill('https://codepen.io/team/codepen/pen/PNaGbb');\n                                    }}\n                                    className=\"justify-start h-auto py-2\"\n                                >\n                                    <SiCodepen color=\"#000000\" size={16} className=\"mr-2\"/>\n                                    CSS Animation Example\n                                </Button>\n                                <Button\n                                    variant=\"outline\"\n                                    size=\"sm\"\n                                    onClick={() => {\n                                        handleProviderChange('spotify');\n                                        handleQuickFill('https://open.spotify.com/track/4iV5W9uYEdYUVa79Axb7Rh');\n                                    }}\n                                    className=\"justify-start h-auto py-2\"\n                                >\n                                    <SiSpotify color=\"#1DB954\" size={16} className=\"mr-2\"/>\n                                    Spotify Track Example\n                                </Button>\n                                <Button\n                                    variant=\"outline\"\n                                    size=\"sm\"\n                                    onClick={() => {\n                                        handleProviderChange('github');\n                                        handleQuickFill('https://github.com/facebook/react');\n                                    }}\n                                    className=\"justify-start h-auto py-2\"\n                                >\n                                    <SiGithub color=\"#181717\" size={16} className=\"mr-2\"/>\n                                    React Repository\n                                </Button>\n                            </div>\n                        </div>\n                    </TabsContent>\n\n                    <TabsContent value=\"options\" className=\"space-y-6 mt-6\">\n                        {selectedProvider?.supportedOptions.length ? (\n                            <div className=\"space-y-6\">\n                                <div className=\"flex items-center gap-2\">\n                                    <Settings className=\"w-5 h-5\"/>\n                                    <Label className=\"text-base font-medium\">\n                                        {selectedProvider.label} Options\n                                    </Label>\n                                </div>\n\n                                <div className=\"grid gap-4\">\n                                    {selectedProvider.supportedOptions.map((optionKey) => {\n                                        const option = optionDefinitions[optionKey];\n                                        if (!option) return null;\n\n                                        if (option.type === 'boolean') {\n                                            return (\n                                                <div key={optionKey}\n                                                     className=\"flex items-center justify-between p-3 border rounded-lg\">\n                                                    <div>\n                                                        <Label className=\"font-medium\">{option.label}</Label>\n                                                        <p className=\"text-sm text-muted-foreground\">{option.description}</p>\n                                                    </div>\n                                                    <Switch\n                                                        checked={!!config.options[optionKey]}\n                                                        onCheckedChange={(checked) => handleOptionChange(optionKey, checked)}\n                                                    />\n                                                </div>\n                                            );\n                                        }\n\n                                        if (option.type === 'select') {\n                                            return (\n                                                <div key={optionKey} className=\"p-3 border rounded-lg space-y-2\">\n                                                    <Label className=\"font-medium\">{option.label}</Label>\n                                                    <p className=\"text-sm text-muted-foreground\">{option.description}</p>\n                                                    <Select\n                                                        value={String(config.options[optionKey] || '')}\n                                                        onValueChange={(value) => handleOptionChange(optionKey, value)}\n                                                    >\n                                                        <SelectTrigger>\n                                                            <SelectValue\n                                                                placeholder={`Select ${option.label.toLowerCase()}`}/>\n                                                        </SelectTrigger>\n                                                        <SelectContent>\n                                                            {option.options?.map((value) => (\n                                                                <SelectItem key={value} value={value}>\n                                                                    {value}\n                                                                </SelectItem>\n                                                            ))}\n                                                        </SelectContent>\n                                                    </Select>\n                                                </div>\n                                            );\n                                        }\n\n                                        if (option.type === 'number') {\n                                            return (\n                                                <div key={optionKey} className=\"p-3 border rounded-lg space-y-2\">\n                                                    <Label className=\"font-medium\">{option.label}</Label>\n                                                    <p className=\"text-sm text-muted-foreground\">{option.description}</p>\n                                                    <Input\n                                                        type=\"number\"\n                                                        value={String(config.options[optionKey] || '')}\n                                                        onChange={(e) => handleOptionChange(optionKey, e.target.value)}\n                                                        placeholder=\"0\"\n                                                    />\n                                                </div>\n                                            );\n                                        }\n\n                                        return null;\n                                    })}\n                                </div>\n                            </div>\n                        ) : (\n                            <div className=\"text-center py-8 text-muted-foreground\">\n                                <Settings className=\"w-12 h-12 mx-auto mb-3 opacity-50\"/>\n                                <div className=\"text-sm\">\n                                    {selectedProvider?.label} doesn&apos;t have configurable options\n                                </div>\n                            </div>\n                        )}\n                    </TabsContent>\n\n                    <TabsContent value=\"preview\" className=\"space-y-6 mt-6\">\n                        <div className=\"grid gap-4\">\n                            {/* Markdown Preview */}\n                            <div className=\"space-y-2\">\n                                <Label>Generated Markdown</Label>\n                                <div className=\"p-4 bg-muted rounded-lg font-mono text-sm break-all\">\n                                    {previewMarkdown}\n                                </div>\n                            </div>\n\n                            {/* Visual Preview */}\n                            <div className=\"space-y-2\">\n                                <Label>Visual Preview</Label>\n                                <div\n                                    className=\"p-4 border rounded-lg bg-gradient-to-br from-muted/30 to-muted/60 min-h-[100px] flex items-center justify-center\">\n                                    {urlValid ? (\n                                        <div className=\"flex items-center gap-3\">\n                                            {selectedProvider &&\n                                                <selectedProvider.icon color={selectedProvider.color} size={32}/>}\n                                            <div className=\"text-left\">\n                                                <div className=\"font-medium text-lg\">{selectedProvider?.label} Embed\n                                                </div>\n                                                <div className=\"text-sm text-muted-foreground truncate max-w-md\">\n                                                    {config.url}\n                                                </div>\n                                                {Object.keys(config.options).length > 0 && (\n                                                    <div className=\"flex gap-1 mt-1\">\n                                                        {Object.entries(config.options).map(([key, value]) => (\n                                                            value &&\n                                                            <Badge key={key} variant=\"secondary\" className=\"text-xs\">\n                                                                {key}: {value.toString()}\n                                                            </Badge>\n                                                        ))}\n                                                    </div>\n                                                )}\n                                            </div>\n                                        </div>\n                                    ) : (\n                                        <div className=\"text-center py-8 text-muted-foreground\">\n                                            <div className=\"text-2xl mb-2\">🎬</div>\n                                            <div className=\"text-sm\">Enter a valid URL to see preview</div>\n                                        </div>\n                                    )}\n                                </div>\n                            </div>\n                        </div>\n                    </TabsContent>\n                </Tabs>\n\n                <DialogFooter>\n                    <Button variant=\"outline\" onClick={handleReset}>\n                        Reset\n                    </Button>\n                    <Button variant=\"outline\" onClick={onClose}>\n                        Cancel\n                    </Button>\n                    <Button onClick={handleInsert} disabled={!urlValid}>\n                        Insert Embed\n                    </Button>\n                </DialogFooter>\n            </DialogContent>\n        </Dialog>\n    );\n};"
  },
  {
    "path": "components/markdown-editor/modals/CUMTableModal.tsx",
    "content": "'use client';\n\nimport React, { useState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport {\n    Select,\n    SelectContent,\n    SelectItem,\n    SelectTrigger,\n    SelectValue,\n} from '@/components/ui/select';\nimport {\n    Dialog,\n    DialogContent,\n    DialogFooter,\n    DialogHeader,\n    DialogTitle,\n} from '@/components/ui/dialog';\nimport { Card, CardContent } from '@/components/ui/card';\nimport {\n    Eye,\n    EyeOff,\n    RotateCcw,\n    Plus,\n    Trash2,\n    AlignLeft,\n    AlignCenter,\n    AlignRight,\n    Grid3x3,\n    CheckCircle,\n} from 'lucide-react';\nimport { renderMarkdown } from '@/lib/services/core/markdown/useCustomExtensions';\nimport { CUMModalProps, CUMTableConfig } from '@/components/markdown-editor/types/cum-extensions';\n\nexport const CUMTableModal: React.FC<CUMModalProps> = ({\n    isOpen,\n    onClose,\n    onInsert,\n}) => {\n    const [config, setConfig] = useState<CUMTableConfig>({\n        rows: 3,\n        columns: 3,\n        hasHeader: true,\n        alignments: ['left', 'left', 'left'],\n        data: [\n            ['Header 1', 'Header 2', 'Header 3'],\n            ['Cell 1', 'Cell 2', 'Cell 3'],\n            ['Cell 4', 'Cell 5', 'Cell 6'],\n        ],\n    });\n\n    const [showPreview, setShowPreview] = useState(true);\n\n    // Update table dimensions\n    const updateDimensions = (newRows: number, newCols: number) => {\n        const oldData = config.data;\n        const newData: string[][] = [];\n\n        // Keep existing data where it fits\n        for (let i = 0; i < newRows; i++) {\n            newData[i] = [];\n            for (let j = 0; j < newCols; j++) {\n                newData[i][j] = oldData[i]?.[j] || `Cell ${i * newCols + j + 1}`;\n            }\n        }\n\n        // Update alignments to match new column count\n        const newAlignments = Array(newCols)\n            .fill(null)\n            .map((_, i) => config.alignments[i] || 'left');\n\n        setConfig({\n            ...config,\n            rows: newRows,\n            columns: newCols,\n            data: newData,\n            alignments: newAlignments,\n        });\n    };\n\n    // Update cell content\n    const updateCell = (row: number, col: number, value: string) => {\n        const newData = config.data.map((r, i) =>\n            i === row ? r.map((c, j) => (j === col ? value : c)) : r\n        );\n        setConfig({ ...config, data: newData });\n    };\n\n    // Update column alignment\n    const updateAlignment = (col: number, align: 'left' | 'center' | 'right') => {\n        const newAlignments = [...config.alignments];\n        newAlignments[col] = align;\n        setConfig({ ...config, alignments: newAlignments });\n    };\n\n    // Generate markdown table\n    const generateMarkdown = (): string => {\n        let markdown = '';\n\n        // Header row\n        markdown += '| ' + config.data[0].join(' | ') + ' |\\n';\n\n        // Separator row\n        const separators = config.alignments.map(align => {\n            if (align === 'center') return ':-----:';\n            if (align === 'right') return '-----:';\n            return ':-----';\n        });\n        markdown += '| ' + separators.join(' | ') + ' |\\n';\n\n        // Data rows\n        for (let i = 1; i < config.data.length; i++) {\n            markdown += '| ' + config.data[i].join(' | ') + ' |\\n';\n        }\n\n        return markdown;\n    };\n\n    const handleInsert = () => {\n        onInsert(generateMarkdown());\n        onClose();\n    };\n\n    const handleReset = () => {\n        setConfig({\n            rows: 3,\n            columns: 3,\n            hasHeader: true,\n            alignments: ['left', 'left', 'left'],\n            data: [\n                ['Header 1', 'Header 2', 'Header 3'],\n                ['Cell 1', 'Cell 2', 'Cell 3'],\n                ['Cell 4', 'Cell 5', 'Cell 6'],\n            ],\n        });\n    };\n\n    const getAlignmentIcon = (align: string) => {\n        if (align === 'center') return <AlignCenter className=\"h-3 w-3\" />;\n        if (align === 'right') return <AlignRight className=\"h-3 w-3\" />;\n        return <AlignLeft className=\"h-3 w-3\" />;\n    };\n\n    return (\n        <Dialog open={isOpen} onOpenChange={onClose}>\n            <DialogContent className=\"sm:max-w-[900px] max-h-[90vh] flex flex-col\">\n                <DialogHeader className=\"space-y-3 flex-shrink-0\">\n                    <DialogTitle className=\"flex items-center gap-3\">\n                        <div className=\"p-2 rounded-lg bg-emerald-100 dark:bg-emerald-900/30\">\n                            <Grid3x3 className=\"h-5 w-5 text-emerald-600 dark:text-emerald-400\" />\n                        </div>\n                        <div>\n                            <div className=\"text-xl font-semibold\">Create Table</div>\n                            <div className=\"text-sm text-muted-foreground font-normal\">\n                                Build a structured table with custom alignment and content\n                            </div>\n                        </div>\n                    </DialogTitle>\n                </DialogHeader>\n\n                <div className=\"space-y-4 overflow-y-auto flex-1 pr-4\">\n                    {/* Dimensions Control */}\n                    <div className=\"bg-muted/50 rounded-lg p-3 space-y-2\">\n                        <div className=\"grid grid-cols-2 gap-3\">\n                            <div>\n                                <Label htmlFor=\"rows\" className=\"text-xs font-medium\">\n                                    Rows\n                                </Label>\n                                <div className=\"flex items-center gap-2 mt-1\">\n                                    <Input\n                                        id=\"rows\"\n                                        type=\"number\"\n                                        min={2}\n                                        max={20}\n                                        value={config.rows}\n                                        onChange={(e) => updateDimensions(parseInt(e.target.value) || config.rows, config.columns)}\n                                        className=\"h-8 text-sm\"\n                                    />\n                                    <span className=\"text-xs text-muted-foreground whitespace-nowrap\">of 20</span>\n                                </div>\n                            </div>\n                            <div>\n                                <Label htmlFor=\"columns\" className=\"text-xs font-medium\">\n                                    Columns\n                                </Label>\n                                <div className=\"flex items-center gap-2 mt-1\">\n                                    <Input\n                                        id=\"columns\"\n                                        type=\"number\"\n                                        min={1}\n                                        max={10}\n                                        value={config.columns}\n                                        onChange={(e) => updateDimensions(config.rows, parseInt(e.target.value) || config.columns)}\n                                        className=\"h-8 text-sm\"\n                                    />\n                                    <span className=\"text-xs text-muted-foreground whitespace-nowrap\">of 10</span>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n\n                    {/* Table Editor */}\n                    <div className=\"space-y-3\">\n                        <Label className=\"text-sm font-medium\">Table Content</Label>\n                        <div className=\"border rounded-lg overflow-x-auto max-h-[300px] overflow-y-auto bg-muted/20\">\n                            <table className=\"w-full border-collapse text-sm\">\n                                <tbody>\n                                    {config.data.map((row, rowIdx) => (\n                                        <tr key={rowIdx} className={rowIdx === 0 ? 'bg-muted' : ''}>\n                                            {row.map((cell, colIdx) => (\n                                                <td key={`${rowIdx}-${colIdx}`} className=\"border p-1\">\n                                                    <Input\n                                                        value={cell}\n                                                        onChange={(e) => updateCell(rowIdx, colIdx, e.target.value)}\n                                                        placeholder={`Row ${rowIdx + 1}, Col ${colIdx + 1}`}\n                                                        className=\"h-8 text-xs\"\n                                                    />\n                                                </td>\n                                            ))}\n                                        </tr>\n                                    ))}\n                                </tbody>\n                            </table>\n                        </div>\n                    </div>\n\n                    {/* Column Alignment Control */}\n                    <div className=\"space-y-3\">\n                        <Label className=\"text-sm font-medium\">Column Alignment</Label>\n                        <div className=\"grid grid-cols-auto gap-2\">\n                            {config.alignments.map((align, idx) => (\n                                <div key={idx} className=\"flex items-center gap-1 p-2 bg-muted rounded-md\">\n                                    <span className=\"text-xs text-muted-foreground flex-1\">Col {idx + 1}</span>\n                                    <div className=\"flex gap-1\">\n                                        <Button\n                                            variant={align === 'left' ? 'default' : 'ghost'}\n                                            size=\"sm\"\n                                            onClick={() => updateAlignment(idx, 'left')}\n                                            className=\"h-7 w-7 p-0\"\n                                        >\n                                            <AlignLeft className=\"h-3 w-3\" />\n                                        </Button>\n                                        <Button\n                                            variant={align === 'center' ? 'default' : 'ghost'}\n                                            size=\"sm\"\n                                            onClick={() => updateAlignment(idx, 'center')}\n                                            className=\"h-7 w-7 p-0\"\n                                        >\n                                            <AlignCenter className=\"h-3 w-3\" />\n                                        </Button>\n                                        <Button\n                                            variant={align === 'right' ? 'default' : 'ghost'}\n                                            size=\"sm\"\n                                            onClick={() => updateAlignment(idx, 'right')}\n                                            className=\"h-7 w-7 p-0\"\n                                        >\n                                            <AlignRight className=\"h-3 w-3\" />\n                                        </Button>\n                                    </div>\n                                </div>\n                            ))}\n                        </div>\n                    </div>\n\n                    {/* Preview */}\n                    <div className=\"space-y-3\">\n                        <div className=\"flex items-center justify-between\">\n                            <div className=\"flex items-center gap-2\">\n                                <Eye className=\"h-4 w-4 text-green-500\" />\n                                <Label className=\"text-sm font-medium\">Live Preview</Label>\n                            </div>\n                            <Button\n                                variant=\"ghost\"\n                                size=\"sm\"\n                                onClick={() => setShowPreview(!showPreview)}\n                            >\n                                {showPreview ? <EyeOff className=\"h-3 w-3\" /> : <Eye className=\"h-3 w-3\" />}\n                            </Button>\n                        </div>\n\n                        {showPreview && (\n                            <Card className=\"border-2 border-dashed\">\n                                <CardContent className=\"p-4\">\n                                    <div className=\"overflow-x-auto\">\n                                        <div\n                                            className=\"inline-block\"\n                                            dangerouslySetInnerHTML={{\n                                                __html: renderMarkdown(generateMarkdown()),\n                                            }}\n                                        />\n                                    </div>\n                                </CardContent>\n                            </Card>\n                        )}\n                    </div>\n\n                    {/* Markdown Preview */}\n                    <div className=\"space-y-2\">\n                        <Label className=\"text-sm font-medium\">Generated Markdown</Label>\n                        <div className=\"p-3 bg-muted rounded-md font-mono text-xs break-all max-h-[150px] overflow-y-auto whitespace-pre-wrap\">\n                            {generateMarkdown()}\n                        </div>\n                    </div>\n\n                    {/* Info */}\n                    <div className=\"flex items-start gap-2 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-md\">\n                        <CheckCircle className=\"h-4 w-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0\" />\n                        <div className=\"text-xs text-blue-700 dark:text-blue-300\">\n                            <p className=\"font-semibold\">Column alignment:</p>\n                            <p>Left-aligned (default) • Center-aligned • Right-aligned</p>\n                        </div>\n                    </div>\n                </div>\n\n                <DialogFooter className=\"flex justify-between\">\n                    <Button\n                        variant=\"ghost\"\n                        onClick={handleReset}\n                        className=\"flex items-center gap-2\"\n                    >\n                        <RotateCcw className=\"h-3 w-3\" />\n                        Reset\n                    </Button>\n\n                    <div className=\"flex gap-2\">\n                        <Button variant=\"outline\" onClick={onClose}>\n                            Cancel\n                        </Button>\n                        <Button\n                            onClick={handleInsert}\n                            className=\"flex items-center gap-2\"\n                        >\n                            <Plus className=\"h-3 w-3\" />\n                            Insert Table\n                        </Button>\n                    </div>\n                </DialogFooter>\n            </DialogContent>\n        </Dialog>\n    );\n};\n"
  },
  {
    "path": "components/markdown-editor/modals/index.ts",
    "content": "// components/markdown-editor/modals/index.ts\n\nexport { CUMButtonModal } from './CUMButtonModal';\nexport { CUMAlertModal } from './CUMAlertModal';\nexport { CUMEmbedModal } from './CUMEmbedModal';\nexport { CUMTableModal } from './CUMTableModal';"
  },
  {
    "path": "components/markdown-editor/types/cum-extensions.ts",
    "content": "export interface CUMButtonConfig {\n    text: string;\n    url: string;\n    style: 'default' | 'primary' | 'secondary' | 'success' | 'danger' | 'outline';\n    size?: 'sm' | 'md' | 'lg';\n    icon?: string;\n    disabled?: boolean;\n    target?: '_blank' | '_self';\n}\n\nexport interface CUMAlertConfig {\n    type: 'info' | 'warning' | 'error' | 'success' | 'tip';\n    title?: string;\n    content: string;\n    dismissible?: boolean;\n    icon?: string;\n}\n\nexport interface CUMEmbedConfig {\n    provider: 'youtube' | 'codepen' | 'figma' | 'twitter' | 'github' | 'generic';\n    url: string;\n    options?: {\n        width?: string;\n        height?: string;\n        autoplay?: boolean;\n        theme?: 'light' | 'dark';\n        showControls?: boolean;\n    };\n}\n\nexport interface CUMTableConfig {\n    rows: number;\n    columns: number;\n    hasHeader: boolean;\n    alignments: ('left' | 'center' | 'right')[];\n    data: string[][];\n}\n\nexport interface CUMModalProps {\n    isOpen: boolean;\n    onClose: () => void;\n    onInsert: (markdown: string) => void;\n}\n\nexport type CUMExtensionType = 'button' | 'alert' | 'embed' | 'table';"
  },
  {
    "path": "components/markdown-editor/utils/formatting.ts",
    "content": "/**\n * Utilities for formatting markdown text\n */\n\n/**\n * Formats for inserting around selected text\n */\nexport interface TextFormatOptions {\n    prefix: string;\n    suffix?: string;\n    blockLevel?: boolean;\n    replaceSelection?: boolean;\n    multiline?: boolean;\n    linePrefix?: string;\n}\n\n/**\n * Common formatting options\n */\nexport const FORMAT_OPTIONS = {\n    bold: { prefix: '**', suffix: '**' },\n    italic: { prefix: '_', suffix: '_' },\n    strikethrough: { prefix: '~~', suffix: '~~' },\n    code: { prefix: '`', suffix: '`' },\n    codeBlock: { prefix: '```\\n', suffix: '\\n```', blockLevel: true },\n    link: { prefix: '[', suffix: '](url)' },\n    image: { prefix: '![', suffix: '](url)' },\n    quote: { prefix: '> ', blockLevel: true, multiline: true, linePrefix: '> ' },\n    h1: { prefix: '# ', blockLevel: true },\n    h2: { prefix: '## ', blockLevel: true },\n    h3: { prefix: '### ', blockLevel: true },\n    h4: { prefix: '#### ', blockLevel: true },\n    h5: { prefix: '##### ', blockLevel: true },\n    h6: { prefix: '###### ', blockLevel: true },\n    unorderedList: { prefix: '- ', blockLevel: true, multiline: true, linePrefix: '- ' },\n    orderedList: { prefix: '1. ', blockLevel: true, multiline: true, linePrefix: (i: number) => `${i + 1}. ` },\n    taskList: { prefix: '- [ ] ', blockLevel: true, multiline: true, linePrefix: '- [ ] ' },\n    table: {\n        replaceSelection: true,\n        prefix: '| Header 1 | Header 2 |\\n|----------|----------|\\n| Cell 1   | Cell 2   |',\n        blockLevel: true\n    },\n    horizontalRule: { prefix: '\\n---\\n', blockLevel: true, replaceSelection: true },\n};\n\n/**\n * Apply formatting to text with special handling for multiline and block-level elements\n */\nexport function formatText(\n    text: string,\n    selection: { start: number; end: number; text: string },\n    options: TextFormatOptions\n): { newText: string; newSelection: { start: number; end: number } } {\n    const { prefix, suffix = '', blockLevel = false, replaceSelection = false, multiline = false, linePrefix } = options;\n    const { start, end, text: selectedText } = selection;\n\n    // If there's no selection and it's a block level element, add newlines\n    if (selectedText === '' && blockLevel) {\n        const newLine = text.charAt(start - 1) !== '\\n' && start > 0 ? '\\n' : '';\n        const endLine = text.charAt(end) !== '\\n' ? '\\n' : '';\n        const insertText = replaceSelection ? prefix : `${newLine}${prefix}${suffix}${endLine}`;\n\n        const newText = text.substring(0, start) + insertText + text.substring(end);\n        const cursorPos = start + newLine.length + prefix.length;\n\n        return {\n            newText,\n            newSelection: { start: cursorPos, end: cursorPos },\n        };\n    }\n\n    // If selection should be replaced entirely\n    if (replaceSelection) {\n        const newText = text.substring(0, start) + prefix + text.substring(end);\n        const cursorPos = start + prefix.length;\n\n        return {\n            newText,\n            newSelection: { start: cursorPos, end: cursorPos },\n        };\n    }\n\n    // Handle multiline formatting (lists, quotes, etc.)\n    if (multiline && selectedText.includes('\\n')) {\n        const lines = selectedText.split('\\n');\n\n        // Format each line\n        const formattedLines = lines.map((line, i) => {\n            if (!line.trim()) return line; // Skip empty lines\n\n            // If linePrefix is a function, call it with the index\n            const prefixValue = typeof linePrefix === 'function'\n                ? (linePrefix as (i: number) => string)(i)\n                : linePrefix || prefix;\n\n            return `${prefixValue}${line}`;\n        });\n\n        const formattedText = formattedLines.join('\\n');\n        const newText = text.substring(0, start) + formattedText + text.substring(end);\n\n        return {\n            newText,\n            newSelection: { start, end: start + formattedText.length },\n        };\n    }\n\n    // Standard formatting (bold, italic, etc.)\n    const formattedText = prefix + selectedText + suffix;\n    const newText = text.substring(0, start) + formattedText + text.substring(end);\n\n    // Set cursor position based on whether there was a selection\n    const newStart = selectedText ? start : start + prefix.length;\n    const newEnd = selectedText ? start + formattedText.length : start + prefix.length;\n\n    return {\n        newText,\n        newSelection: { start: newStart, end: newEnd },\n    };\n}\n\n/**\n * Is a line empty or contains only whitespace?\n */\nexport function isEmptyLine(line: string): boolean {\n    return line.trim() === '';\n}\n\n/**\n * Does the line start with the given prefix?\n */\nexport function lineStartsWith(line: string, prefix: string): boolean {\n    return line.trimStart().startsWith(prefix);\n}\n\n/**\n * Get the indentation level of a line (number of spaces at the beginning)\n */\nexport function getIndentationLevel(line: string): number {\n    const match = line.match(/^(\\s*)/);\n    return match ? match[1].length : 0;\n}\n\n/**\n * Add indentation to a line\n */\nexport function addIndentation(line: string, spaces: number): string {\n    return ' '.repeat(spaces) + line;\n}\n\n/**\n * Remove indentation from a line\n */\nexport function removeIndentation(line: string, spaces: number): string {\n    const indentation = getIndentationLevel(line);\n    const removeSpaces = Math.min(indentation, spaces);\n    return line.substring(removeSpaces);\n}\n\n/**\n * Toggle a formatting feature on selected text\n */\nexport function toggleFormatting(\n    text: string,\n    selection: { start: number; end: number; text: string },\n    options: TextFormatOptions\n): { newText: string; newSelection: { start: number; end: number } } {\n    const { prefix, suffix = '' } = options;\n    const { start, end, text: selectedText } = selection;\n\n    // If there's no selection, just insert the formatting\n    if (selectedText === '') {\n        return formatText(text, selection, options);\n    }\n\n    // Check if the selection already has the formatting\n    if (selectedText.startsWith(prefix) && selectedText.endsWith(suffix)) {\n        // Remove the formatting\n        const unformattedText = selectedText.substring(prefix.length, selectedText.length - suffix.length);\n        const newText = text.substring(0, start) + unformattedText + text.substring(end);\n\n        return {\n            newText,\n            newSelection: { start, end: start + unformattedText.length },\n        };\n    }\n\n    // Add the formatting\n    return formatText(text, selection, options);\n}\n\n/**\n * Calculate word and character count\n */\nexport function getTextMetrics(text: string): { words: number; chars: number; lines: number } {\n    const lines = text.split('\\n').length;\n    const chars = text.length;\n    const words = text.trim() ? text.trim().split(/\\s+/).length : 0;\n\n    return { words, chars, lines };\n}\n\n/**\n * Determine the line and column number for a cursor position\n */\nexport function positionToLineColumn(text: string, position: number): { line: number; column: number } {\n    const lines = text.substring(0, position).split('\\n');\n    const line = lines.length;\n    const column = lines[lines.length - 1].length + 1;\n\n    return { line, column };\n}\n\n/**\n * Extract the current paragraph around the cursor\n */\nexport function getCurrentParagraph(text: string, position: number): { text: string; start: number; end: number } {\n    const lines = text.split('\\n');\n    let currentIndex = 0;\n    let paragraphStart = 0;\n    let paragraphEnd = 0;\n\n    for (let i = 0; i < lines.length; i++) {\n        const lineLength = lines[i].length + 1; // +1 for the newline\n\n        if (position >= currentIndex && position < currentIndex + lineLength) {\n            // We found the line containing the cursor\n            if (isEmptyLine(lines[i])) {\n                // If the cursor is on an empty line, return just that line\n                paragraphStart = currentIndex;\n                paragraphEnd = currentIndex + lineLength - 1;\n                break;\n            }\n\n            // Find the start of the paragraph\n            let start = i;\n            while (start > 0 && !isEmptyLine(lines[start - 1])) {\n                start--;\n            }\n\n            // Find the end of the paragraph\n            let end = i;\n            while (end < lines.length - 1 && !isEmptyLine(lines[end + 1])) {\n                end++;\n            }\n\n            // Calculate start and end positions\n            paragraphStart = lines.slice(0, start).reduce((sum, line) => sum + line.length + 1, 0);\n            paragraphEnd = paragraphStart + lines.slice(start, end + 1).join('\\n').length;\n            break;\n        }\n\n        currentIndex += lineLength;\n    }\n\n    return {\n        text: text.substring(paragraphStart, paragraphEnd),\n        start: paragraphStart,\n        end: paragraphEnd,\n    };\n}"
  },
  {
    "path": "components/markdown-editor/utils/keyboard-shortcuts.ts",
    "content": "/**\n * Keyboard shortcut utilities for the markdown editor\n */\n\n/**\n * Keyboard shortcut definition\n */\nexport interface KeyboardShortcut {\n    key: string;\n    ctrlKey?: boolean;\n    shiftKey?: boolean;\n    altKey?: boolean;\n    metaKey?: boolean;\n    preventDefault?: boolean;\n    action: () => void;\n    description: string;\n}\n\n/**\n * Keyboard shortcut handler configuration\n */\nexport interface KeyboardShortcutsConfig {\n    shortcuts: KeyboardShortcut[];\n    // Additional options can be added here\n    enableInInputs?: boolean;\n}\n\n/**\n * Default keyboard shortcuts for markdown editor\n */\nexport const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcut[] = [\n    {\n        key: 'b',\n        ctrlKey: true,\n        action: () => {}, // Will be connected in the component\n        description: 'Bold',\n        preventDefault: true,\n    },\n    {\n        key: 'i',\n        ctrlKey: true,\n        action: () => {}, // Will be connected in the component\n        description: 'Italic',\n        preventDefault: true,\n    },\n    {\n        key: 'k',\n        ctrlKey: true,\n        action: () => {}, // Will be connected in the component\n        description: 'Link',\n        preventDefault: true,\n    },\n    {\n        key: '1',\n        ctrlKey: true,\n        action: () => {}, // Will be connected in the component\n        description: 'Heading 1',\n        preventDefault: true,\n    },\n    {\n        key: '2',\n        ctrlKey: true,\n        action: () => {}, // Will be connected in the component\n        description: 'Heading 2',\n        preventDefault: true,\n    },\n    {\n        key: '3',\n        ctrlKey: true,\n        action: () => {}, // Will be connected in the component\n        description: 'Heading 3',\n        preventDefault: true,\n    },\n    {\n        key: 'z',\n        ctrlKey: true,\n        action: () => {}, // Will be connected in the component\n        description: 'Undo',\n        preventDefault: true,\n    },\n    {\n        key: 'z',\n        ctrlKey: true,\n        shiftKey: true,\n        action: () => {}, // Will be connected in the component\n        description: 'Redo',\n        preventDefault: true,\n    },\n    {\n        key: 's',\n        ctrlKey: true,\n        action: () => {}, // Will be connected in the component\n        description: 'Save',\n        preventDefault: true,\n    },\n    {\n        key: 'a',\n        altKey: true,\n        action: () => {}, // Will be connected in the component\n        description: 'Open AI Assistant',\n        preventDefault: true,\n    },\n];\n\n// Store active handlers to prevent duplicates and allow cleanup\nconst activeHandlers = new Map<HTMLElement, (e: KeyboardEvent) => void>();\n\n/**\n * Check if keyboard event matches a shortcut\n * @param event The keyboard event\n * @param shortcut The shortcut to match against\n */\nexport function matchesShortcut(event: KeyboardEvent, shortcut: KeyboardShortcut): boolean {\n    // For non-modifier keys, we need to match key and modifiers exactly\n    if (shortcut.key.toLowerCase() !== event.key.toLowerCase()) return false;\n\n    // Ctrl/Cmd check (either event or shortcut must have it, and they must match)\n    if ((shortcut.ctrlKey || shortcut.metaKey) !== (event.ctrlKey || event.metaKey)) return false;\n\n    // Shift check\n    if ((shortcut.shiftKey === true) !== event.shiftKey) return false;\n\n    // Alt check\n    if ((shortcut.altKey === true) !== event.altKey) return false;\n\n    return true;\n}\n\n/**\n * Format shortcut for display\n */\nexport function formatShortcut(shortcut: KeyboardShortcut): string {\n    const parts: string[] = [];\n\n    if (shortcut.ctrlKey || shortcut.metaKey) {\n        // Use platform-specific symbol\n        const isMac = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform);\n        parts.push(isMac ? '⌘' : 'Ctrl');\n    }\n\n    if (shortcut.shiftKey) {\n        parts.push('⇧');\n    }\n\n    if (shortcut.altKey) {\n        parts.push('Alt');\n    }\n\n    // Format the key (capitalize single letters)\n    const key = shortcut.key.length === 1\n        ? shortcut.key.toUpperCase()\n        : shortcut.key;\n\n    parts.push(key);\n\n    return parts.join('+');\n}\n\n/**\n * Create a keyboard event handler\n */\nexport function createShortcutHandler(config: KeyboardShortcutsConfig) {\n    return (event: KeyboardEvent) => {\n        // Critical fix: Skip if target is an input/textarea and NO modifiers\n        // This allows normal typing to work in inputs/textareas\n        if (!config.enableInInputs) {\n            const tagName = (event.target as HTMLElement)?.tagName;\n            if (tagName === 'INPUT' || tagName === 'TEXTAREA') {\n                // Only process shortcuts with modifiers\n                if (!event.ctrlKey && !event.metaKey && !event.altKey) {\n                    return;\n                }\n            }\n        }\n\n        // Check each shortcut\n        for (const shortcut of config.shortcuts) {\n            if (matchesShortcut(event, shortcut)) {\n                // Only prevent default if specified\n                if (shortcut.preventDefault) {\n                    event.preventDefault();\n                }\n\n                try {\n                    // Execute the action\n                    shortcut.action();\n                } catch (error) {\n                    console.error(\"Error executing keyboard shortcut:\", error);\n                }\n\n                // Stop after first match\n                return;\n            }\n        }\n    };\n}\n\n/**\n * Connect keyboard shortcuts to a specific element\n */\nexport function bindShortcutsToElement(\n    element: HTMLElement,\n    shortcuts: KeyboardShortcut[],\n    options: Partial<KeyboardShortcutsConfig> = {}\n): () => void {\n    // Remove any existing handler for this element\n    if (activeHandlers.has(element)) {\n        const oldHandler = activeHandlers.get(element);\n        if (oldHandler) {\n            element.removeEventListener('keydown', oldHandler as EventListener);\n        }\n    }\n\n    // Set up config with defaults\n    const config: KeyboardShortcutsConfig = {\n        shortcuts,\n        enableInInputs: options.enableInInputs ?? false,\n    };\n\n    const handler = createShortcutHandler(config);\n\n    // Store and add the handler\n    activeHandlers.set(element, handler);\n    element.addEventListener('keydown', handler as EventListener);\n\n    // Return a cleanup function\n    return () => {\n        element.removeEventListener('keydown', handler as EventListener);\n        activeHandlers.delete(element);\n    };\n}\n\n/**\n * Hook-friendly function to bind shortcuts to document\n */\nexport function bindGlobalShortcuts(\n    shortcuts: KeyboardShortcut[],\n    options: Partial<KeyboardShortcutsConfig> = {}\n): () => void {\n    // Skip if not in browser environment\n    if (typeof document === 'undefined') {\n        return () => {};\n    }\n\n    return bindShortcutsToElement(document.documentElement, shortcuts, options);\n}\n\n/**\n * Get shortcut map for displaying in help dialogs\n */\nexport function getShortcutMap(shortcuts: KeyboardShortcut[]): Record<string, string> {\n    return shortcuts.reduce((acc, shortcut) => {\n        acc[shortcut.description] = formatShortcut(shortcut);\n        return acc;\n    }, {} as Record<string, string>);\n}"
  },
  {
    "path": "components/markdown-editor/utils/renderer.ts",
    "content": "/**\n * Utilities for rendering markdown content to HTML\n */\n\n/**\n * Options for rendering markdown\n */\nexport interface MarkdownRenderOptions {\n    /**\n     * Enable/disable specific features\n     */\n    features?: {\n        headings?: boolean;\n        anchors?: boolean;\n        bold?: boolean;\n        italic?: boolean;\n        strikethrough?: boolean;\n        blockquotes?: boolean;\n        code?: boolean;\n        inlineCode?: boolean;\n        links?: boolean;\n        images?: boolean;\n        lists?: boolean;\n        taskLists?: boolean;\n        tables?: boolean;\n        footnotes?: boolean;\n        lineBreaks?: boolean;\n        horizontalRules?: boolean;\n    };\n\n    /**\n     * CSS class to add to the container\n     */\n    className?: string;\n\n    /**\n     * Custom URL for links\n     */\n    baseUrl?: string;\n\n    /**\n     * Should links open in a new tab?\n     */\n    openLinksInNewTab?: boolean;\n\n    /**\n     * Allow HTML in markdown\n     */\n    allowHtml?: boolean;\n\n    /**\n     * Custom renderer functions\n     */\n    customRenderers?: Record<string, (content: string) => string>;\n}\n\n/**\n * Default render options\n */\nexport const DEFAULT_RENDER_OPTIONS: MarkdownRenderOptions = {\n    features: {\n        headings: true,\n        anchors: true,\n        bold: true,\n        italic: true,\n        strikethrough: true,\n        blockquotes: true,\n        code: true,\n        inlineCode: true,\n        links: true,\n        images: true,\n        lists: true,\n        taskLists: true,\n        tables: true,\n        footnotes: true,\n        lineBreaks: true,\n        horizontalRules: true,\n    },\n    openLinksInNewTab: true,\n    allowHtml: false,\n};\n\n/**\n * Escape special HTML characters\n */\nexport function escapeHtml(unsafe: string): string {\n    return unsafe\n        .replace(/&/g, \"&amp;\")\n        .replace(/</g, \"&lt;\")\n        .replace(/>/g, \"&gt;\")\n        .replace(/\"/g, \"&quot;\")\n        .replace(/'/g, \"&#039;\");\n}\n\n/**\n * Process code blocks\n */\nfunction processCodeBlocks(input: string, features?: MarkdownRenderOptions['features']): string {\n    if (!features?.code) return input;\n\n    return input.replace(/```([a-z]*)\\n([\\s\\S]*?)\\n```/g, (_, lang, code) => {\n        const escapedCode = escapeHtml(code.trim());\n        return `<pre class=\"bg-muted p-4 rounded-md overflow-x-auto my-4\"><code class=\"language-${escapeHtml(lang)}\">${escapedCode}</code></pre>`;\n    });\n}\n\n/**\n * Process inline code\n */\nfunction processInlineCode(input: string, features?: MarkdownRenderOptions['features']): string {\n    if (!features?.inlineCode) return input;\n\n    return input.replace(/`([^`]+)`/g, (_, code) =>\n        `<code class=\"bg-muted px-1.5 py-0.5 rounded text-sm font-mono\">${escapeHtml(code)}</code>`\n    );\n}\n\n/**\n * Process lists, including task lists\n */\nfunction processLists(input: string, features?: MarkdownRenderOptions['features']): string {\n    if (!features?.lists) return input;\n\n    let processedContent = input;\n\n    // Process task lists if enabled\n    if (features.taskLists) {\n        processedContent = processedContent.replace(/^(\\s*)-\\s\\[([ xX])\\]\\s(.+)$/gm, (match, spaces, checked, content) => {\n            const isChecked = checked.toLowerCase() === 'x';\n            const indentation = spaces.length;\n            const escapedContent = escapeHtml(content);\n\n            // Use a special marker for task list items\n            return `__TASK_ITEM_${indentation}_${isChecked ? 'CHECKED' : 'UNCHECKED'}_${escapedContent}__`;\n        });\n    }\n\n    // Process ordered lists (1. Item)\n    processedContent = processedContent.replace(\n        /^([ \\t]*)((?:[0-9]+\\.)[ \\t]+)(.+)(?:\\n|$)/gm,\n        (match, indent, marker, content) => {\n            // Return the match untouched if it's inside a code block or already processed\n            if (match.includes('<pre') || match.includes('<li>')) return match;\n            return `${indent}<ol-item depth=\"${indent.length}\" marker=\"${marker.trim()}\">${escapeHtml(content)}</ol-item>\\n`;\n        }\n    );\n\n    // Process unordered lists (- Item or * Item)\n    processedContent = processedContent.replace(\n        /^([ \\t]*)([-*])[ \\t]+([^\\n]+)(?:\\n|$)/gm,\n        (match, indent, marker, content) => {\n            // Skip if it's a task list marker, inside code block, or already processed\n            if (match.includes('__TASK_ITEM_') || match.includes('<pre') || match.includes('<li>')) return match;\n            return `${indent}<ul-item depth=\"${indent.length}\" marker=\"${marker}\">${escapeHtml(content)}</ul-item>\\n`;\n        }\n    );\n\n    // Now convert the task list markers back to HTML\n    if (features.taskLists) {\n        processedContent = processedContent.replace(/__TASK_ITEM_(\\d+)_(CHECKED|UNCHECKED)_(.+?)__/g,\n            (_, indentation, checkedState, content) => {\n                const isChecked = checkedState === 'CHECKED';\n                const indent = parseInt(indentation);\n\n                return `<task-item depth=\"${indent}\" checked=\"${isChecked}\">${content}</task-item>\\n`;\n            }\n        );\n    }\n\n    // Convert the markers to proper HTML\n    const processLists = (content: string): string => {\n        // Process ul-item elements\n        let currentUlDepth: number | null = null;\n        let inUl = false;\n\n        content = content.split('\\n').map(line => {\n            const ulMatch = line.match(/<ul-item depth=\"(\\d+)\" marker=\"([-*])\">(.+?)<\\/ul-item>/);\n\n            if (ulMatch) {\n                // eslint-disable-next-line @typescript-eslint/no-unused-vars\n                const [, depthStr, marker, itemContent] = ulMatch;\n                const depth = parseInt(depthStr);\n\n                if (!inUl || currentUlDepth !== depth) {\n                    let result = '';\n\n                    // Close previous list if needed\n                    if (inUl) {\n                        result += '</ul>'.repeat(1);\n                    }\n\n                    // Open new list\n                    result += `<ul class=\"list-disc list-inside space-y-2 my-4 ml-${depth > 0 ? depth * 4 : '0'}\">\\n`;\n                    inUl = true;\n                    currentUlDepth = depth;\n\n                    return result + `<li>${itemContent}</li>`;\n                }\n\n                return `<li>${itemContent}</li>`;\n            } else if (inUl && !line.includes('<ul-item')) {\n                inUl = false;\n                currentUlDepth = null;\n                return '</ul>\\n' + line;\n            }\n\n            return line;\n        }).join('\\n');\n\n        // Close any open lists\n        if (inUl) {\n            content += '\\n</ul>';\n        }\n\n        // Process ol-item elements\n        let currentOlDepth: number | null = null;\n        let inOl = false;\n\n        content = content.split('\\n').map(line => {\n            const olMatch = line.match(/<ol-item depth=\"(\\d+)\" marker=\"([0-9]+\\.)\">(.+?)<\\/ol-item>/);\n\n            if (olMatch) {\n                // eslint-disable-next-line @typescript-eslint/no-unused-vars\n                const [, depthStr, marker, itemContent] = olMatch;\n                const depth = parseInt(depthStr);\n\n                if (!inOl || currentOlDepth !== depth) {\n                    let result = '';\n\n                    // Close previous list if needed\n                    if (inOl) {\n                        result += '</ol>'.repeat(1);\n                    }\n\n                    // Open new list\n                    result += `<ol class=\"list-decimal list-inside space-y-2 my-4 ml-${depth > 0 ? depth * 4 : '0'}\">\\n`;\n                    inOl = true;\n                    currentOlDepth = depth;\n\n                    return result + `<li>${itemContent}</li>`;\n                }\n\n                return `<li>${itemContent}</li>`;\n            } else if (inOl && !line.includes('<ol-item')) {\n                inOl = false;\n                currentOlDepth = null;\n                return '</ol>\\n' + line;\n            }\n\n            return line;\n        }).join('\\n');\n\n        // Close any open lists\n        if (inOl) {\n            content += '\\n</ol>';\n        }\n\n        // Process task items\n        let currentTaskListDepth: number | null = null;\n        let inTaskList = false;\n\n        content = content.split('\\n').map(line => {\n            const taskMatch = line.match(/<task-item depth=\"(\\d+)\" checked=\"(true|false)\">(.+?)<\\/task-item>/);\n\n            if (taskMatch) {\n                const [, depthStr, checkedStr, itemContent] = taskMatch;\n                const depth = parseInt(depthStr);\n                const isChecked = checkedStr === 'true';\n\n                if (!inTaskList || currentTaskListDepth !== depth) {\n                    let result = '';\n\n                    // Close previous list if needed\n                    if (inTaskList) {\n                        result += '</div>'.repeat(1);\n                    }\n\n                    // Open new task list\n                    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n                    result += `<div class=\"flex flex-col gap-2 my-4 ml-${depth > 0 ? depth * 4 : '0'}\">\\n`;\n                    inTaskList = true;\n                    currentTaskListDepth = depth;\n                }\n\n                const checkboxHtml = `\n          <div class=\"flex items-center gap-2 my-1\">\n            <input type=\"checkbox\" ${isChecked ? 'checked' : ''} disabled \n              class=\"form-checkbox h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary\" />\n            <span${isChecked ? ' class=\"line-through text-muted-foreground\"' : ''}>${itemContent}</span>\n          </div>\n        `;\n\n                return checkboxHtml;\n            } else if (inTaskList && !line.includes('<task-item')) {\n                inTaskList = false;\n                currentTaskListDepth = null;\n                return '</div>\\n' + line;\n            }\n\n            return line;\n        }).join('\\n');\n\n        // Close any open task lists\n        if (inTaskList) {\n            content += '\\n</div>';\n        }\n\n        return content;\n    };\n\n    return processLists(processedContent);\n}\n\n/**\n * Process line breaks\n */\nfunction processLineBreaks(input: string, features?: MarkdownRenderOptions['features']): string {\n    if (!features?.lineBreaks) return input;\n\n    // Handle single line breaks (when a line ends with two spaces)\n    let output = input.replace(/ {2}\\n/g, '<br />');\n\n    // Handle paragraphs (double line breaks)\n    output = output.replace(/\\n\\n/g, '</p><p class=\"leading-7 mb-4\">');\n\n    return output;\n}\n\n/**\n * Process headings (with optional anchor links)\n */\nfunction processHeadings(input: string, features?: MarkdownRenderOptions['features']): string {\n    if (!features?.headings) return input;\n\n    return input.replace(/^(#{1,6})\\s(.+)$/gm, (_, level, content) => {\n        const headingLevel = level.length;\n        const escapedContent = escapeHtml(content);\n        const id = content.toLowerCase().replace(/[^\\w]+/g, '-');\n\n        // Apply different styling based on heading level\n        let headingClasses = 'group relative flex items-center gap-2';\n\n        switch(headingLevel) {\n            case 1:\n                headingClasses += ' text-3xl font-bold mt-8 mb-4';\n                break;\n            case 2:\n                headingClasses += ' text-2xl font-semibold mt-6 mb-3';\n                break;\n            case 3:\n                headingClasses += ' text-xl font-medium mt-5 mb-3';\n                break;\n            case 4:\n                headingClasses += ' text-lg font-medium mt-4 mb-2';\n                break;\n            case 5:\n                headingClasses += ' text-base font-medium mt-3 mb-2';\n                break;\n            case 6:\n                headingClasses += ' text-sm font-medium mt-3 mb-2';\n                break;\n        }\n\n        return `<h${headingLevel} id=\"${id}\" class=\"${headingClasses}\">\n      ${escapedContent}\n      ${features.anchors ? `\n        <a href=\"#${id}\" class=\"opacity-0 group-hover:opacity-100 text-muted-foreground transition-opacity\">\n          <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n            <path d=\"M7.5 4H5.75A3.75 3.75 0 002 7.75v.5a3.75 3.75 0 003.75 3.75h1.5m-1.5-4h3m1.5-4h1.75A3.75 3.75 0 0114 7.75v.5a3.75 3.75 0 01-3.75 3.75H8.5\"/>\n          </svg>\n        </a>\n      ` : ''}\n    </h${headingLevel}>`;\n    });\n}\n\n/**\n * Process blockquotes\n */\nfunction processBlockquotes(input: string, features?: MarkdownRenderOptions['features']): string {\n    if (!features?.blockquotes) return input;\n\n    return input.replace(/^(>+)\\s(.+)$/gm, (_, level, content) => {\n        const depth = level.length;\n        const padding = (depth - 1) * 1.5;\n        return `<blockquote class=\"pl-4 py-2 border-l-2 border-border ml-${padding} italic text-muted-foreground my-4\">${escapeHtml(content)}</blockquote>`;\n    });\n}\n\n/**\n * Process tables\n */\nfunction processTables(input: string, features?: MarkdownRenderOptions['features']): string {\n    if (!features?.tables) return input;\n\n    // First, identify and process entire tables\n    return input.replace(/(\\|.+\\|\\n)+/g, (tableBlock) => {\n        const rows = tableBlock.trim().split('\\n');\n\n        // Extract the header row\n        const headerRow = rows[0];\n        const headerCells = headerRow\n            .split('|')\n            .slice(1, -1)\n            .map(cell => escapeHtml(cell.trim()));\n\n        // Check if we have alignment row\n        const isAlignmentRow = rows[1] && rows[1].includes('---');\n\n        // Process body rows\n        const bodyRows = rows.slice(isAlignmentRow ? 2 : 1);\n\n        // Create table HTML\n        let tableHtml = '<table class=\"w-full border-collapse border-2 rounded-md my-6 mx-auto\">\\n';\n\n        // Add header\n        tableHtml += '<thead class=\"bg-muted/50\">\\n<tr>\\n';\n        headerCells.forEach(cell => {\n            tableHtml += `<th class=\"border px-4 py-2 text-left font-medium\">${cell}</th>\\n`;\n        });\n        tableHtml += '</tr>\\n</thead>\\n';\n\n        // Add body\n        tableHtml += '<tbody>\\n';\n        bodyRows.forEach(row => {\n            const cells = row\n                .split('|')\n                .slice(1, -1)\n                .map(cell => escapeHtml(cell.trim()));\n\n            tableHtml += '<tr class=\"hover:bg-muted/20\">\\n';\n            cells.forEach(cell => {\n                tableHtml += `<td class=\"border px-4 py-2\">${cell}</td>\\n`;\n            });\n            tableHtml += '</tr>\\n';\n        });\n        tableHtml += '</tbody>\\n</table>';\n\n        return tableHtml;\n    });\n}\n\n/**\n * Process inline elements (bold, italic, etc.)\n */\nfunction processInlineFormatting(input: string, features?: MarkdownRenderOptions['features']): string {\n    let output = input;\n\n    // Bold\n    if (features?.bold) {\n        output = output.replace(/\\*\\*(.*?)\\*\\*/g, (_, content) => `<strong>${escapeHtml(content)}</strong>`);\n    }\n\n    // Italic\n    if (features?.italic) {\n        output = output.replace(/\\b_(.*?)_\\b/g, (_, content) => `<em>${escapeHtml(content)}</em>`);\n    }\n\n    // Strikethrough\n    if (features?.strikethrough) {\n        output = output.replace(/~~(.*?)~~/g, (_, content) => `<del>${escapeHtml(content)}</del>`);\n    }\n\n    return output;\n}\n\n/**\n * Process links\n */\nfunction processLinks(input: string, options: MarkdownRenderOptions): string {\n    if (!options.features?.links) return input;\n\n    const targetAttr = options.openLinksInNewTab ? ' target=\"_blank\" rel=\"noopener noreferrer\"' : '';\n\n    return input.replace(\n        /\\[([^\\]]+)\\]\\(([^)\"]+)(?:\\s+\"([^\"]+)\")?\\)/g,\n        (_, text, url, title) => {\n            let finalUrl = url;\n\n            // Add base URL if provided and URL is relative\n            if (options.baseUrl && !url.match(/^(https?:\\/\\/|mailto:|tel:)/)) {\n                finalUrl = new URL(url, options.baseUrl).toString();\n            }\n\n            const titleAttr = title ? ` title=\"${escapeHtml(title)}\"` : '';\n\n            return `<a href=\"${finalUrl}\"${titleAttr} class=\"text-primary hover:underline inline-flex items-center gap-1\"${targetAttr}>\n        ${escapeHtml(text)}\n        <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-external-link\">\n          <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"></path>\n          <polyline points=\"15 3 21 3 21 9\"></polyline>\n          <line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\"></line>\n        </svg>\n      </a>`;\n        }\n    );\n}\n\n/**\n * Process images\n */\nfunction processImages(input: string, options: MarkdownRenderOptions): string {\n    if (!options.features?.images) return input;\n\n    return input.replace(/!\\[(.*?)\\]\\(([^)]+)(?:\\s+\"([^\"]+)\")?\\)/g, (_, alt, src, title) => {\n        let finalSrc = src;\n\n        // Add base URL if provided and URL is relative\n        if (options.baseUrl && !src.match(/^(https?:\\/\\/)/)) {\n            finalSrc = new URL(src, options.baseUrl).toString();\n        }\n\n        const titleAttr = title ? ` title=\"${escapeHtml(title)}\"` : '';\n        const escapedAlt = escapeHtml(alt || '');\n\n        return `<img src=\"${finalSrc}\" alt=\"${escapedAlt}\"${titleAttr} class=\"max-w-full h-auto rounded-lg my-4\" loading=\"lazy\" />`;\n    });\n}\n\n/**\n * Process footnotes\n */\nfunction processFootnotes(input: string, features?: MarkdownRenderOptions['features']): string {\n    if (!features?.footnotes) return input;\n\n    // Process footnote references\n    let output = input.replace(/\\[\\^(\\d+)\\](?!:)/g, (_, num) =>\n        `<sup><a href=\"#fn${escapeHtml(num)}\" id=\"fnref${escapeHtml(num)}\">[${escapeHtml(num)}]</a></sup>`\n    );\n\n    // Process footnote definitions\n    output = output.replace(/\\[\\^(\\d+)\\]:\\s*(.+)$/gm, (_, num, content) => {\n        return `<div id=\"fn${escapeHtml(num)}\" class=\"text-sm text-muted-foreground mt-8 pt-2 border-t\">\n      ${escapeHtml(num)}. ${escapeHtml(content)}\n      <a href=\"#fnref${escapeHtml(num)}\" class=\"text-primary\">↩</a>\n    </div>`;\n    });\n\n    return output;\n}\n\n/**\n * Process horizontal rules\n */\nfunction processHorizontalRules(input: string, features?: MarkdownRenderOptions['features']): string {\n    if (!features?.horizontalRules) return input;\n\n    return input.replace(/^---$/gm, '<hr class=\"my-6 border-t border-border\">');\n}\n\n/**\n * Wrap paragraphs\n */\nfunction wrapParagraphs(input: string): string {\n    return input.replace(/([^\\n]+?)(?:\\n\\n|$)/g, (_, content) => {\n        if (\n            content.startsWith('<') || // Skip if content starts with HTML tag\n            content.trim() === '' // Skip empty lines\n        ) {\n            return content;\n        }\n        return `<p class=\"leading-7 mb-4\">${content}</p>\\n`;\n    });\n}\n\n/**\n * Apply custom renderers\n */\nfunction applyCustomRenderers(\n    input: string,\n    customRenderers?: Record<string, (content: string) => string>\n): string {\n    if (!customRenderers) return input;\n\n    let output = input;\n\n    // Apply each custom renderer\n    Object.entries(customRenderers).forEach(([pattern, renderer]) => {\n        const regex = new RegExp(pattern, 'g');\n        output = output.replace(regex, (match, ...args) => {\n            // Extract the content from the match (typically the first capture group)\n            const content = args[0] || match;\n            return renderer(content);\n        });\n    });\n\n    return output;\n}\n\n/**\n * Main renderer function\n */\nexport function renderMarkdown(markdown: string, options: MarkdownRenderOptions = {}): string {\n    // Merge options with defaults\n    const mergedOptions: MarkdownRenderOptions = {\n        ...DEFAULT_RENDER_OPTIONS,\n        ...options,\n        features: {\n            ...DEFAULT_RENDER_OPTIONS.features,\n            ...options.features,\n        },\n    };\n\n    let html = markdown;\n\n    // Process blocks in specific order\n    html = processCodeBlocks(html, mergedOptions.features);\n    html = processLists(html, mergedOptions.features);\n    html = processHeadings(html, mergedOptions.features);\n    html = processBlockquotes(html, mergedOptions.features);\n    html = processTables(html, mergedOptions.features);\n\n    // Process line breaks\n    html = processLineBreaks(html, mergedOptions.features);\n\n    // Process inline code (after blocks to avoid conflicts)\n    html = processInlineCode(html, mergedOptions.features);\n\n    // Process inline formatting\n    html = processInlineFormatting(html, mergedOptions.features);\n\n    // Process links\n    html = processLinks(html, mergedOptions);\n\n    // Process images\n    html = processImages(html, mergedOptions);\n\n    // Process footnotes\n    html = processFootnotes(html, mergedOptions.features);\n\n    // Process horizontal rules\n    html = processHorizontalRules(html, mergedOptions.features);\n\n    // Apply custom renderers\n    html = applyCustomRenderers(html, mergedOptions.customRenderers);\n\n    // Wrap paragraphs last\n    html = wrapParagraphs(html);\n\n    // Apply sanitization if needed\n    // Note: In a real-world implementation, you'd probably use a library like DOMPurify\n    if (!mergedOptions.allowHtml) {\n        // Simple sanitization (this is not comprehensive!)\n        html = html.replace(/<(script|iframe|object|embed|style)/gi, '&lt;$1');\n    }\n\n    // Wrap in a container with the specified class\n    if (mergedOptions.className) {\n        html = `<div class=\"${mergedOptions.className}\">${html}</div>`;\n    }\n\n    return html;\n}\n\n/**\n * React-friendly renderer component\n * This is just a placeholder generated by Grazie to help you with your component - the actual component would be in a .tsx file\n */\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport function MarkdownRenderer(props: {\n    children: string;\n    options?: MarkdownRenderOptions;\n}) {\n    // In a real implementation, you'd use:\n    // 1. A useEffect to run DOMPurify on client-side\n    // 2. Handle hydration issues with server/client rendering\n    // 3. Use dangerouslySetInnerHTML with proper sanitization\n\n    // Example of how it would be used:\n    // return (\n    //   <div\n    //     dangerouslySetInnerHTML={{ __html: renderMarkdown(props.children, props.options) }}\n    //     suppressHydrationWarning\n    //   />\n    // );\n\n    // This is just a placeholder\n    return null;\n}\n\n/**\n * Extract all headings from markdown text\n * Useful for generating table of contents\n */\nexport function extractHeadings(markdown: string): Array<{\n    text: string;\n    level: number;\n    id: string;\n}> {\n    const headings: Array<{ text: string; level: number; id: string }> = [];\n    const headingRegex = /^(#{1,6})\\s+(.+)$/gm;\n\n    let match;\n    while ((match = headingRegex.exec(markdown)) !== null) {\n        const level = match[1].length;\n        const text = match[2];\n        const id = text.toLowerCase().replace(/[^\\w]+/g, '-');\n\n        headings.push({ text, level, id });\n    }\n\n    return headings;\n}\n\n/**\n * Get word count from markdown text\n */\nexport function getWordCount(markdown: string): number {\n    // Remove code blocks to avoid counting code as words\n    const withoutCode = markdown.replace(/```[\\s\\S]*?```/g, '');\n\n    // Remove inline code\n    const withoutInlineCode = withoutCode.replace(/`[^`]+`/g, '');\n\n    // Remove URLs\n    const withoutUrls = withoutInlineCode.replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, '$1');\n\n    // Count words using regex\n    const words = withoutUrls.trim().match(/\\S+/g);\n    return words ? words.length : 0;\n}\n\n/**\n * Get reading time in minutes\n */\nexport function getReadingTime(markdown: string, wordsPerMinute: number = 200): number {\n    const wordCount = getWordCount(markdown);\n    const minutes = wordCount / wordsPerMinute;\n    return Math.ceil(minutes);\n}\n\n/**\n * Create a table of contents HTML\n */\nexport function generateTableOfContents(markdown: string): string {\n    const headings = extractHeadings(markdown);\n\n    if (headings.length === 0) {\n        return '';\n    }\n\n    let toc = '<nav class=\"toc mb-8\">\\n';\n    toc += '<h2 class=\"text-lg font-medium mb-4\">Table of Contents</h2>\\n';\n    toc += '<ul class=\"space-y-2 text-sm\">\\n';\n\n    // Track the current level for proper nesting\n    let currentLevel = 0;\n\n    headings.forEach((heading) => {\n        // Handle nesting\n        if (heading.level > currentLevel) {\n            // Indent deeper\n            for (let i = currentLevel; i < heading.level; i++) {\n                toc += '<ul class=\"ml-4 mt-2 space-y-2\">\\n';\n            }\n        } else if (heading.level < currentLevel) {\n            // Close deeper levels\n            for (let i = currentLevel; i > heading.level; i--) {\n                toc += '</ul>\\n';\n            }\n        }\n\n        // Update current level\n        currentLevel = heading.level;\n\n        // Add the heading link\n        toc += `<li><a href=\"#${heading.id}\" class=\"hover:text-primary hover:underline\">${heading.text}</a></li>\\n`;\n    });\n\n    // Close any remaining open lists\n    for (let i = currentLevel; i > 0; i--) {\n        toc += '</ul>\\n';\n    }\n\n    toc += '</ul>\\n</nav>';\n\n    return toc;\n}\n\n/**\n * Helper to detect if the markdown contains a specific element type\n */\nexport function containsElementType(markdown: string, type: 'table' | 'code' | 'heading' | 'list'): boolean {\n    switch (type) {\n        case 'table':\n            return /\\|(.+)\\|[\\r\\n]/.test(markdown);\n        case 'code':\n            return /```[\\s\\S]*?```/.test(markdown);\n        case 'heading':\n            return /^#{1,6}\\s+.+$/m.test(markdown);\n        case 'list':\n            return /^(\\s*)([-*+]|\\d+\\.)(\\s+)(.+)$/m.test(markdown);\n        default:\n            return false;\n    }\n}"
  },
  {
    "path": "components/markdown-editor/utils/safe-keyboard-shortcuts.tsx",
    "content": "'use client';\n\nimport React, { useCallback, useEffect, useRef } from 'react';\n\n/**\n * Keyboard shortcut definition\n */\nexport interface KeyboardShortcut {\n    key: string;\n    ctrlKey?: boolean;\n    shiftKey?: boolean;\n    altKey?: boolean;\n    metaKey?: boolean;\n    action: () => void;\n    description: string;\n}\n\n/**\n * Hook for safely adding keyboard shortcuts without breaking typing\n */\nexport function useSafeKeyboardShortcuts(\n    shortcuts: KeyboardShortcut[],\n    targetRef: React.RefObject<HTMLElement>,\n    enabled: boolean = true,\n    debug: boolean = false\n) {\n    // Ref to track currently active handlers\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n    const handlersRef = useRef<Function[]>([]);\n\n    // Log function that only logs in debug mode\n    const log = useCallback((message: string) => {\n        if (debug) {\n            console.log(`[KeyboardShortcuts] ${message}`);\n        }\n    }, [debug]);\n\n    // Check if a keyboard event matches a shortcut\n    const matchesShortcut = useCallback((event: KeyboardEvent, shortcut: KeyboardShortcut): boolean => {\n        // First check the key\n        if (shortcut.key.toLowerCase() !== event.key.toLowerCase()) {\n            return false;\n        }\n\n        // Check modifiers - shortcut requires modifier but event doesn't have it\n        if ((shortcut.ctrlKey || shortcut.metaKey) && !(event.ctrlKey || event.metaKey)) {\n            return false;\n        }\n\n        if (shortcut.shiftKey && !event.shiftKey) {\n            return false;\n        }\n\n        if (shortcut.altKey && !event.altKey) {\n            return false;\n        }\n\n        // Check modifiers - event has modifier but shortcut doesn't require it\n        // This ensures you can still type normally (e.g., shift+a for 'A')\n        if (!shortcut.ctrlKey && !shortcut.metaKey && (event.ctrlKey || event.metaKey)) {\n            return false;\n        }\n\n        if (!shortcut.altKey && event.altKey) {\n            return false;\n        }\n\n        return true;\n    }, []);\n\n    // Set up shortcuts\n    useEffect(() => {\n        // Clean up any existing handlers\n        handlersRef.current.forEach(cleanup => cleanup());\n        handlersRef.current = [];\n\n        // If not enabled or no target element, skip\n        if (!enabled || !targetRef.current) {\n            log('Shortcuts disabled or no target element');\n            return;\n        }\n\n        const targetElement = targetRef.current;\n        log(`Setting up ${shortcuts.length} shortcuts on ${targetElement.tagName}`);\n\n        // Create handler for this element\n        const handler = (event: KeyboardEvent) => {\n            // Skip if target is different (event bubbling)\n            if (event.target !== targetElement) {\n                return;\n            }\n\n            // Only process events with modifier keys when in an input or textarea\n            // This allows normal typing to work uninterrupted\n            const isInputElement =\n                event.target instanceof HTMLInputElement ||\n                event.target instanceof HTMLTextAreaElement;\n\n            if (isInputElement && !event.ctrlKey && !event.metaKey && !event.altKey) {\n                return;\n            }\n\n            // Check each shortcut\n            for (const shortcut of shortcuts) {\n                if (matchesShortcut(event, shortcut)) {\n                    log(`Matched shortcut: ${shortcut.description}`);\n\n                    // Prevent default to avoid triggering browser shortcuts\n                    event.preventDefault();\n\n                    try {\n                        // Execute the action\n                        shortcut.action();\n                    } catch (error) {\n                        console.error(`Error executing shortcut ${shortcut.description}:`, error);\n                    }\n\n                    // Stop after first match\n                    return;\n                }\n            }\n        };\n\n        // Add handler\n        targetElement.addEventListener('keydown', handler);\n\n        // Save cleanup function\n        handlersRef.current.push(() => {\n            targetElement.removeEventListener('keydown', handler);\n        });\n\n        // Cleanup function\n        return () => {\n            handlersRef.current.forEach(cleanup => cleanup());\n            handlersRef.current = [];\n        };\n    }, [shortcuts, targetRef, enabled, matchesShortcut, log]);\n\n    // Helper to manually clean up handlers\n    const cleanupHandlers = useCallback(() => {\n        handlersRef.current.forEach(cleanup => cleanup());\n        handlersRef.current = [];\n    }, []);\n\n    return { cleanupHandlers };\n}\n\n/**\n * Component that adds keyboard shortcuts to children\n */\nexport function KeyboardShortcutsProvider({\n                                              shortcuts,\n                                              children,\n                                              enabled = true,\n                                              debug = false\n                                          }: {\n    shortcuts: KeyboardShortcut[];\n    children: React.ReactNode;\n    enabled?: boolean;\n    debug?: boolean;\n}) {\n    // Ref for the container\n    const containerRef = useRef<HTMLDivElement>(null);\n\n    // Use our hook\n    // @ts-expect-error containerRef might throw an error if initialized too early\n    useSafeKeyboardShortcuts(shortcuts, containerRef, enabled, debug);\n\n    return (\n        <div ref={containerRef} className=\"keyboard-shortcuts-container\">\n        {children}\n        </div>\n);\n}\n\nexport default useSafeKeyboardShortcuts;"
  },
  {
    "path": "components/project/ProjectNavItem.tsx",
    "content": "import Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport { cn } from '@/lib/utils'\nimport { LucideIcon } from 'lucide-react'\n\ninterface ProjectNavItemProps {\n    href: string\n    icon: LucideIcon\n    label: string\n    isActive?: boolean\n}\n\nexport function ProjectNavItem({\n                                   href,\n                                   icon: Icon,\n                                   label,\n                                   isActive\n                               }: ProjectNavItemProps) {\n    const pathname = usePathname()\n    const isCurrentPath = isActive ?? pathname === href\n\n    return (\n        <Link\n            href={href}\n            className={cn(\n                \"flex items-center py-2 px-3 text-sm rounded-md transition-colors\",\n                isCurrentPath\n                    ? \"bg-accent text-accent-foreground font-medium\"\n                    : \"text-muted-foreground hover:text-foreground hover:bg-accent/50\"\n            )}\n        >\n            <Icon className=\"mr-2 h-4 w-4\" />\n            <span>{label}</span>\n        </Link>\n    )\n}"
  },
  {
    "path": "components/project/ProjectSettingsPage.tsx",
    "content": "'use client'\n\nimport { useState } from 'react'\nimport { useQuery } from '@tanstack/react-query'\nimport { Plus, Search, Grid, List, Calendar, Star, Sparkles, Settings } from 'lucide-react'\nimport { motion, AnimatePresence } from 'framer-motion'\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { Badge } from '@/components/ui/badge'\nimport { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card'\nimport { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'\nimport Link from 'next/link'\nimport { useAuth } from '@/context/auth'\nimport { Skeleton } from '@/components/ui/skeleton'\nimport { Project as PrismaProject, Changelog, ChangelogEntry } from '@prisma/client'\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'\nimport { useTimezone } from '@/hooks/use-timezone'\n\n// Project interface\ninterface Project extends PrismaProject {\n    entryCount: number\n    latestEntry: (ChangelogEntry & { version: string }) | null\n    changelog?: Changelog & {\n        entries: ChangelogEntry[]\n    }\n}\n\n// Generate vibrant pastel colors for project cards\nconst getProjectColor = (id: string) => {\n    // Use the project id to generate a consistent color\n    const hash = id.split('').reduce((acc, char) => char.charCodeAt(0) + acc, 0);\n    const hue = hash % 360;\n    return `hsl(${hue}, 85%, 88%)`;\n}\n\n// Format date helper function\nconst formatDate = (dateString: string | number | Date, timeZone = 'UTC') => {\n    const date = new Date(dateString)\n    return new Intl.DateTimeFormat('en-US', {\n        month: 'short',\n        day: 'numeric',\n        year: 'numeric',\n        timeZone,\n    }).format(date)\n}\n\nexport default function ProjectsPage() {\n    const { user } = useAuth()\n    const timezone = useTimezone()\n    const [searchTerm, setSearchTerm] = useState('')\n    const [viewType, setViewType] = useState('grid')\n    const [sortOrder, setSortOrder] = useState('newest')\n\n    // Fetch projects data\n    const { data: projects, isLoading } = useQuery<Project[]>({\n        queryKey: ['projects'],\n        queryFn: async () => {\n            const response = await fetch('/api/projects', {\n                credentials: 'include',\n            })\n            if (!response.ok) throw new Error('Failed to fetch projects')\n            return response.json()\n        }\n    })\n\n    // Filter projects based on search term\n    const filteredProjects = projects?.filter(project =>\n        project.name.toLowerCase().includes(searchTerm.toLowerCase())\n    ) || []\n\n    // Sort projects based on selected order\n    const sortedProjects = [...(filteredProjects || [])].sort((a, b) => {\n        if (sortOrder === 'newest') {\n            return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()\n        } else if (sortOrder === 'oldest') {\n            return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()\n        } else if (sortOrder === 'name') {\n            return a.name.localeCompare(b.name)\n        } else if (sortOrder === 'entries') {\n            return (b.entryCount || 0) - (a.entryCount || 0)\n        }\n        return 0\n    })\n\n    // Fun empty state component\n    const EmptyState = () => (\n        <motion.div\n            initial={{ opacity: 0, y: 20 }}\n            animate={{ opacity: 1, y: 0 }}\n            className=\"flex flex-col items-center justify-center h-64 p-6 text-center bg-muted/30 rounded-lg border-2 border-dashed border-muted\"\n        >\n            <motion.div\n                initial={{ scale: 0.8 }}\n                animate={{ scale: 1 }}\n                transition={{ repeat: Infinity, repeatType: \"reverse\", duration: 2 }}\n            >\n                <Sparkles className=\"h-16 w-16 text-primary mb-4\" />\n            </motion.div>\n            <h3 className=\"text-xl font-bold mb-2\">No projects found</h3>\n            <p className=\"text-muted-foreground mb-4 max-w-md\">\n                {searchTerm ?\n                    `No projects match \"${searchTerm}\"` :\n                    \"Start tracking your changelogs by creating your first project!\"}\n            </p>\n            <Link href=\"/dashboard/projects/new\">\n                <Button className=\"hover:opacity-90 transition-opacity\">\n                    <Plus className=\"w-4 h-4 mr-2\" />\n                    Create Your First Project\n                </Button>\n            </Link>\n        </motion.div>\n    )\n\n    // Project card animations\n    const cardVariants = {\n        hidden: { opacity: 0, y: 20 },\n        visible: (index: number) => ({\n            opacity: 1,\n            y: 0,\n            transition: {\n                delay: index * 0.05,\n                duration: 0.3\n            }\n        }),\n        exit: { opacity: 0, scale: 0.9 }\n    }\n\n    return (\n        <TooltipProvider>\n            <div className=\"space-y-6\">\n                {/* Header section */}\n                <motion.div\n                    initial={{ opacity: 0, y: -10 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    className=\"flex flex-col md:flex-row gap-4 justify-between items-start md:items-center\"\n                >\n                    <div>\n                        <h2 className=\"text-3xl font-bold tracking-tight mb-1\">Your Projects</h2>\n                        <p className=\"text-muted-foreground\">\n                            {!isLoading && `Manage ${filteredProjects.length} project${filteredProjects.length !== 1 ? 's' : ''}`}\n                        </p>\n                    </div>\n                    <Link href=\"/dashboard/projects/new\">\n                        <Button size=\"lg\" className=\"hover:shadow-md transition-all\">\n                            <Plus className=\"w-4 h-4 mr-2\" />\n                            New Project\n                        </Button>\n                    </Link>\n                </motion.div>\n\n                {/* Search and filters */}\n                <motion.div\n                    initial={{ opacity: 0, y: 10 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    transition={{ delay: 0.1 }}\n                    className=\"flex flex-col sm:flex-row gap-4 bg-card/30 p-3 rounded-lg\"\n                >\n                    <div className=\"relative flex-grow\">\n                        <Search className=\"absolute left-3 top-3 h-4 w-4 text-muted-foreground\" />\n                        <Input\n                            placeholder=\"Search projects...\"\n                            value={searchTerm}\n                            onChange={(e) => setSearchTerm(e.target.value)}\n                            className=\"pl-9 bg-background/70 border-muted-foreground/20\"\n                        />\n                    </div>\n\n                    <div className=\"flex gap-2\">\n                        <Tabs value={sortOrder} onValueChange={setSortOrder} className=\"w-auto\">\n                            <TabsList className=\"bg-muted/60 h-10\">\n                                <TabsTrigger value=\"newest\" className=\"text-xs px-3\">\n                                    Newest\n                                </TabsTrigger>\n                                <TabsTrigger value=\"name\" className=\"text-xs px-3\">\n                                    Name\n                                </TabsTrigger>\n                                <TabsTrigger value=\"entries\" className=\"text-xs px-3\">\n                                    Most Active\n                                </TabsTrigger>\n                            </TabsList>\n                        </Tabs>\n\n                        <Tabs value={viewType} onValueChange={setViewType} className=\"w-auto\">\n                            <TabsList className=\"bg-muted/60 h-10\">\n                                <TabsTrigger value=\"grid\" className=\"px-3\">\n                                    <Grid className=\"h-4 w-4\" />\n                                </TabsTrigger>\n                                <TabsTrigger value=\"list\" className=\"px-3\">\n                                    <List className=\"h-4 w-4\" />\n                                </TabsTrigger>\n                            </TabsList>\n                        </Tabs>\n                    </div>\n                </motion.div>\n\n                {/* Loading skeletons */}\n                {isLoading && (\n                    viewType === 'grid' ? (\n                        <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\">\n                            {[...Array(6)].map((_, i) => (\n                                <Card key={i} className=\"overflow-hidden\">\n                                    <CardHeader className=\"pb-2\">\n                                        <Skeleton className=\"h-6 w-3/4 mb-2\" />\n                                        <Skeleton className=\"h-4 w-1/2\" />\n                                    </CardHeader>\n                                    <CardContent>\n                                        <Skeleton className=\"h-4 w-full mb-3\" />\n                                        <Skeleton className=\"h-4 w-3/4\" />\n                                    </CardContent>\n                                    <CardFooter>\n                                        <Skeleton className=\"h-9 w-full\" />\n                                    </CardFooter>\n                                </Card>\n                            ))}\n                        </div>\n                    ) : (\n                        <div className=\"space-y-3\">\n                            {[...Array(5)].map((_, i) => (\n                                <div key={i} className=\"flex items-center p-4 border rounded-lg\">\n                                    <div className=\"flex-1\">\n                                        <Skeleton className=\"h-6 w-1/3 mb-2\" />\n                                        <Skeleton className=\"h-4 w-1/4\" />\n                                    </div>\n                                    <Skeleton className=\"h-9 w-24 mr-2\" />\n                                    <Skeleton className=\"h-9 w-24\" />\n                                </div>\n                            ))}\n                        </div>\n                    )\n                )}\n\n                {/* No results */}\n                {!isLoading && sortedProjects.length === 0 && <EmptyState />}\n\n                {/* Grid view */}\n                {!isLoading && sortedProjects.length > 0 && viewType === 'grid' && (\n                    <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5\">\n                        <AnimatePresence>\n                            {sortedProjects.map((project, index) => {\n                                const projectColor = getProjectColor(project.id);\n\n                                return (\n                                    <motion.div\n                                        key={project.id}\n                                        custom={index}\n                                        variants={cardVariants}\n                                        initial=\"hidden\"\n                                        animate=\"visible\"\n                                        exit=\"exit\"\n                                        layout\n                                        layoutId={`project-${project.id}`}\n                                        whileHover={{ y: -5, transition: { duration: 0.2 } }}\n                                    >\n                                        <Card className=\"h-full overflow-hidden hover:shadow-lg transition-all cursor-pointer border border-border/60 group\">\n                                            <div className=\"h-2 group-hover:h-3 transition-all\" style={{ background: projectColor }} />\n                                            <CardHeader className=\"pb-2\">\n                                                <div className=\"flex justify-between items-start\">\n                                                    <CardTitle className=\"text-xl font-semibold line-clamp-1\">{project.name}</CardTitle>\n                                                    {project.isPublic && (\n                                                        <Badge variant=\"secondary\" className=\"text-xs\">Public</Badge>\n                                                    )}\n                                                </div>\n                                                <CardDescription className=\"flex items-center gap-1\">\n                                                    {project.latestEntry?.version ? (\n                                                        <>\n                                                            <span className=\"text-xs px-1.5 py-0.5 rounded bg-accent\">{project.latestEntry.version}</span>\n                                                        </>\n                                                    ) : (\n                                                        <span className=\"text-xs text-muted-foreground\">No versions yet</span>\n                                                    )}\n                                                </CardDescription>\n                                            </CardHeader>\n                                            <CardContent>\n                                                <div className=\"flex gap-2 flex-wrap\">\n                                                    <Badge variant=\"outline\" className=\"flex gap-1 items-center bg-muted/40\">\n                                                        <Calendar className=\"h-3 w-3\" />\n                                                        {formatDate(project.createdAt, timezone)}\n                                                    </Badge>\n                                                    <Badge variant=\"outline\" className=\"flex gap-1 items-center bg-muted/40\">\n                                                        <Star className=\"h-3 w-3\" />\n                                                        {project.entryCount || 0} entries\n                                                    </Badge>\n                                                </div>\n                                            </CardContent>\n                                            <CardFooter className=\"pt-2\">\n                                                <div className=\"flex gap-2 w-full\">\n                                                    <Link href={`/dashboard/projects/${project.id}`} className=\"flex-1\">\n                                                        <Button variant=\"secondary\" className=\"w-full\">\n                                                            View Changelog\n                                                        </Button>\n                                                    </Link>\n\n                                                    {user?.role === 'ADMIN' && (\n                                                        <Tooltip>\n                                                            <TooltipTrigger asChild>\n                                                                <Link href={`/dashboard/projects/${project.id}/settings`}>\n                                                                    <Button variant=\"outline\" size=\"icon\" className=\"h-10 w-10\">\n                                                                        <Settings className=\"h-4 w-4\" />\n                                                                    </Button>\n                                                                </Link>\n                                                            </TooltipTrigger>\n                                                            <TooltipContent>\n                                                                <span>Project Settings</span>\n                                                            </TooltipContent>\n                                                        </Tooltip>\n                                                    )}\n                                                </div>\n                                            </CardFooter>\n                                        </Card>\n                                    </motion.div>\n                                )\n                            })}\n                        </AnimatePresence>\n                    </div>\n                )}\n\n                {/* List view */}\n                {!isLoading && sortedProjects.length > 0 && viewType === 'list' && (\n                    <div className=\"space-y-3\">\n                        <AnimatePresence>\n                            {sortedProjects.map((project, index) => {\n                                const projectColor = getProjectColor(project.id);\n\n                                return (\n                                    <motion.div\n                                        key={project.id}\n                                        custom={index}\n                                        variants={cardVariants}\n                                        initial=\"hidden\"\n                                        animate=\"visible\"\n                                        exit=\"exit\"\n                                        layout\n                                        layoutId={`project-${project.id}-list`}\n                                        whileHover={{ x: 4, scale: 1.01, transition: { duration: 0.2 } }}\n                                    >\n                                        <div\n                                            className=\"flex items-center p-4 border rounded-lg hover:bg-card/50 hover:shadow-md transition-all cursor-pointer group relative overflow-hidden\"\n                                        >\n                                            <div\n                                                className=\"absolute left-0 top-0 bottom-0 w-1 group-hover:w-1.5 transition-all\"\n                                                style={{ background: projectColor }}\n                                            />\n\n                                            <div className=\"flex-1 ml-3\">\n                                                <div className=\"flex items-center gap-2\">\n                                                    <Link href={`/dashboard/projects/${project.id}`}>\n                                                        <h3 className=\"font-semibold text-lg hover:text-primary transition-colors\">{project.name}</h3>\n                                                    </Link>\n                                                    {project.isPublic && (\n                                                        <Badge variant=\"secondary\" className=\"text-xs\">Public</Badge>\n                                                    )}\n                                                </div>\n                                                <div className=\"flex gap-3 text-sm text-muted-foreground mt-1\">\n                                                    <span className=\"flex items-center gap-1\">\n                                                        <Calendar className=\"h-3 w-3\" />\n                                                        {formatDate(project.createdAt, timezone)}\n                                                    </span>\n                                                    <span className=\"flex items-center gap-1\">\n                                                        <Star className=\"h-3 w-3\" />\n                                                        {project.entryCount || 0} entries\n                                                    </span>\n                                                    {project.latestEntry?.version && (\n                                                        <span className=\"px-1.5 py-0.5 bg-muted rounded text-xs\">\n                                                            {project.latestEntry.version}\n                                                        </span>\n                                                    )}\n                                                </div>\n                                            </div>\n\n                                            <div className=\"flex gap-2\">\n                                                <Link href={`/dashboard/projects/${project.id}`}>\n                                                    <Button variant=\"secondary\" size=\"sm\">\n                                                        View Changelog\n                                                    </Button>\n                                                </Link>\n\n                                                {user?.role === 'ADMIN' && (\n                                                    <Tooltip>\n                                                        <TooltipTrigger asChild>\n                                                            <Link href={`/dashboard/projects/${project.id}/settings`}>\n                                                                <Button variant=\"outline\" size=\"sm\">\n                                                                    <Settings className=\"h-4 w-4 mr-1\" />\n                                                                    Settings\n                                                                </Button>\n                                                            </Link>\n                                                        </TooltipTrigger>\n                                                        <TooltipContent>\n                                                            <span>Project Settings</span>\n                                                        </TooltipContent>\n                                                    </Tooltip>\n                                                )}\n                                            </div>\n                                        </div>\n                                    </motion.div>\n                                )\n                            })}\n                        </AnimatePresence>\n                    </div>\n                )}\n            </div>\n        </TooltipProvider>\n    )\n}"
  },
  {
    "path": "components/project/ProjectSidebar.tsx",
    "content": "// /components/project/ProjectSidebar.tsx\n\n'use client'\n\nimport React from 'react'\nimport Link from 'next/link'\nimport {usePathname} from 'next/navigation'\nimport {useQuery} from '@tanstack/react-query'\nimport {\n    ChevronLeft,\n    LayoutDashboard,\n    Settings,\n    FileText,\n    ExternalLink,\n    Plus,\n    Clock,\n    Bookmark,\n    Eye,\n    Star,\n    Code,\n    History,\n    UserSquare2,\n    PenTool,\n    MailIcon,\n    Rss,\n    type LucideIcon,\n    ChartNoAxesCombined,\n    Globe,\n    Key\n} from 'lucide-react';\nimport {SiGithub} from '@icons-pack/react-simple-icons';\nimport {Button} from '@/components/ui/button'\nimport {ScrollArea} from '@/components/ui/scroll-area'\nimport {Separator} from '@/components/ui/separator'\nimport {Skeleton} from '@/components/ui/skeleton'\nimport {Badge} from '@/components/ui/badge'\nimport {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from '@/components/ui/tooltip'\nimport {Alert, AlertDescription} from '@/components/ui/alert'\nimport {cn} from '@/lib/utils'\nimport {formatDistanceToNow} from 'date-fns'\nimport {useBookmarks} from '@/hooks/useBookmarks'\n\ninterface NavItemProps {\n    href: string\n    icon: LucideIcon\n    label: string\n    active?: boolean\n    external?: boolean\n    badge?: string\n    disabled?: boolean\n}\n\ninterface ChangelogEntry {\n    id: string\n    title: string\n    createdAt: string\n    updatedAt: string\n    publishedAt: string | null\n    version: string | null\n}\n\ninterface ChangelogData {\n    entries: ChangelogEntry[]\n    totalCount: number\n}\n\ninterface Project {\n    id: string\n    name: string\n    isPublic: boolean\n}\n\nfunction NavItem({href, icon: Icon, label, active, external, badge, disabled}: NavItemProps) {\n    if (disabled) {\n        return (\n            <TooltipProvider>\n                <Tooltip>\n                    <TooltipTrigger asChild>\n                        <div className={cn(\n                            \"flex items-center justify-between py-2 px-3 text-sm rounded-md\",\n                            \"text-muted-foreground/50 bg-muted/20 cursor-not-allowed\"\n                        )}>\n                            <div className=\"flex items-center\">\n                                <Icon className=\"mr-2 h-4 w-4 flex-shrink-0\"/>\n                                <span className=\"truncate\">{label}</span>\n                            </div>\n                            {badge && (\n                                <Badge variant=\"outline\" className=\"ml-2 text-xs opacity-50 flex-shrink-0\">\n                                    {badge}\n                                </Badge>\n                            )}\n                        </div>\n                    </TooltipTrigger>\n                    <TooltipContent>\n                        <p className=\"text-xs\">Requires public project</p>\n                    </TooltipContent>\n                </Tooltip>\n            </TooltipProvider>\n        )\n    }\n\n    return (\n        <Link\n            href={href}\n            className={cn(\n                \"flex items-center justify-between py-2 px-3 text-sm rounded-md transition-colors group\",\n                active\n                    ? \"bg-accent text-accent-foreground font-medium\"\n                    : \"text-muted-foreground hover:text-foreground hover:bg-accent/50\"\n            )}\n            {...(external ? {target: \"_blank\", rel: \"noopener noreferrer\"} : {})}\n        >\n            <div className=\"flex items-center min-w-0\">\n                <Icon className=\"mr-2 h-4 w-4 flex-shrink-0\"/>\n                <span className=\"truncate\">{label}</span>\n            </div>\n            {badge && (\n                <Badge variant=\"outline\" className=\"ml-2 text-xs bg-primary/5 group-hover:bg-primary/10 flex-shrink-0\">\n                    {badge}\n                </Badge>\n            )}\n            {external && <ExternalLink className=\"ml-2 h-3 w-3 opacity-70 flex-shrink-0\"/>}\n        </Link>\n    )\n}\n\ninterface RecentChangelogProps {\n    id: string\n    projectId: string\n    title: string\n    date: string\n    version?: string | null\n    isPublished?: boolean\n}\n\nfunction RecentChangelog({\n                             id,\n                             projectId,\n                             title,\n                             date,\n                             version,\n                             isPublished\n                         }: RecentChangelogProps) {\n    const {toggleBookmark, isBookmarked} = useBookmarks({\n        projectId,\n        entryId: id\n    });\n\n    const handleBookmarkClick = async (e: React.MouseEvent) => {\n        e.preventDefault();\n        e.stopPropagation();\n        await toggleBookmark(id, title, projectId);\n    };\n\n    return (\n        <div className=\"group relative\">\n            <Link\n                href={`/dashboard/projects/${projectId}/changelog/${id}`}\n                className=\"block p-2 pr-10 hover:bg-accent/50 rounded-md transition-colors\"\n            >\n                <div className=\"flex items-start gap-2\">\n                    <div\n                        className=\"h-8 w-8 bg-primary/10 rounded-md flex items-center justify-center mt-0.5 flex-shrink-0\">\n                        <FileText className=\"h-4 w-4 text-primary\"/>\n                    </div>\n                    <div className=\"flex-1 min-w-0\">\n                        <div className=\"flex items-start gap-1.5\">\n                            <h4 className=\"font-medium text-sm group-hover:text-primary transition-colors break-words line-clamp-2\">\n                                {title}\n                                {!isPublished && (\n                                    <Badge\n                                        variant=\"outline\"\n                                        className=\"ml-1.5 inline-flex align-baseline h-5 px-1 text-xs bg-amber-500/10 text-amber-700 dark:bg-amber-500/20 dark:text-amber-300 border-amber-200 dark:border-amber-800/40\"\n                                    >\n                                        Draft\n                                    </Badge>\n                                )}\n                            </h4>\n                        </div>\n                        <div className=\"flex items-center gap-2 mt-1\">\n                            <div className=\"flex items-center text-xs text-muted-foreground\">\n                                <Clock className=\"h-3 w-3 mr-1\"/>\n                                <span>{formatDistanceToNow(new Date(date))} ago</span>\n                            </div>\n                            {version && (\n                                <Badge variant=\"secondary\" className=\"h-4 px-1 text-[10px]\">\n                                    {version}\n                                </Badge>\n                            )}\n                        </div>\n                    </div>\n                </div>\n            </Link>\n            <div className=\"absolute right-2 top-3\">\n                <TooltipProvider>\n                    <Tooltip>\n                        <TooltipTrigger asChild>\n                            <Button\n                                variant=\"ghost\"\n                                size=\"icon\"\n                                className=\"h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity\"\n                                onClick={handleBookmarkClick}\n                            >\n                                <Star\n                                    className={cn(\n                                        \"h-3.5 w-3.5\",\n                                        isBookmarked\n                                            ? \"text-amber-500 fill-amber-500\"\n                                            : \"text-muted-foreground\"\n                                    )}\n                                />\n                            </Button>\n                        </TooltipTrigger>\n                        <TooltipContent>\n                            <p className=\"text-xs\">{isBookmarked ? \"Remove bookmark\" : \"Bookmark\"}</p>\n                        </TooltipContent>\n                    </Tooltip>\n                </TooltipProvider>\n            </div>\n        </div>\n    )\n}\n\ninterface BookmarkedChangelogProps {\n    id: string\n    projectId: string\n    title: string\n}\n\nfunction BookmarkedChangelog({id, projectId, title}: BookmarkedChangelogProps) {\n    const {removeBookmark} = useBookmarks({projectId});\n\n    const handleRemoveBookmark = async (e: React.MouseEvent) => {\n        e.preventDefault();\n        e.stopPropagation();\n        await removeBookmark(id, projectId);\n    };\n\n    return (\n        <div className=\"group relative\">\n            <Link\n                href={`/dashboard/projects/${projectId}/changelog/${id}`}\n                className=\"flex items-center gap-2 p-2 pr-10 hover:bg-accent/50 rounded-md text-sm transition-colors\"\n            >\n                <Bookmark className=\"h-4 w-4 text-amber-500 flex-shrink-0\"/>\n                <span className=\"line-clamp-1 break-words\">\n                    {title}\n                </span>\n            </Link>\n            <div className=\"absolute right-2 top-1/2 -translate-y-1/2\">\n                <TooltipProvider>\n                    <Tooltip>\n                        <TooltipTrigger asChild>\n                            <Button\n                                variant=\"ghost\"\n                                size=\"icon\"\n                                className=\"h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity\"\n                                onClick={handleRemoveBookmark}\n                            >\n                                <Star className=\"h-3.5 w-3.5 text-amber-500 fill-amber-500\"/>\n                            </Button>\n                        </TooltipTrigger>\n                        <TooltipContent>\n                            <p className=\"text-xs\">Remove bookmark</p>\n                        </TooltipContent>\n                    </Tooltip>\n                </TooltipProvider>\n            </div>\n        </div>\n    )\n}\n\nexport function ProjectSidebar({projectId}: { projectId: string }) {\n    const pathname = usePathname()\n    const {bookmarks, isLoading: isLoadingBookmarks} = useBookmarks({projectId});\n\n    // Fetch project details\n    const {data: project, isLoading: isLoadingProject} = useQuery<Project>({\n        queryKey: ['project', projectId],\n        queryFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}`)\n            if (!response.ok) throw new Error('Failed to fetch project')\n            return response.json()\n        }\n    })\n\n    // Fetch recent changelogs\n    const {data: changelogData, isLoading: isLoadingChangelogs} = useQuery<ChangelogData>({\n        queryKey: ['recent-changelogs', projectId],\n        queryFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}/changelog?limit=4`)\n            if (!response.ok) throw new Error('Failed to fetch recent changelogs')\n            return response.json()\n        }\n    })\n\n    // Determine if project is public\n    const isPublic = project?.isPublic || false;\n\n    // Determine the changelog count\n    const changelogCount = changelogData?.totalCount || 0;\n    const publishedCount = changelogData?.entries?.filter((e) => e.publishedAt)?.length || 0;\n    const draftCount = changelogData?.entries?.filter((e) => !e.publishedAt)?.length || 0;\n\n    // Construct RSS feed URL\n    const rssUrl = `/changelog/${projectId}/rss.xml`;\n\n    if (isLoadingProject) {\n        return (\n            <div\n                className=\"hidden md:flex fixed inset-y-0 left-0 z-40 flex-col border-r bg-background w-64 transition-all duration-300\">\n                <div className=\"p-4 border-b\">\n                    <Skeleton className=\"h-8 w-36\"/>\n                </div>\n                <div className=\"p-4 space-y-3\">\n                    {Array.from({length: 4}).map((_, i) => (\n                        <Skeleton key={i} className=\"h-8 w-full\"/>\n                    ))}\n                </div>\n            </div>\n        )\n    }\n\n    return (\n        <div\n            className=\"hidden md:flex fixed inset-y-0 left-0 z-40 flex-col border-r bg-background w-64 transition-all duration-300\">\n            {/* Header */}\n            <div className=\"h-16 flex items-center justify-between border-b p-4\">\n                <div className=\"flex items-center gap-2 min-w-0\">\n                    <Button\n                        variant=\"ghost\"\n                        size=\"icon\"\n                        asChild\n                        className=\"h-8 w-8 flex-shrink-0\"\n                    >\n                        <Link href=\"/dashboard/projects\">\n                            <ChevronLeft className=\"h-4 w-4\"/>\n                            <span className=\"sr-only\">Back to projects</span>\n                        </Link>\n                    </Button>\n                    <TooltipProvider>\n                        <Tooltip>\n                            <TooltipTrigger asChild>\n                                <h2 className=\"font-semibold truncate flex-1\">{project?.name || 'Project'}</h2>\n                            </TooltipTrigger>\n                            <TooltipContent>\n                                <p className=\"text-xs\">{project?.name}</p>\n                            </TooltipContent>\n                        </Tooltip>\n                    </TooltipProvider>\n                </div>\n\n                <TooltipProvider>\n                    <Tooltip>\n                        <TooltipTrigger asChild>\n                            <Button\n                                size=\"sm\"\n                                className=\"h-8 gap-1 flex-shrink-0\"\n                                asChild\n                            >\n                                <Link href={`/dashboard/projects/${projectId}/changelog/new`}>\n                                    <Plus className=\"h-3.5 w-3.5\"/>\n                                    <span className=\"text-xs\">New</span>\n                                </Link>\n                            </Button>\n                        </TooltipTrigger>\n                        <TooltipContent>\n                            <p className=\"text-xs\">Create a new changelog entry</p>\n                        </TooltipContent>\n                    </Tooltip>\n                </TooltipProvider>\n            </div>\n\n            {/* Navigation */}\n            <ScrollArea className=\"flex-1\">\n                <div className=\"py-4 px-3\">\n                    <nav className=\"space-y-1\">\n                        <NavItem\n                            href={`/dashboard/projects/${projectId}`}\n                            icon={LayoutDashboard}\n                            label=\"Overview\"\n                            active={pathname === `/dashboard/projects/${projectId}`}\n                        />\n\n                        <NavItem\n                            href={`/dashboard/projects/${projectId}/changelog`}\n                            icon={FileText}\n                            label=\"All Changelogs\"\n                            badge={changelogCount > 0 ? changelogCount.toString() : undefined}\n                            active={\n                                pathname.includes(`/dashboard/projects/${projectId}/changelog`) &&\n                                !pathname.includes(`/new`)\n                            }\n                        />\n\n                        <NavItem\n                            href={`/changelog/${projectId}`}\n                            icon={Eye}\n                            label=\"View Public Page\"\n                            external={true}\n                            disabled={!isPublic}\n                        />\n                    </nav>\n\n                    <Separator className=\"my-3\"/>\n\n                    <div className=\"px-3 mb-2\">\n                        <h3 className=\"text-xs font-semibold text-muted-foreground uppercase tracking-wider\">\n                            Integrations\n                        </h3>\n                    </div>\n\n                    <nav className=\"space-y-1\">\n                        <NavItem\n                            href={`/dashboard/projects/${projectId}/integrations/widget`}\n                            icon={Code}\n                            label=\"Widget\"\n                            active={pathname.includes(`/dashboard/projects/${projectId}/integrations/widget`)}\n                            disabled={!isPublic}\n                        />\n\n                        <NavItem\n                            href={`/dashboard/projects/${projectId}/integrations/email`}\n                            icon={MailIcon}\n                            label=\"Email\"\n                            active={pathname.includes(`/dashboard/projects/${projectId}/integrations/email`)}\n                        />\n                        <NavItem\n                            href={`/dashboard/projects/${projectId}/integrations/github`}\n                            icon={SiGithub}\n                            label=\"GitHub\"\n                            active={pathname.includes(`/dashboard/projects/${projectId}/integrations/github`)}\n                        />\n                        <NavItem\n                            href={`/dashboard/projects/${projectId}/analytics`}\n                            icon={ChartNoAxesCombined}\n                            label=\"Analytics\"\n                            active={pathname.includes(`/dashboard/projects/${projectId}/analytics`)}\n                        />\n                        <NavItem\n                            href={`/dashboard/projects/${projectId}/domains`}\n                            icon={Globe}\n                            label=\"Domains\"\n                            active={pathname.includes(`/dashboard/projects/${projectId}/domains`)}\n                            disabled={!isPublic}\n                        />\n                        <NavItem\n                            href={`/dashboard/projects/${projectId}/api-keys`}\n                            icon={Key}\n                            label=\"API Keys\"\n                            active={pathname.includes(`/dashboard/projects/${projectId}/api-keys`)}\n                        />\n                    </nav>\n\n                    <Separator className=\"my-3\"/>\n\n                    <nav className=\"space-y-1\">\n                        <NavItem\n                            href={`/dashboard/projects/${projectId}/settings`}\n                            icon={Settings}\n                            label=\"Settings\"\n                            active={pathname === `/dashboard/projects/${projectId}/settings`}\n                        />\n                    </nav>\n                </div>\n\n                {/* Public Project Alert */}\n                {!isPublic && (\n                    <div className=\"px-3 py-2\">\n                        <Alert variant=\"warning\" className=\"py-2 px-3\">\n                            <AlertDescription className=\"text-xs\">\n                                Make this project public in settings to enable all features.\n                            </AlertDescription>\n                        </Alert>\n                    </div>\n                )}\n\n                {/* Bookmarks Section */}\n                {!isLoadingBookmarks && bookmarks.length > 0 && (\n                    <div className=\"py-2 px-3 mt-2\">\n                        <div className=\"flex items-center mb-2\">\n                            <Star className=\"h-4 w-4 text-amber-500 mr-1.5\"/>\n                            <h3 className=\"text-xs font-semibold\">Bookmarked</h3>\n                        </div>\n\n                        <div className=\"space-y-1\">\n                            {bookmarks.map((bookmark) => (\n                                <BookmarkedChangelog\n                                    key={bookmark.id}\n                                    id={bookmark.id}\n                                    projectId={projectId}\n                                    title={bookmark.title}\n                                />\n                            ))}\n                        </div>\n                    </div>\n                )}\n\n                {/* Recent Changelogs */}\n                <div className=\"py-2 px-3 mt-2\">\n                    <div className=\"flex items-center justify-between mb-2\">\n                        <div className=\"flex items-center\">\n                            <History className=\"h-4 w-4 text-primary mr-1.5\"/>\n                            <h3 className=\"text-xs font-semibold\">Recent Updates</h3>\n                        </div>\n                        {changelogCount > 0 && (\n                            <Button\n                                variant=\"ghost\"\n                                size=\"sm\"\n                                asChild\n                                className=\"h-6 text-xs\"\n                            >\n                                <Link href={`/dashboard/projects/${projectId}/changelog`}>\n                                    View all\n                                </Link>\n                            </Button>\n                        )}\n                    </div>\n\n                    <div className=\"space-y-1\">\n                        {isLoadingChangelogs ? (\n                            Array.from({length: 3}).map((_, i) => (\n                                <div key={i} className=\"p-2\">\n                                    <Skeleton className=\"h-5 w-full mb-2\"/>\n                                    <Skeleton className=\"h-3 w-24\"/>\n                                </div>\n                            ))\n                        ) : changelogData?.entries && changelogData.entries.length > 0 ? (\n                            changelogData.entries.map((changelog) => (\n                                <RecentChangelog\n                                    key={changelog.id}\n                                    id={changelog.id}\n                                    projectId={projectId}\n                                    title={changelog.title}\n                                    date={changelog.updatedAt || changelog.createdAt}\n                                    version={changelog.version}\n                                    isPublished={!!changelog.publishedAt}\n                                />\n                            ))\n                        ) : (\n                            <div className=\"py-6 text-center\">\n                                <PenTool className=\"h-8 w-8 text-muted-foreground/40 mx-auto mb-2\"/>\n                                <p className=\"text-sm text-muted-foreground\">No changelogs yet</p>\n                                <Button\n                                    variant=\"link\"\n                                    asChild\n                                    className=\"mt-2 h-auto p-0\"\n                                >\n                                    <Link href={`/dashboard/projects/${projectId}/changelog/new`}>\n                                        Create your first changelog\n                                    </Link>\n                                </Button>\n                            </div>\n                        )}\n                    </div>\n                </div>\n\n                {/* Project Stats */}\n                {changelogCount > 0 && (\n                    <div className=\"py-2 px-3 mt-2\">\n                        <div className=\"flex items-center mb-2\">\n                            <UserSquare2 className=\"h-4 w-4 text-primary mr-1.5\"/>\n                            <h3 className=\"text-xs font-semibold\">Project Stats</h3>\n                        </div>\n\n                        <div className=\"space-y-1 p-2 bg-muted/40 rounded-md\">\n                            <div className=\"flex items-center justify-between text-xs\">\n                                <span className=\"text-muted-foreground\">Published entries</span>\n                                <span className=\"font-medium\">\n                                    {isLoadingChangelogs ? (\n                                        <Skeleton className=\"h-3 w-8 inline-block\"/>\n                                    ) : publishedCount}\n                                </span>\n                            </div>\n                            <div className=\"flex items-center justify-between text-xs\">\n                                <span className=\"text-muted-foreground\">Draft entries</span>\n                                <span className=\"font-medium\">\n                                    {isLoadingChangelogs ? (\n                                        <Skeleton className=\"h-3 w-8 inline-block\"/>\n                                    ) : draftCount}\n                                </span>\n                            </div>\n                            <div className=\"flex items-center justify-between text-xs\">\n                                <span className=\"text-muted-foreground\">Last updated</span>\n                                <span className=\"font-medium\">\n                                    {isLoadingChangelogs || !changelogData?.entries?.length ? (\n                                        <Skeleton className=\"h-3 w-16 inline-block\"/>\n                                    ) : (\n                                        formatDistanceToNow(new Date(changelogData.entries[0].updatedAt || changelogData.entries[0].createdAt)) + ' ago'\n                                    )}\n                                </span>\n                            </div>\n                        </div>\n                    </div>\n                )}\n            </ScrollArea>\n\n            {/* Footer */}\n            <div className=\"p-3 border-t flex items-center justify-between\">\n                <Button variant=\"outline\" className=\"justify-start text-xs h-8\" asChild>\n                    <Link href=\"/dashboard/projects\">\n                        <ChevronLeft className=\"h-3.5 w-3.5 mr-1\"/>\n                        All Projects\n                    </Link>\n                </Button>\n\n                {isPublic && (\n                    <TooltipProvider>\n                        <Tooltip>\n                            <TooltipTrigger asChild>\n                                <Button variant=\"ghost\" size=\"icon\" className=\"h-8 w-8\" asChild>\n                                    <Link href={rssUrl} target=\"_blank\" rel=\"noopener noreferrer\">\n                                        <Rss className=\"h-4 w-4 text-orange-500\"/>\n                                    </Link>\n                                </Button>\n                            </TooltipTrigger>\n                            <TooltipContent>\n                                <p className=\"text-xs\">RSS Feed</p>\n                            </TooltipContent>\n                        </Tooltip>\n                    </TooltipProvider>\n                )}\n            </div>\n        </div>\n    )\n}"
  },
  {
    "path": "components/project/RecentChangelogItem.tsx",
    "content": "import Link from 'next/link'\nimport { formatDistanceToNow } from 'date-fns'\nimport { Clock, FileText } from 'lucide-react'\n\ninterface RecentChangelogItemProps {\n    id: string\n    projectId: string\n    title: string\n    updatedAt: string\n}\n\nexport function RecentChangelogItem({\n                                        id,\n                                        projectId,\n                                        title,\n                                        updatedAt\n                                    }: RecentChangelogItemProps) {\n    return (\n        <Link\n            href={`/dashboard/projects/${projectId}/changelog/${id}`}\n            className=\"flex items-center gap-2 p-2 hover:bg-accent/50 rounded-md text-sm transition-colors\"\n        >\n            <div className=\"flex-shrink-0 h-8 w-8 bg-primary/10 rounded-md flex items-center justify-center\">\n                <FileText className=\"h-4 w-4 text-primary\" />\n            </div>\n            <div className=\"min-w-0 flex-1\">\n                <p className=\"truncate font-medium\">{title}</p>\n                <div className=\"flex items-center text-xs text-muted-foreground mt-1\">\n                    <Clock className=\"h-3 w-3 mr-1\" />\n                    <span>{formatDistanceToNow(new Date(updatedAt))} ago</span>\n                </div>\n            </div>\n        </Link>\n    )\n}"
  },
  {
    "path": "components/project/catch-up/CatchUpEntry.tsx",
    "content": "'use client';\n\nimport { Card, CardContent } from '@/components/ui/card';\nimport { Badge } from '@/components/ui/badge';\nimport { Calendar, GitBranch } from 'lucide-react';\nimport { formatDistanceToNow, format } from 'date-fns';\nimport type { CatchUpEntry as CatchUpEntryType } from '@/lib/types/projects/catch-up/types';\n\ninterface CatchUpEntryProps {\n    entry: CatchUpEntryType;\n}\n\nexport function CatchUpEntry({ entry }: CatchUpEntryProps) {\n    const publishedDate = entry.publishedAt ? new Date(entry.publishedAt) : null;\n\n    // Truncate content for summary view\n    const truncateContent = (content: string, maxLength: number = 200): string => {\n        if (content.length <= maxLength) return content;\n\n        // Try to break at word boundary\n        const truncated = content.substring(0, maxLength);\n        const lastSpace = truncated.lastIndexOf(' ');\n\n        if (lastSpace > maxLength * 0.8) {\n            return truncated.substring(0, lastSpace) + '...';\n        }\n\n        return truncated + '...';\n    };\n\n    // Clean markdown from content for display\n    const cleanContent = entry.content\n        .replace(/#{1,6}\\s+/g, '') // Remove headers\n        .replace(/\\*\\*(.*?)\\*\\*/g, '$1') // Remove bold\n        .replace(/\\*(.*?)\\*/g, '$1') // Remove italic\n        .replace(/`(.*?)`/g, '$1') // Remove inline code\n        .replace(/\\[(.*?)\\]\\(.*?\\)/g, '$1') // Remove links, keep text\n        .replace(/\\n\\s*\\n/g, '\\n') // Remove double line breaks\n        .replace(/^\\s*[-*+]\\s+/gm, '• ') // Convert list items to bullets\n        .trim();\n\n    return (\n        <Card className=\"transition-all hover:shadow-md\">\n            <CardContent className=\"pt-4\">\n                <div className=\"space-y-3\">\n                    {/* Header with version and date */}\n                    <div className=\"flex items-start justify-between gap-4\">\n                        <div className=\"flex-1\">\n                            <h4 className=\"font-semibold text-lg leading-tight\">\n                                {entry.title}\n                            </h4>\n                            <div className=\"flex items-center gap-3 mt-2 text-sm text-muted-foreground\">\n                                {entry.version && (\n                                    <div className=\"flex items-center gap-1\">\n                                        <GitBranch className=\"h-3 w-3\" />\n                                        <span className=\"font-mono\">{entry.version}</span>\n                                    </div>\n                                )}\n                                {publishedDate && (\n                                    <div className=\"flex items-center gap-1\">\n                                        <Calendar className=\"h-3 w-3\" />\n                                        <span>\n                      {formatDistanceToNow(publishedDate, { addSuffix: true })}\n                    </span>\n                                    </div>\n                                )}\n                            </div>\n                        </div>\n                        {publishedDate && (\n                            <div className=\"text-xs text-muted-foreground font-mono\">\n                                {format(publishedDate, 'MMM d, yyyy')}\n                            </div>\n                        )}\n                    </div>\n\n                    {/* Tags */}\n                    {entry.tags && entry.tags.length > 0 && (\n                        <div className=\"flex flex-wrap gap-1\">\n                            {entry.tags.map((tag) => (\n                                <Badge\n                                    key={tag.id}\n                                    variant=\"secondary\"\n                                    className=\"text-xs\"\n                                    style={\n                                        tag.color\n                                            ? {\n                                                backgroundColor: `${tag.color}15`,\n                                                borderColor: `${tag.color}40`,\n                                                color: tag.color,\n                                            }\n                                            : undefined\n                                    }\n                                >\n                                    {tag.name}\n                                </Badge>\n                            ))}\n                        </div>\n                    )}\n\n                    {/* Content preview */}\n                    {cleanContent && (\n                        <div className=\"prose prose-sm max-w-none\">\n                            <p className=\"text-muted-foreground leading-relaxed m-0\">\n                                {truncateContent(cleanContent)}\n                            </p>\n                        </div>\n                    )}\n                </div>\n            </CardContent>\n        </Card>\n    );\n}"
  },
  {
    "path": "components/project/catch-up/CatchUpView.tsx",
    "content": "'use client';\n\nimport {useState} from 'react';\nimport {useQuery} from '@tanstack/react-query';\nimport {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@/components/ui/card';\nimport {Button} from '@/components/ui/button';\nimport {Badge} from '@/components/ui/badge';\nimport {Skeleton} from '@/components/ui/skeleton';\nimport {Copy, Clock, Tag, TrendingUp, Bug, Zap, GitBranch, Calendar, Sparkles, Bot} from 'lucide-react';\nimport {useToast} from '@/hooks/use-toast';\nimport {SinceSelector} from './SinceSelector';\nimport type {CatchUpResponse} from '@/lib/types/projects/catch-up/types';\nimport {formatDistanceToNow, format} from 'date-fns';\nimport Link from 'next/link';\n\ninterface CatchUpViewProps {\n    projectId: string;\n}\n\ninterface TimelineEntryProps {\n    entry: CatchUpResponse['entries'][0];\n    isLast: boolean;\n}\n\nfunction TimelineEntry({entry, isLast}: TimelineEntryProps) {\n    const publishedDate = entry.publishedAt ? new Date(entry.publishedAt) : null;\n    const isPublished = !!entry.publishedAt;\n\n    return (\n        <div className=\"relative\">\n            {/* Timeline line */}\n            {!isLast && (\n                <div className=\"absolute left-6 top-16 bottom-0 w-px bg-gradient-to-b from-border to-transparent\"/>\n            )}\n\n            {/* Timeline dot */}\n            <div className=\"relative flex items-start gap-6\">\n                <div className={`\n          relative z-10 flex h-12 w-12 items-center justify-center rounded-full border-2 shadow-sm\n          ${isPublished\n                    ? 'bg-green-100 border-green-300 dark:bg-green-900/20 dark:border-green-600'\n                    : 'bg-yellow-100 border-yellow-300 dark:bg-yellow-900/20 dark:border-yellow-600'\n                }\n        `}>\n                    {entry.version ? (\n                        <GitBranch\n                            className={`h-5 w-5 ${isPublished ? 'text-green-600 dark:text-green-400' : 'text-yellow-600 dark:text-yellow-400'}`}/>\n                    ) : (\n                        <Clock\n                            className={`h-5 w-5 ${isPublished ? 'text-green-600 dark:text-green-400' : 'text-yellow-600 dark:text-yellow-400'}`}/>\n                    )}\n\n                    {/* Pulse animation for recent entries */}\n                    {publishedDate && Date.now() - publishedDate.getTime() < 7 * 24 * 60 * 60 * 1000 && (\n                        <div\n                            className={`absolute inset-0 rounded-full animate-ping ${isPublished ? 'bg-green-400' : 'bg-yellow-400'} opacity-20`}/>\n                    )}\n                </div>\n\n                {/* Content */}\n                <div className=\"flex-1 pb-8\">\n                    <Card className=\"transition-all hover:shadow-md border-l-4 border-l-primary/20\">\n                        <CardContent className=\"p-6\">\n                            {/* Header */}\n                            <div className=\"flex items-start justify-between mb-4\">\n                                <div className=\"flex-1\">\n                                    <div className=\"flex items-center gap-2 mb-2\">\n                                        <Badge variant={isPublished ? \"default\" : \"secondary\"} className=\"text-xs\">\n                                            {isPublished ? 'Published' : 'Draft'}\n                                        </Badge>\n                                        {entry.version && (\n                                            <Badge variant=\"outline\" className=\"text-xs font-mono\">\n                                                {entry.version}\n                                            </Badge>\n                                        )}\n                                    </div>\n                                    <h3 className=\"font-semibold text-lg leading-tight mb-2\">\n                                        {entry.title}\n                                    </h3>\n                                </div>\n\n                                {publishedDate && (\n                                    <div className=\"text-right text-sm text-muted-foreground\">\n                                        <div className=\"font-medium\">\n                                            {formatDistanceToNow(publishedDate, {addSuffix: true})}\n                                        </div>\n                                        <div className=\"text-xs\">\n                                            {format(publishedDate, 'MMM d, yyyy')}\n                                        </div>\n                                    </div>\n                                )}\n                            </div>\n\n                            {/* Tags */}\n                            {entry.tags && entry.tags.length > 0 && (\n                                <div className=\"flex flex-wrap gap-1 mb-4\">\n                                    {entry.tags.map((tag) => (\n                                        <Badge\n                                            key={tag.id}\n                                            variant=\"secondary\"\n                                            className=\"text-xs\"\n                                            style={\n                                                tag.color\n                                                    ? {\n                                                        backgroundColor: `${tag.color}15`,\n                                                        borderColor: `${tag.color}40`,\n                                                        color: tag.color,\n                                                    }\n                                                    : undefined\n                                            }\n                                        >\n                                            {tag.name}\n                                        </Badge>\n                                    ))}\n                                </div>\n                            )}\n\n                            {/* Content preview */}\n                            {entry.content && (\n                                <div className=\"text-sm text-muted-foreground leading-relaxed\">\n                                    {entry.content.length > 150\n                                        ? `${entry.content.substring(0, 150)}...`\n                                        : entry.content\n                                    }\n                                </div>\n                            )}\n                        </CardContent>\n                    </Card>\n                </div>\n            </div>\n        </div>\n    );\n}\n\nexport function CatchUpView({projectId}: CatchUpViewProps) {\n    const [since, setSince] = useState('auto');\n    const {toast} = useToast();\n\n    const {\n        data,\n        isLoading,\n        error,\n        refetch,\n    } = useQuery<CatchUpResponse>({\n        queryKey: ['catch-up', projectId, since],\n        queryFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}/catch-up?since=${encodeURIComponent(since)}`);\n            if (!response.ok) {\n                throw new Error('Failed to fetch catch-up data');\n            }\n            return response.json();\n        },\n        staleTime: 1000 * 60 * 5, // 5 minutes\n    });\n\n    const handleCopySummary = async () => {\n        if (!data) return;\n\n        const summary = `Changelog Summary\n\nFrom: ${new Date(data.fromDate).toLocaleDateString()}${data.fromVersion ? ` (${data.fromVersion})` : ''}\nTo: ${data.toVersion || 'Latest'}\n\n📊 ${data.totalEntries} total updates\n✨ ${data.summary.features} features\n🐛 ${data.summary.fixes} fixes  \n📝 ${data.summary.other} other changes\n\n${data.entries.map(entry =>\n            `• ${entry.title}${entry.version ? ` (${entry.version})` : ''}`\n        ).join('\\n')}`;\n\n        try {\n            await navigator.clipboard.writeText(summary);\n            toast({\n                title: \"Success\",\n                description: \"Summary copied to clipboard!\",\n            });\n        } catch {\n            toast({\n                title: \"Error\",\n                description: \"Failed to copy summary\",\n                variant: \"destructive\",\n            });\n        }\n    };\n\n    if (error) {\n        return (\n            <Card>\n                <CardHeader>\n                    <CardTitle className=\"text-destructive\">Error</CardTitle>\n                    <CardDescription>\n                        Failed to load catch-up data. Please try again.\n                    </CardDescription>\n                </CardHeader>\n                <CardContent>\n                    <Button onClick={() => refetch()} variant=\"outline\">\n                        Try Again\n                    </Button>\n                </CardContent>\n            </Card>\n        );\n    }\n\n    return (\n        <div className=\"space-y-6\">\n            {/* Header with Enhanced Feature Promotion */}\n            <Card\n                className=\"border-2 border-primary/20 bg-gradient-to-br from-primary/5 via-background to-purple/5 relative overflow-hidden\">\n                <div\n                    className=\"absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl from-primary/10 to-transparent rounded-full -translate-y-16 translate-x-16\"/>\n                <CardHeader className=\"relative\">\n                    <div className=\"flex items-start justify-between\">\n                        <div className=\"space-y-3\">\n                            <div className=\"flex items-center gap-3\">\n                                <div className=\"h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center\">\n                                    <Clock className=\"h-5 w-5 text-primary\"/>\n                                </div>\n                                <div>\n                                    <CardTitle className=\"text-2xl\">Catch-Up</CardTitle>\n                                    <CardDescription className=\"text-base mt-1\">\n                                        Here&apos;s what happened while you were away\n                                    </CardDescription>\n                                </div>\n                            </div>\n\n                            <div className=\"flex items-center gap-4 pl-13\">\n                                <Button asChild className=\"gap-2 shadow-lg\">\n                                    <Link href={`/dashboard/projects/${projectId}/catch-up`}>\n                                        <Bot className=\"h-4 w-4\"/>\n                                        Enhanced Catch-Up\n                                        <Badge variant=\"secondary\" className=\"gap-1 text-xs ml-1 bg-background/80\">\n                                            <Sparkles className=\"h-3 w-3\"/>\n                                            AI\n                                        </Badge>\n                                    </Link>\n                                </Button>\n\n                                <div className=\"text-xs text-muted-foreground\">\n                                    Get smart summaries and insights\n                                    <br/>\n                                    <span className=\"font-medium\">Note:</span> Requires AI features enabled\n                                </div>\n                            </div>\n                        </div>\n\n                        <Button\n                            onClick={() => refetch()}\n                            variant=\"outline\"\n                            size=\"sm\"\n                            disabled={isLoading}\n                        >\n                            Refresh\n                        </Button>\n                    </div>\n                </CardHeader>\n                <CardContent className=\"space-y-4\">\n                    <SinceSelector value={since} onChange={setSince} projectId={projectId}/>\n                </CardContent>\n            </Card>\n\n            {/* Summary Stats */}\n            {isLoading ? (\n                <Card>\n                    <CardContent className=\"pt-6\">\n                        <div className=\"space-y-4\">\n                            <Skeleton className=\"h-4 w-48\"/>\n                            <div className=\"flex gap-2\">\n                                <Skeleton className=\"h-6 w-20\"/>\n                                <Skeleton className=\"h-6 w-20\"/>\n                                <Skeleton className=\"h-6 w-20\"/>\n                            </div>\n                        </div>\n                    </CardContent>\n                </Card>\n            ) : data ? (\n                <Card className=\"bg-gradient-to-r from-muted/30 to-muted/10 border-muted\">\n                    <CardContent className=\"pt-6\">\n                        <div className=\"space-y-4\">\n                            <div className=\"text-center\">\n                                {data.totalEntries === 0 ? (\n                                    <div className=\"py-4\">\n                                        <h3 className=\"text-lg font-semibold text-muted-foreground mb-2\">\n                                            All caught up! 🎉\n                                        </h3>\n                                        <p className=\"text-sm text-muted-foreground\">\n                                            No new changes\n                                            since {formatDistanceToNow(new Date(data.fromDate), {addSuffix: true})}\n                                        </p>\n                                    </div>\n                                ) : (\n                                    <div className=\"space-y-3\">\n                                        <h3 className=\"text-lg font-semibold\">\n                                            📖 {data.totalEntries} update{data.totalEntries !== 1 ? 's' : ''} since{' '}\n                                            {formatDistanceToNow(new Date(data.fromDate), {addSuffix: true})}\n                                            {data.fromVersion && ` (${data.fromVersion})`}\n                                        </h3>\n\n                                        <div className=\"flex justify-center gap-4 flex-wrap\">\n                                            {data.summary.features > 0 && (\n                                                <div\n                                                    className=\"flex items-center gap-2 px-3 py-2 bg-green-100 dark:bg-green-900/20 rounded-full\">\n                                                    <Zap className=\"h-4 w-4 text-green-600 dark:text-green-400\"/>\n                                                    <span\n                                                        className=\"text-sm font-medium text-green-700 dark:text-green-300\">\n                                                        {data.summary.features} feature{data.summary.features !== 1 ? 's' : ''}\n                                                    </span>\n                                                </div>\n                                            )}\n\n                                            {data.summary.fixes > 0 && (\n                                                <div\n                                                    className=\"flex items-center gap-2 px-3 py-2 bg-blue-100 dark:bg-blue-900/20 rounded-full\">\n                                                    <Bug className=\"h-4 w-4 text-blue-600 dark:text-blue-400\"/>\n                                                    <span\n                                                        className=\"text-sm font-medium text-blue-700 dark:text-blue-300\">\n                                                        {data.summary.fixes} fix{data.summary.fixes !== 1 ? 'es' : ''}\n                                                    </span>\n                                                </div>\n                                            )}\n\n                                            {data.summary.other > 0 && (\n                                                <div\n                                                    className=\"flex items-center gap-2 px-3 py-2 bg-purple-100 dark:bg-purple-900/20 rounded-full\">\n                                                    <Tag className=\"h-4 w-4 text-purple-600 dark:text-purple-400\"/>\n                                                    <span\n                                                        className=\"text-sm font-medium text-purple-700 dark:text-purple-300\">\n                                                        {data.summary.other} other\n                                                    </span>\n                                                </div>\n                                            )}\n                                        </div>\n\n                                        <Button\n                                            onClick={handleCopySummary}\n                                            variant=\"outline\"\n                                            size=\"sm\"\n                                            className=\"gap-2 bg-background/80\"\n                                        >\n                                            <Copy className=\"h-4 w-4\"/>\n                                            Copy Summary\n                                        </Button>\n                                    </div>\n                                )}\n                            </div>\n                        </div>\n                    </CardContent>\n                </Card>\n            ) : null}\n\n            {/* Timeline */}\n            {data && data.entries.length > 0 && (\n                <div className=\"space-y-6\">\n                    <div className=\"flex items-center gap-3\">\n                        <TrendingUp className=\"h-5 w-5 text-muted-foreground\"/>\n                        <h3 className=\"text-xl font-semibold\">Timeline</h3>\n                        <div className=\"flex-1 h-px bg-gradient-to-r from-border to-transparent\"/>\n                    </div>\n\n                    <div className=\"space-y-0\">\n                        {data.entries.map((entry, index) => (\n                            <TimelineEntry\n                                key={entry.id}\n                                entry={entry}\n                                isLast={index === data.entries.length - 1}\n                            />\n                        ))}\n                    </div>\n\n                    {/* Timeline end */}\n                    <div className=\"flex items-center justify-center py-6\">\n                        <div className=\"flex items-center gap-3 text-sm text-muted-foreground\">\n                            <div className=\"h-px w-12 bg-gradient-to-r from-transparent to-border\"/>\n                            <Calendar className=\"h-4 w-4\"/>\n                            <span>You&apos;re all caught up!</span>\n                            <div className=\"h-px w-12 bg-gradient-to-l from-transparent to-border\"/>\n                        </div>\n                    </div>\n                </div>\n            )}\n        </div>\n    );\n}"
  },
  {
    "path": "components/project/catch-up/SinceSelector.tsx",
    "content": "'use client';\n\nimport {useQuery} from '@tanstack/react-query';\nimport {\n    Select,\n    SelectContent,\n    SelectItem,\n    SelectTrigger,\n    SelectValue,\n} from '@/components/ui/select';\nimport {Label} from '@/components/ui/label';\nimport {Calendar, Clock, GitBranch, User} from 'lucide-react';\nimport type {SinceOption} from '@/lib/types/projects/catch-up/types';\n\ninterface SinceSelectorProps {\n    value: string;\n    onChange: (value: string) => void;\n    projectId: string;\n}\n\nexport function SinceSelector({value, onChange, projectId}: SinceSelectorProps) {\n\n    // Fetch recent versions for the selector\n    const {data: versions} = useQuery<{ versions: string[] }>({\n        queryKey: ['project-versions', projectId],\n        queryFn: async () => {\n            const response = await fetch(`/api/projects/${projectId}/versions`);\n            if (!response.ok) throw new Error('Failed to fetch versions');\n            return response.json();\n        },\n        staleTime: 1000 * 60 * 10, // 10 minutes\n    });\n\n    const baseOptions: SinceOption[] = [\n        {\n            label: 'My last login',\n            value: 'auto',\n            type: 'auto',\n            description: 'Show changes since I was last here',\n        },\n        {\n            label: 'Last 24 hours',\n            value: '1d',\n            type: 'relative',\n            description: 'Changes in the past day',\n        },\n        {\n            label: 'Last 7 days',\n            value: '7d',\n            type: 'relative',\n            description: 'Changes in the past week',\n        },\n        {\n            label: 'Last 30 days',\n            value: '30d',\n            type: 'relative',\n            description: 'Changes in the past month',\n        },\n    ];\n\n    // Add recent versions to options\n    const versionOptions: SinceOption[] = versions?.versions\n        ?.slice(0, 5) // Only show last 5 versions\n        ?.map(version => ({\n            label: `Since ${version}`,\n            value: version,\n            type: 'version',\n            description: `Changes since version ${version}`,\n        })) || [];\n\n    const allOptions = [\n        ...baseOptions,\n        ...(versionOptions.length > 0 ? versionOptions : []),\n    ];\n\n    const getIcon = (type: SinceOption['type']) => {\n        switch (type) {\n            case 'auto':\n                return <User className=\"h-4 w-4\"/>;\n            case 'relative':\n                return <Clock className=\"h-4 w-4\"/>;\n            case 'version':\n                return <GitBranch className=\"h-4 w-4\"/>;\n            case 'date':\n                return <Calendar className=\"h-4 w-4\"/>;\n            default:\n                return <Clock className=\"h-4 w-4\"/>;\n        }\n    };\n\n    const selectedOption = allOptions.find(option => option.value === value);\n\n    return (\n        <div className=\"space-y-2\">\n            <Label htmlFor=\"since-selector\" className=\"text-sm font-medium\">\n                Show me what&apos;s new since:\n            </Label>\n            <Select value={value} onValueChange={onChange}>\n                <SelectTrigger id=\"since-selector\" className=\"w-full\">\n                    <SelectValue>\n                        <div className=\"flex items-center gap-2\">\n                            {selectedOption && getIcon(selectedOption.type)}\n                            <span>{selectedOption?.label || value}</span>\n                        </div>\n                    </SelectValue>\n                </SelectTrigger>\n                <SelectContent>\n                    {baseOptions.map((option) => (\n                        <SelectItem key={option.value} value={option.value}>\n                            <div className=\"flex items-start gap-2 py-1\">\n                                {getIcon(option.type)}\n                                <div className=\"flex-1\">\n                                    <div className=\"font-medium\">{option.label}</div>\n                                    {option.description && (\n                                        <div className=\"text-xs text-muted-foreground\">\n                                            {option.description}\n                                        </div>\n                                    )}\n                                </div>\n                            </div>\n                        </SelectItem>\n                    ))}\n\n                    {versionOptions.length > 0 && (\n                        <>\n                            <div className=\"px-2 py-1.5\">\n                                <div className=\"text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n                                    Recent Versions\n                                </div>\n                            </div>\n                            {versionOptions.map((option) => (\n                                <SelectItem key={option.value} value={option.value}>\n                                    <div className=\"flex items-start gap-2 py-1\">\n                                        {getIcon(option.type)}\n                                        <div className=\"flex-1\">\n                                            <div className=\"font-medium\">{option.label}</div>\n                                            {option.description && (\n                                                <div className=\"text-xs text-muted-foreground\">\n                                                    {option.description}\n                                                </div>\n                                            )}\n                                        </div>\n                                    </div>\n                                </SelectItem>\n                            ))}\n                        </>\n                    )}\n                </SelectContent>\n            </Select>\n        </div>\n    );\n}"
  },
  {
    "path": "components/project/settings/TagManagement.tsx",
    "content": "// components/settings/TagManagement.tsx\n'use client';\n\nimport React, {useState, useCallback, useEffect} from 'react';\nimport {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';\nimport {motion, AnimatePresence} from 'framer-motion';\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle\n} from '@/components/ui/card';\nimport {Button} from '@/components/ui/button';\nimport {Input} from '@/components/ui/input';\nimport {Label} from '@/components/ui/label';\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogFooter,\n    DialogHeader,\n    DialogTitle\n} from '@/components/ui/dialog';\nimport {\n    AlertDialog,\n    AlertDialogAction,\n    AlertDialogCancel,\n    AlertDialogContent,\n    AlertDialogDescription,\n    AlertDialogFooter,\n    AlertDialogHeader,\n    AlertDialogTitle,\n} from '@/components/ui/alert-dialog';\nimport {Skeleton} from '@/components/ui/skeleton';\nimport {\n    Edit2,\n    Trash2,\n    Save,\n    AlertCircle,\n    Tag as TagIcon,\n    Loader2\n} from 'lucide-react';\nimport {ColorPicker, ColoredTag} from '@/components/changelog/editor/TagColorPicker';\nimport {toast} from '@/hooks/use-toast';\n\ninterface Tag {\n    id: string;\n    name: string;\n    color?: string | null;\n    _count?: {\n        entries: number;\n    };\n}\n\ninterface TagManagementProps {\n    projectId: string;\n}\n\ninterface CreateTagData {\n    name: string;\n    color?: string | null;\n}\n\ninterface UpdateTagData {\n    tagId: string;\n    name?: string;\n    color?: string | null;\n}\n\n// Animation variants\nconst containerVariants = {\n    hidden: {opacity: 0},\n    visible: {\n        opacity: 1,\n        transition: {\n            staggerChildren: 0.1\n        }\n    }\n};\n\nconst itemVariants = {\n    hidden: {opacity: 0, y: 20},\n    visible: {\n        opacity: 1,\n        y: 0,\n        transition: {duration: 0.3}\n    }\n};\n\nexport default function TagManagement({projectId}: TagManagementProps) {\n    const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);\n    const [editingTag, setEditingTag] = useState<Tag | null>(null);\n    const [deletingTag, setDeletingTag] = useState<Tag | null>(null);\n    const [newTagData, setNewTagData] = useState<CreateTagData>({name: '', color: null});\n    const [editTagData, setEditTagData] = useState<UpdateTagData>({tagId: '', name: '', color: null});\n\n    const queryClient = useQueryClient();\n\n    // Fetch tags for the project\n    const {data: tags, isLoading, error} = useQuery({\n        queryKey: ['project-tags', projectId],\n        queryFn: async (): Promise<Tag[]> => {\n            const response = await fetch(`/api/projects/${projectId}/changelog/tags?includeUsage=true`);\n            if (!response.ok) {\n                throw new Error('Failed to fetch tags');\n            }\n            const data = await response.json();\n            return data.tags || [];\n        }\n    });\n\n    // Create tag mutation\n    const createTagMutation = useMutation({\n        mutationFn: async (data: CreateTagData): Promise<Tag> => {\n            const response = await fetch(`/api/projects/${projectId}/changelog/tags`, {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify(data)\n            });\n            if (!response.ok) {\n                const error = await response.json();\n                throw new Error(error.error || 'Failed to create tag');\n            }\n            return response.json();\n        },\n        onSuccess: () => {\n            queryClient.invalidateQueries({queryKey: ['project-tags', projectId]});\n            setIsCreateDialogOpen(false);\n            setNewTagData({name: '', color: null});\n            toast({\n                title: 'Success',\n                description: 'Tag created successfully',\n            });\n        },\n        onError: (error: Error) => {\n            toast({\n                title: 'Error',\n                description: error.message,\n                variant: 'destructive',\n            });\n        }\n    });\n\n    // Update tag mutation\n    const updateTagMutation = useMutation({\n        mutationFn: async (data: UpdateTagData): Promise<Tag> => {\n            const response = await fetch(`/api/projects/${projectId}/changelog/tags`, {\n                method: 'PATCH',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify(data)\n            });\n            if (!response.ok) {\n                const error = await response.json();\n                throw new Error(error.error || 'Failed to update tag');\n            }\n            return response.json();\n        },\n        onSuccess: () => {\n            queryClient.invalidateQueries({queryKey: ['project-tags', projectId]});\n            setEditingTag(null);\n            setEditTagData({tagId: '', name: '', color: null});\n            toast({\n                title: 'Success',\n                description: 'Tag updated successfully',\n            });\n        },\n        onError: (error: Error) => {\n            toast({\n                title: 'Error',\n                description: error.message,\n                variant: 'destructive',\n            });\n        }\n    });\n\n    // Delete tag mutation\n    const deleteTagMutation = useMutation({\n        mutationFn: async (tagId: string): Promise<void> => {\n            const response = await fetch(`/api/projects/${projectId}/changelog/tags/${tagId}`, {\n                method: 'DELETE'\n            });\n            if (!response.ok) {\n                const error = await response.json();\n                throw new Error(error.error || 'Failed to delete tag');\n            }\n        },\n        onSuccess: () => {\n            queryClient.invalidateQueries({queryKey: ['project-tags', projectId]});\n            setDeletingTag(null);\n            toast({\n                title: 'Success',\n                description: 'Tag deleted successfully',\n            });\n        },\n        onError: (error: Error) => {\n            toast({\n                title: 'Error',\n                description: error.message,\n                variant: 'destructive',\n            });\n        }\n    });\n\n    // Handlers\n    const handleCreateTag = useCallback(() => {\n        if (!newTagData.name.trim()) return;\n        createTagMutation.mutate(newTagData);\n    }, [newTagData, createTagMutation]);\n\n    const handleEditTag = useCallback((tag: Tag) => {\n        setEditingTag(tag);\n        setEditTagData({\n            tagId: tag.id,\n            name: tag.name,\n            color: tag.color\n        });\n    }, []);\n\n    const handleUpdateTag = useCallback(() => {\n        if (!editTagData.name?.trim()) return;\n        updateTagMutation.mutate(editTagData);\n    }, [editTagData, updateTagMutation]);\n\n    const handleDeleteTag = useCallback((tag: Tag) => {\n        setDeletingTag(tag);\n    }, []);\n\n    const confirmDeleteTag = useCallback(() => {\n        if (!deletingTag) return;\n        deleteTagMutation.mutate(deletingTag.id);\n    }, [deletingTag, deleteTagMutation]);\n\n    // Reset form data when dialogs close\n    useEffect(() => {\n        if (!isCreateDialogOpen) {\n            setNewTagData({name: '', color: null});\n        }\n    }, [isCreateDialogOpen]);\n\n    useEffect(() => {\n        if (!editingTag) {\n            setEditTagData({tagId: '', name: '', color: null});\n        }\n    }, [editingTag]);\n\n    if (error) {\n        return (\n            <Card>\n                <CardContent className=\"pt-6\">\n                    <div className=\"flex items-center justify-center h-32 text-destructive\">\n                        <AlertCircle className=\"h-6 w-6 mr-2\"/>\n                        Failed to load tags: {error instanceof Error ? error.message : 'Unknown error'}\n                    </div>\n                </CardContent>\n            </Card>\n        );\n    }\n\n    return (\n        <div className=\"space-y-6\">\n            {/* Header */}\n            <Card>\n                <CardHeader>\n                    <div className=\"flex items-center justify-between\">\n                        <div>\n                            <CardTitle className=\"flex items-center gap-2\">\n                                <TagIcon className=\"h-5 w-5\"/>\n                                Tag Management\n                            </CardTitle>\n                            <CardDescription>\n                                Manage tags for your changelog entries. Tags help organize and categorize\n                                your releases.\n                            </CardDescription>\n                        </div>\n                        {/*<Button*/}\n                        {/*    onClick={() => setIsCreateDialogOpen(true)}*/}\n                        {/*    className=\"shrink-0\"*/}\n                        {/*>*/}\n                        {/*    <Plus className=\"h-4 w-4 mr-2\"/>*/}\n                        {/*    Create Tag*/}\n                        {/*</Button>*/}\n                    </div>\n                </CardHeader>\n\n                <CardContent>\n                    {isLoading ? (\n                        <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4\">\n                            {Array.from({length: 6}).map((_, i) => (\n                                <div key={i} className=\"border rounded-lg p-4 space-y-3\">\n                                    <div className=\"flex items-center justify-between\">\n                                        <Skeleton className=\"h-6 w-24\"/>\n                                        <div className=\"flex gap-1\">\n                                            <Skeleton className=\"h-8 w-8\"/>\n                                            <Skeleton className=\"h-8 w-8\"/>\n                                        </div>\n                                    </div>\n                                    <Skeleton className=\"h-4 w-32\"/>\n                                    <Skeleton className=\"h-5 w-16\"/>\n                                </div>\n                            ))}\n                        </div>\n                    ) : !tags || tags.length === 0 ? (\n                        <div className=\"text-center py-12 text-muted-foreground\">\n                            <TagIcon className=\"h-12 w-12 mx-auto mb-4 opacity-50\"/>\n                            <h3 className=\"text-lg font-medium mb-2\">No tags yet</h3>\n                            <p className=\"mb-4\">Create your first tag to help organize your changelog entries.</p>\n                        </div>\n                    ) : (\n                        <motion.div\n                            className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4\"\n                            variants={containerVariants}\n                            initial=\"hidden\"\n                            animate=\"visible\"\n                        >\n                            <AnimatePresence>\n                                {tags.map((tag) => (\n                                    <motion.div\n                                        key={tag.id}\n                                        variants={itemVariants}\n                                        layout\n                                        className=\"border rounded-lg p-4 space-y-3 hover:border-primary/50 transition-colors\"\n                                    >\n                                        <div className=\"flex items-center justify-between\">\n                                            <div className=\"flex items-center gap-2\">\n                                                {tag.color && (\n                                                    <div\n                                                        className=\"h-4 w-4 rounded-full border border-gray-300\"\n                                                        style={{backgroundColor: tag.color}}\n                                                    />\n                                                )}\n                                                <h4 className=\"font-medium truncate\">{tag.name}</h4>\n                                            </div>\n                                            <div className=\"flex gap-1\">\n                                                <Button\n                                                    variant=\"ghost\"\n                                                    size=\"sm\"\n                                                    onClick={() => handleEditTag(tag)}\n                                                    className=\"h-8 w-8 p-0\"\n                                                >\n                                                    <Edit2 className=\"h-4 w-4\"/>\n                                                </Button>\n                                                <Button\n                                                    variant=\"ghost\"\n                                                    size=\"sm\"\n                                                    onClick={() => handleDeleteTag(tag)}\n                                                    className=\"h-8 w-8 p-0 text-destructive hover:text-destructive\"\n                                                >\n                                                    <Trash2 className=\"h-4 w-4\"/>\n                                                </Button>\n                                            </div>\n                                        </div>\n\n                                        <div className=\"text-sm text-muted-foreground\">\n                                            Used\n                                            in {tag._count?.entries || 0} entr{tag._count?.entries === 1 ? 'y' : 'ies'}\n                                        </div>\n\n                                        <ColoredTag\n                                            name={tag.name}\n                                            color={tag.color}\n                                            size=\"sm\"\n                                            className=\"w-fit\"\n                                        />\n                                    </motion.div>\n                                ))}\n                            </AnimatePresence>\n                        </motion.div>\n                    )}\n                </CardContent>\n            </Card>\n\n            {/* Create Tag Dialog */}\n            <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>\n                <DialogContent>\n                    <DialogHeader>\n                        <DialogTitle>Create New Tag</DialogTitle>\n                        <DialogDescription>\n                            Add a new tag to organize your changelog entries. You can assign a color to make it visually\n                            distinct.\n                        </DialogDescription>\n                    </DialogHeader>\n\n                    <div className=\"space-y-4\">\n                        <div className=\"space-y-2\">\n                            <Label htmlFor=\"tag-name\">Tag Name</Label>\n                            <Input\n                                id=\"tag-name\"\n                                value={newTagData.name}\n                                onChange={(e) => setNewTagData(prev => ({...prev, name: e.target.value}))}\n                                placeholder=\"Enter tag name...\"\n                                onKeyDown={(e) => e.key === 'Enter' && handleCreateTag()}\n                            />\n                        </div>\n\n                        <div className=\"space-y-2\">\n                            <Label>Tag Color (Optional)</Label>\n                            <ColorPicker\n                                value={newTagData.color}\n                                onChange={(color) => setNewTagData(prev => ({...prev, color}))}\n                                placeholder=\"Choose a color for this tag\"\n                            />\n                        </div>\n\n                        {newTagData.name && (\n                            <div className=\"space-y-2\">\n                                <Label>Preview</Label>\n                                <div>\n                                    <ColoredTag\n                                        name={newTagData.name}\n                                        color={newTagData.color}\n                                        size=\"default\"\n                                    />\n                                </div>\n                            </div>\n                        )}\n                    </div>\n\n                    <DialogFooter>\n                        <Button\n                            variant=\"outline\"\n                            onClick={() => setIsCreateDialogOpen(false)}\n                            disabled={createTagMutation.isPending}\n                        >\n                            Cancel\n                        </Button>\n                        <Button\n                            onClick={handleCreateTag}\n                            disabled={!newTagData.name.trim() || createTagMutation.isPending}\n                        >\n                            {createTagMutation.isPending ? (\n                                <Loader2 className=\"h-4 w-4 mr-2 animate-spin\"/>\n                            ) : (\n                                <Save className=\"h-4 w-4 mr-2\"/>\n                            )}\n                            Create Tag\n                        </Button>\n                    </DialogFooter>\n                </DialogContent>\n            </Dialog>\n\n            {/* Edit Tag Dialog */}\n            <Dialog open={!!editingTag} onOpenChange={(open) => !open && setEditingTag(null)}>\n                <DialogContent>\n                    <DialogHeader>\n                        <DialogTitle>Edit Tag</DialogTitle>\n                        <DialogDescription>\n                            Update the tag name and color. Changes will apply to all existing entries using this tag.\n                        </DialogDescription>\n                    </DialogHeader>\n\n                    <div className=\"space-y-4\">\n                        <div className=\"space-y-2\">\n                            <Label htmlFor=\"edit-tag-name\">Tag Name</Label>\n                            <Input\n                                id=\"edit-tag-name\"\n                                value={editTagData.name || ''}\n                                onChange={(e) => setEditTagData(prev => ({...prev, name: e.target.value}))}\n                                placeholder=\"Enter tag name...\"\n                                onKeyDown={(e) => e.key === 'Enter' && handleUpdateTag()}\n                            />\n                        </div>\n\n                        <div className=\"space-y-2\">\n                            <Label>Tag Color</Label>\n                            <ColorPicker\n                                value={editTagData.color}\n                                onChange={(color) => setEditTagData(prev => ({...prev, color}))}\n                                placeholder=\"Choose a color for this tag\"\n                            />\n                        </div>\n\n                        {editTagData.name && (\n                            <div className=\"space-y-2\">\n                                <Label>Preview</Label>\n                                <div>\n                                    <ColoredTag\n                                        name={editTagData.name}\n                                        color={editTagData.color}\n                                        size=\"default\"\n                                    />\n                                </div>\n                            </div>\n                        )}\n                    </div>\n\n                    <DialogFooter>\n                        <Button\n                            variant=\"outline\"\n                            onClick={() => setEditingTag(null)}\n                            disabled={updateTagMutation.isPending}\n                        >\n                            Cancel\n                        </Button>\n                        <Button\n                            onClick={handleUpdateTag}\n                            disabled={!editTagData.name?.trim() || updateTagMutation.isPending}\n                        >\n                            {updateTagMutation.isPending ? (\n                                <Loader2 className=\"h-4 w-4 mr-2 animate-spin\"/>\n                            ) : (\n                                <Save className=\"h-4 w-4 mr-2\"/>\n                            )}\n                            Update Tag\n                        </Button>\n                    </DialogFooter>\n                </DialogContent>\n            </Dialog>\n\n            {/* Delete Confirmation Dialog */}\n            <AlertDialog open={!!deletingTag} onOpenChange={(open) => !open && setDeletingTag(null)}>\n                <AlertDialogContent>\n                    <AlertDialogHeader>\n                        <AlertDialogTitle>Delete Tag</AlertDialogTitle>\n                        <AlertDialogDescription>\n                            Are you sure you want to delete the tag &ldquo;{deletingTag?.name}&rdquo;?\n                            This will remove it from all {deletingTag?._count?.entries || 0} changelog entries that use\n                            it.\n                            This action cannot be undone.\n                        </AlertDialogDescription>\n                    </AlertDialogHeader>\n                    <AlertDialogFooter>\n                        <AlertDialogCancel disabled={deleteTagMutation.isPending}>\n                            Cancel\n                        </AlertDialogCancel>\n                        <AlertDialogAction\n                            onClick={confirmDeleteTag}\n                            disabled={deleteTagMutation.isPending}\n                            className=\"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\n                        >\n                            {deleteTagMutation.isPending ? (\n                                <Loader2 className=\"h-4 w-4 mr-2 animate-spin\"/>\n                            ) : (\n                                <Trash2 className=\"h-4 w-4 mr-2\"/>\n                            )}\n                            Delete Tag\n                        </AlertDialogAction>\n                    </AlertDialogFooter>\n                </AlertDialogContent>\n            </AlertDialog>\n        </div>\n    );\n}"
  },
  {
    "path": "components/projects/importing/ChangelogImportModal.tsx",
    "content": "'use client';\n\nimport {useState, useCallback} from 'react';\nimport {Upload, FileText, CheckCircle, Loader2, Code} from 'lucide-react';\nimport {motion, AnimatePresence} from 'framer-motion';\n\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogHeader,\n    DialogTitle,\n} from '@/components/ui/dialog';\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card';\nimport {Button} from '@/components/ui/button';\nimport {Label} from '@/components/ui/label';\nimport {Textarea} from '@/components/ui/textarea';\nimport {Checkbox} from '@/components/ui/checkbox';\nimport {RadioGroup, RadioGroupItem} from '@/components/ui/radio-group';\nimport {Badge} from '@/components/ui/badge';\nimport {Alert, AlertDescription} from '@/components/ui/alert';\nimport {Progress} from '@/components/ui/progress';\nimport {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs';\nimport {useToast} from '@/hooks/use-toast';\n\nimport {\n    ImportPreview,\n    ImportOptions,\n    ImportResult,\n    ParsedChangelog,\n    ValidatedEntry\n} from '@/lib/types/projects/importing';\n\nimport {CannyImportStep} from './integrations/CannyImportStep';\n\ninterface ChangelogImportModalProps {\n    open: boolean;\n    onOpenChange: (open: boolean) => void;\n    projectId: string;\n    onImportComplete: (result: ImportResult) => void;\n}\n\ntype ImportStep = 'source' | 'preview' | 'configure' | 'importing';\ntype ImportSource = 'markdown' | 'canny';\n\nexport function ChangelogImportModal({\n                                         open,\n                                         onOpenChange,\n                                         projectId,\n                                         onImportComplete\n                                     }: ChangelogImportModalProps) {\n    const {toast} = useToast();\n\n    // State management\n    const [currentStep, setCurrentStep] = useState<ImportStep>('source');\n    const [importSource, setImportSource] = useState<ImportSource>('markdown');\n\n    // Markdown import state\n    const [markdownContent, setMarkdownContent] = useState('');\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    const [parsedChangelog, setParsedChangelog] = useState<ParsedChangelog | null>(null);\n\n    // Common state\n    const [preview, setPreview] = useState<ImportPreview | null>(null);\n    const [validatedEntries, setValidatedEntries] = useState<ValidatedEntry[]>([]);\n    const [importOptions, setImportOptions] = useState<ImportOptions>({\n        strategy: 'merge',\n        preserveExistingEntries: true,\n        autoGenerateVersions: false,\n        defaultTags: [],\n        publishImportedEntries: false,\n        dateHandling: 'preserve',\n        conflictResolution: 'skip'\n    });\n    const [isProcessing, setIsProcessing] = useState(false);\n    const [importProgress, setImportProgress] = useState(0);\n    const [importStatus, setImportStatus] = useState('');\n\n    // File upload handler for markdown\n    const handleFileUpload = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {\n        const file = event.target.files?.[0];\n        if (!file) return;\n\n        if (!file.name.endsWith('.md') && !file.name.endsWith('.txt')) {\n            toast({\n                title: 'Invalid file type',\n                description: 'Please upload a Markdown (.md) or text (.txt) file.',\n                variant: 'destructive'\n            });\n            return;\n        }\n\n        try {\n            const content = await file.text();\n            setMarkdownContent(content);\n            await processMarkdown(content);\n        } catch {\n            toast({\n                title: 'Error reading file',\n                description: 'Failed to read the uploaded file.',\n                variant: 'destructive'\n            });\n        }\n    }, [toast]);\n\n    // Process markdown content\n    const processMarkdown = async (content: string) => {\n        setIsProcessing(true);\n        setImportStatus('Analyzing content structure...');\n\n        try {\n            // Simulate processing time for user feedback\n            await new Promise(resolve => setTimeout(resolve, 800));\n\n            setImportStatus('Parsing changelog entries...');\n            await new Promise(resolve => setTimeout(resolve, 600));\n\n            const response = await fetch('/api/projects/import/parse', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({content, projectId})\n            });\n\n            if (!response.ok) throw new Error('Failed to parse markdown');\n\n            setImportStatus('Validating entries...');\n            await new Promise(resolve => setTimeout(resolve, 400));\n\n            const data = await response.json();\n            setParsedChangelog(data.parsed);\n            setPreview(data.preview);\n            setValidatedEntries(data.validatedEntries);\n            setCurrentStep('preview');\n\n        } catch {\n            toast({\n                title: 'Parsing failed',\n                description: 'Failed to parse the changelog content.',\n                variant: 'destructive'\n            });\n        } finally {\n            setIsProcessing(false);\n            setImportStatus('');\n        }\n    };\n\n    // Handle manual text input for markdown\n    const handleTextSubmit = () => {\n        if (!markdownContent.trim()) {\n            toast({\n                title: 'No content',\n                description: 'Please enter some changelog content.',\n                variant: 'destructive'\n            });\n            return;\n        }\n        processMarkdown(markdownContent);\n    };\n\n    // Handle Canny import\n    const handleCannyImport = async (entries: ValidatedEntry[]) => {\n        try {\n            // Simulate validation for Canny entries\n            setIsProcessing(true);\n            setImportStatus('Processing Canny entries...');\n            await new Promise(resolve => setTimeout(resolve, 500));\n\n            // Create mock preview for Canny entries\n            const cannyPreview: ImportPreview = {\n                totalEntries: entries.length,\n                validEntries: entries.length,\n                invalidEntries: 0,\n                duplicateVersions: [],\n                missingTitles: 0,\n                missingContent: 0,\n                suggestedMappings: {versions: {}, tags: {}},\n                warnings: [],\n                errors: []\n            };\n\n            setValidatedEntries(entries);\n            setPreview(cannyPreview);\n            setCurrentStep('preview');\n\n        } catch {\n            toast({\n                title: 'Processing failed',\n                description: 'Failed to process Canny entries.',\n                variant: 'destructive'\n            });\n        } finally {\n            setIsProcessing(false);\n            setImportStatus('');\n        }\n    };\n\n    // Perform the import\n    const performImport = async () => {\n        setIsProcessing(true);\n        setCurrentStep('importing');\n        setImportProgress(0);\n        setImportStatus('Preparing import...');\n\n        try {\n            // Check import progress\n            const updateProgress = (progress: number, status: string) => {\n                setImportProgress(progress);\n                setImportStatus(status);\n            };\n\n            updateProgress(10, 'Validating entries...');\n            await new Promise(resolve => setTimeout(resolve, 500));\n\n            updateProgress(25, 'Checking for conflicts...');\n            await new Promise(resolve => setTimeout(resolve, 400));\n\n            updateProgress(40, 'Processing entries...');\n            await new Promise(resolve => setTimeout(resolve, 600));\n\n            const response = await fetch('/api/projects/import/process', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({\n                    projectId,\n                    entries: validatedEntries.filter(e => e.isValid),\n                    options: importOptions\n                })\n            });\n\n            if (!response.ok) throw new Error('Import failed');\n\n            updateProgress(70, 'Creating entries...');\n            await new Promise(resolve => setTimeout(resolve, 500));\n\n            const result: ImportResult = await response.json();\n\n            updateProgress(90, 'Finalizing import...');\n            await new Promise(resolve => setTimeout(resolve, 400));\n\n            updateProgress(100, 'Import completed!');\n            await new Promise(resolve => setTimeout(resolve, 300));\n\n            onImportComplete(result);\n\n            if (result.success) {\n                toast({\n                    title: 'Import successful',\n                    description: `Imported ${result.importedCount} entries successfully.`\n                });\n            } else {\n                toast({\n                    title: 'Import completed with errors',\n                    description: `${result.importedCount} imported, ${result.errorCount} failed.`,\n                    variant: 'destructive'\n                });\n            }\n\n        } catch {\n            toast({\n                title: 'Import failed',\n                description: 'Failed to import the changelog entries.',\n                variant: 'destructive'\n            });\n            setCurrentStep('configure');\n        } finally {\n            setIsProcessing(false);\n            setImportProgress(0);\n            setImportStatus('');\n        }\n    };\n\n    // Reset modal state\n    const resetModal = () => {\n        setCurrentStep('source');\n        setImportSource('markdown');\n        setMarkdownContent('');\n        setParsedChangelog(null);\n        setPreview(null);\n        setValidatedEntries([]);\n        setImportProgress(0);\n        setImportStatus('');\n    };\n\n    return (\n        <Dialog open={open} onOpenChange={(open) => {\n            onOpenChange(open);\n            if (!open) resetModal();\n        }}>\n            <DialogContent className=\"max-w-4xl max-h-[90vh] overflow-hidden flex flex-col\">\n                <DialogHeader>\n                    <DialogTitle>Import Existing Changelog</DialogTitle>\n                    <DialogDescription>\n                        Import your existing changelog data from various sources\n                    </DialogDescription>\n                </DialogHeader>\n\n                <div className=\"flex-1 overflow-y-auto\">\n                    <AnimatePresence mode=\"wait\">\n                        {currentStep === 'source' && (\n                            <SourceSelectionStep\n                                importSource={importSource}\n                                onSourceChange={setImportSource}\n                                markdownContent={markdownContent}\n                                onMarkdownChange={setMarkdownContent}\n                                onFileUpload={handleFileUpload}\n                                onTextSubmit={handleTextSubmit}\n                                onCannyImport={handleCannyImport}\n                                isProcessing={isProcessing}\n                                processingStatus={importStatus}\n                            />\n                        )}\n\n                        {currentStep === 'preview' && preview && (\n                            <PreviewStep\n                                preview={preview}\n                                validatedEntries={validatedEntries}\n                                importSource={importSource}\n                                onNext={() => setCurrentStep('configure')}\n                                onBack={() => setCurrentStep('source')}\n                            />\n                        )}\n\n                        {currentStep === 'configure' && (\n                            <ConfigureStep\n                                options={importOptions}\n                                onOptionsChange={setImportOptions}\n                                preview={preview}\n                                onImport={performImport}\n                                onBack={() => setCurrentStep('preview')}\n                            />\n                        )}\n\n                        {currentStep === 'importing' && (\n                            <ImportingStep\n                                progress={importProgress}\n                                status={importStatus}\n                            />\n                        )}\n                    </AnimatePresence>\n                </div>\n            </DialogContent>\n        </Dialog>\n    );\n}\n\nfunction SourceSelectionStep({\n                                 importSource,\n                                 onSourceChange,\n                                 markdownContent,\n                                 onMarkdownChange,\n                                 onFileUpload,\n                                 onTextSubmit,\n                                 onCannyImport,\n                                 isProcessing,\n                                 processingStatus\n                             }: {\n    importSource: ImportSource;\n    onSourceChange: (source: ImportSource) => void;\n    markdownContent: string;\n    onMarkdownChange: (content: string) => void;\n    onFileUpload: (event: React.ChangeEvent<HTMLInputElement>) => void;\n    onTextSubmit: () => void;\n    onCannyImport: (entries: ValidatedEntry[]) => void;\n    isProcessing: boolean;\n    processingStatus: string;\n}) {\n    return (\n        <motion.div\n            initial={{opacity: 0, y: 20}}\n            animate={{opacity: 1, y: 0}}\n            exit={{opacity: 0, y: -20}}\n            className=\"space-y-6\"\n        >\n            {isProcessing && (\n                <div className=\"text-center py-8\">\n                    <div className=\"inline-flex items-center gap-3 px-6 py-3 bg-muted rounded-lg\">\n                        <Loader2 className=\"h-5 w-5 animate-spin text-primary\"/>\n                        <span className=\"text-sm font-medium\">{processingStatus}</span>\n                    </div>\n                </div>\n            )}\n\n            {!isProcessing && (\n                <>\n                    <div className=\"text-center space-y-2\">\n                        <h3 className=\"text-lg font-semibold\">Choose Import Source</h3>\n                        <p className=\"text-muted-foreground\">\n                            Select where you want to import your changelog data from\n                        </p>\n                    </div>\n\n                    <Tabs value={importSource} onValueChange={(value) => onSourceChange(value as ImportSource)}>\n                        <TabsList className=\"grid w-full grid-cols-2\">\n                            <TabsTrigger value=\"markdown\" className=\"flex items-center gap-2\">\n                                <FileText className=\"h-4 w-4\"/>\n                                Markdown / Files\n                            </TabsTrigger>\n                            <TabsTrigger value=\"canny\" className=\"flex items-center gap-2\">\n                                Canny\n                            </TabsTrigger>\n                        </TabsList>\n\n                        <TabsContent value=\"markdown\" className=\"space-y-6\">\n                            <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n                                {/* File Upload */}\n                                <Card>\n                                    <CardHeader>\n                                        <CardTitle className=\"flex items-center gap-2\">\n                                            <Upload className=\"h-5 w-5\"/>\n                                            Upload File\n                                        </CardTitle>\n                                        <CardDescription>\n                                            Upload your existing CHANGELOG.md or similar file\n                                        </CardDescription>\n                                    </CardHeader>\n                                    <CardContent>\n                                        <div\n                                            className=\"border-2 border-dashed border-muted-foreground/25 rounded-lg p-6 text-center\">\n                                            <input\n                                                type=\"file\"\n                                                accept=\".md,.txt\"\n                                                onChange={onFileUpload}\n                                                className=\"hidden\"\n                                                id=\"file-upload\"\n                                            />\n                                            <label htmlFor=\"file-upload\" className=\"cursor-pointer\">\n                                                <FileText className=\"h-12 w-12 mx-auto mb-4 text-muted-foreground\"/>\n                                                <p className=\"text-sm font-medium\">Click to upload</p>\n                                                <p className=\"text-xs text-muted-foreground mt-1\">\n                                                    Supports .md and .txt files\n                                                </p>\n                                            </label>\n                                        </div>\n                                    </CardContent>\n                                </Card>\n\n                                {/* Manual Input */}\n                                <Card>\n                                    <CardHeader>\n                                        <CardTitle className=\"flex items-center gap-2\">\n                                            <FileText className=\"h-5 w-5\"/>\n                                            Paste Content\n                                        </CardTitle>\n                                        <CardDescription>\n                                            Paste your changelog content directly\n                                        </CardDescription>\n                                    </CardHeader>\n                                    <CardContent>\n                                        <div className=\"space-y-4\">\n                                            <Textarea\n                                                placeholder=\"Paste your changelog markdown here...\"\n                                                value={markdownContent}\n                                                onChange={(e) => onMarkdownChange(e.target.value)}\n                                                className=\"min-h-[150px] font-mono text-sm\"\n                                            />\n                                            <Button\n                                                onClick={onTextSubmit}\n                                                disabled={!markdownContent.trim()}\n                                                className=\"w-full\"\n                                            >\n                                                Parse Content\n                                            </Button>\n                                        </div>\n                                    </CardContent>\n                                </Card>\n                            </div>\n\n                            {/* Format Help */}\n                            <Alert>\n                                <AlertDescription>\n                                    <strong>Supported formats:</strong> Keep a Changelog, GitHub Releases,\n                                    or any markdown with version headers. The parser will automatically\n                                    detect the format and extract entries.\n                                </AlertDescription>\n                            </Alert>\n                        </TabsContent>\n\n                        <TabsContent value=\"canny\">\n                            <CannyImportStep\n                                onImport={onCannyImport}\n                                isProcessing={isProcessing}\n                            />\n                        </TabsContent>\n                    </Tabs>\n                </>\n            )}\n        </motion.div>\n    );\n}\n\nfunction PreviewStep({\n                         preview,\n                         validatedEntries,\n                         importSource,\n                         onNext,\n                         onBack\n                     }: {\n    preview: ImportPreview;\n    validatedEntries: ValidatedEntry[];\n    importSource: ImportSource;\n    onNext: () => void;\n    onBack: () => void;\n}) {\n    const validEntries = validatedEntries.filter(e => e.isValid);\n\n    return (\n        <motion.div\n            initial={{opacity: 0, y: 20}}\n            animate={{opacity: 1, y: 0}}\n            exit={{opacity: 0, y: -20}}\n            className=\"space-y-6\"\n        >\n            {/* Source Badge */}\n            <div className=\"flex items-center justify-center\">\n                <Badge variant=\"outline\" className=\"flex items-center gap-2\">\n                    {importSource === 'markdown' ? (\n                        <FileText className=\"h-3 w-3\"/>\n                    ) : (\n                        <Code className=\"h-3 w-3\"/>\n                    )}\n                    {importSource === 'markdown' ? 'Markdown Import' : 'Canny Import'}\n                </Badge>\n            </div>\n\n            {/* Summary Stats */}\n            <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n                <Card>\n                    <CardContent className=\"p-4 text-center\">\n                        <div className=\"text-2xl font-bold text-green-600\">{preview.validEntries}</div>\n                        <div className=\"text-sm text-muted-foreground\">Valid Entries</div>\n                    </CardContent>\n                </Card>\n                <Card>\n                    <CardContent className=\"p-4 text-center\">\n                        <div className=\"text-2xl font-bold text-red-600\">{preview.invalidEntries}</div>\n                        <div className=\"text-sm text-muted-foreground\">Invalid Entries</div>\n                    </CardContent>\n                </Card>\n                <Card>\n                    <CardContent className=\"p-4 text-center\">\n                        <div className=\"text-2xl font-bold text-yellow-600\">{preview.duplicateVersions.length}</div>\n                        <div className=\"text-sm text-muted-foreground\">Conflicts</div>\n                    </CardContent>\n                </Card>\n                <Card>\n                    <CardContent className=\"p-4 text-center\">\n                        <div className=\"text-2xl font-bold\">{preview.totalEntries}</div>\n                        <div className=\"text-sm text-muted-foreground\">Total Found</div>\n                    </CardContent>\n                </Card>\n            </div>\n\n            {/* Warnings and Errors */}\n            {(preview.warnings.length > 0 || preview.errors.length > 0) && (\n                <div className=\"space-y-2\">\n                    {preview.warnings.length > 0 && (\n                        <Alert variant=\"warning\">\n                            <AlertDescription>\n                                <strong>Warnings ({preview.warnings.length}):</strong>\n                                <ul className=\"mt-1 list-disc list-inside text-sm\">\n                                    {preview.warnings.slice(0, 3).map((warning, i) => (\n                                        <li key={i}>{warning}</li>\n                                    ))}\n                                    {preview.warnings.length > 3 && (\n                                        <li>...and {preview.warnings.length - 3} more</li>\n                                    )}\n                                </ul>\n                            </AlertDescription>\n                        </Alert>\n                    )}\n\n                    {preview.errors.length > 0 && (\n                        <Alert variant=\"destructive\">\n                            <AlertDescription>\n                                <strong>Errors ({preview.errors.length}):</strong>\n                                <ul className=\"mt-1 list-disc list-inside text-sm\">\n                                    {preview.errors.slice(0, 3).map((error, i) => (\n                                        <li key={i}>{error}</li>\n                                    ))}\n                                    {preview.errors.length > 3 && (\n                                        <li>...and {preview.errors.length - 3} more</li>\n                                    )}\n                                </ul>\n                            </AlertDescription>\n                        </Alert>\n                    )}\n                </div>\n            )}\n\n            {/* Entry Preview */}\n            <Card>\n                <CardHeader>\n                    <CardTitle>Preview Entries</CardTitle>\n                    <CardDescription>\n                        First few entries that will be imported\n                    </CardDescription>\n                </CardHeader>\n                <CardContent>\n                    <div className=\"space-y-3 max-h-60 overflow-y-auto\">\n                        {validEntries.slice(0, 5).map((entry, i) => (\n                            <div key={i} className=\"flex items-start justify-between p-3 border rounded-lg\">\n                                <div className=\"flex-1 min-w-0\">\n                                    <div className=\"flex items-center gap-2 mb-1\">\n                                        <CheckCircle className=\"h-4 w-4 text-green-600 flex-shrink-0\"/>\n                                        <span className=\"font-medium truncate\">{entry.title}</span>\n                                        {entry.version && (\n                                            <Badge variant=\"secondary\" className=\"text-xs\">\n                                                {entry.version}\n                                            </Badge>\n                                        )}\n                                        {importSource === 'canny' && (\n                                            <Badge variant=\"outline\" className=\"text-xs\">\n                                                Canny\n                                            </Badge>\n                                        )}\n                                    </div>\n                                    <p className=\"text-sm text-muted-foreground truncate\">\n                                        {entry.content}\n                                    </p>\n                                    {entry.tags && entry.tags.length > 0 && (\n                                        <div className=\"flex gap-1 mt-2\">\n                                            {entry.tags.slice(0, 3).map((tag, tagIndex) => (\n                                                <Badge key={tagIndex} variant=\"outline\" className=\"text-xs\">\n                                                    {tag}\n                                                </Badge>\n                                            ))}\n                                            {entry.tags.length > 3 && (\n                                                <Badge variant=\"outline\" className=\"text-xs\">\n                                                    +{entry.tags.length - 3}\n                                                </Badge>\n                                            )}\n                                        </div>\n                                    )}\n                                </div>\n                            </div>\n                        ))}\n                        {validEntries.length > 5 && (\n                            <p className=\"text-sm text-muted-foreground text-center py-2\">\n                                ...and {validEntries.length - 5} more entries\n                            </p>\n                        )}\n                    </div>\n                </CardContent>\n            </Card>\n\n            {/* Navigation */}\n            <div className=\"flex justify-between\">\n                <Button variant=\"outline\" onClick={onBack}>\n                    Back to Source\n                </Button>\n                <Button\n                    onClick={onNext}\n                    disabled={preview.validEntries === 0}\n                >\n                    Configure Import\n                </Button>\n            </div>\n        </motion.div>\n    );\n}\n\nfunction ConfigureStep({\n                           options,\n                           onOptionsChange,\n                           preview,\n                           onImport,\n                           onBack\n                       }: {\n    options: ImportOptions;\n    onOptionsChange: (options: ImportOptions) => void;\n    preview: ImportPreview | null;\n    onImport: () => void;\n    onBack: () => void;\n}) {\n    return (\n        <motion.div\n            initial={{opacity: 0, y: 20}}\n            animate={{opacity: 1, y: 0}}\n            exit={{opacity: 0, y: -20}}\n            className=\"space-y-6\"\n        >\n            <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n                {/* Import Strategy */}\n                <Card>\n                    <CardHeader>\n                        <CardTitle>Import Strategy</CardTitle>\n                        <CardDescription>\n                            How should we handle existing entries?\n                        </CardDescription>\n                    </CardHeader>\n                    <CardContent className=\"space-y-4\">\n                        <RadioGroup\n                            value={options.strategy}\n                            onValueChange={(value: 'merge' | 'replace' | 'append') =>\n                                onOptionsChange({...options, strategy: value})\n                            }\n                        >\n                            <div className=\"flex items-center space-x-2\">\n                                <RadioGroupItem value=\"merge\" id=\"merge\"/>\n                                <Label htmlFor=\"merge\">Merge with existing</Label>\n                            </div>\n                            <div className=\"flex items-center space-x-2\">\n                                <RadioGroupItem value=\"append\" id=\"append\"/>\n                                <Label htmlFor=\"append\">Add to existing</Label>\n                            </div>\n                            <div className=\"flex items-center space-x-2\">\n                                <RadioGroupItem value=\"replace\" id=\"replace\"/>\n                                <Label htmlFor=\"replace\">Replace all existing</Label>\n                            </div>\n                        </RadioGroup>\n\n                        <div className=\"flex items-center space-x-2\">\n                            <Checkbox\n                                id=\"preserve-existing\"\n                                checked={options.preserveExistingEntries}\n                                onCheckedChange={(checked) =>\n                                    onOptionsChange({...options, preserveExistingEntries: !!checked})\n                                }\n                            />\n                            <Label htmlFor=\"preserve-existing\" className=\"text-sm\">\n                                Preserve existing entries\n                            </Label>\n                        </div>\n                    </CardContent>\n                </Card>\n\n                {/* Conflict Resolution */}\n                <Card>\n                    <CardHeader>\n                        <CardTitle>Conflict Resolution</CardTitle>\n                        <CardDescription>\n                            What to do when versions conflict?\n                        </CardDescription>\n                    </CardHeader>\n                    <CardContent className=\"space-y-4\">\n                        <RadioGroup\n                            value={options.conflictResolution}\n                            onValueChange={(value: 'skip' | 'overwrite' | 'prompt') =>\n                                onOptionsChange({...options, conflictResolution: value})\n                            }\n                        >\n                            <div className=\"flex items-center space-x-2\">\n                                <RadioGroupItem value=\"skip\" id=\"skip\"/>\n                                <Label htmlFor=\"skip\">Skip conflicting entries</Label>\n                            </div>\n                            <div className=\"flex items-center space-x-2\">\n                                <RadioGroupItem value=\"overwrite\" id=\"overwrite\"/>\n                                <Label htmlFor=\"overwrite\">Overwrite existing</Label>\n                            </div>\n                            <div className=\"flex items-center space-x-2\">\n                                <RadioGroupItem value=\"prompt\" id=\"prompt\"/>\n                                <Label htmlFor=\"prompt\">Prompt for each conflict</Label>\n                            </div>\n                        </RadioGroup>\n\n                        {preview?.duplicateVersions && preview.duplicateVersions.length > 0 && (\n                            <Alert variant=\"warning\">\n                                <AlertDescription>\n                                    <strong>Conflicts detected:</strong> {preview.duplicateVersions.join(', ')}\n                                </AlertDescription>\n                            </Alert>\n                        )}\n                    </CardContent>\n                </Card>\n\n                {/* Publishing Options */}\n                <Card>\n                    <CardHeader>\n                        <CardTitle>Publishing Options</CardTitle>\n                        <CardDescription>\n                            Control how entries are published\n                        </CardDescription>\n                    </CardHeader>\n                    <CardContent className=\"space-y-4\">\n                        <div className=\"flex items-center space-x-2\">\n                            <Checkbox\n                                id=\"publish-imported\"\n                                checked={options.publishImportedEntries}\n                                onCheckedChange={(checked) =>\n                                    onOptionsChange({...options, publishImportedEntries: !!checked})\n                                }\n                            />\n                            <Label htmlFor=\"publish-imported\" className=\"text-sm\">\n                                Publish imported entries immediately\n                            </Label>\n                        </div>\n\n                        <div className=\"space-y-2\">\n                            <Label className=\"text-sm font-medium\">Date Handling</Label>\n                            <RadioGroup\n                                value={options.dateHandling}\n                                onValueChange={(value: 'preserve' | 'current' | 'sequence') =>\n                                    onOptionsChange({...options, dateHandling: value})\n                                }\n                            >\n                                <div className=\"flex items-center space-x-2\">\n                                    <RadioGroupItem value=\"preserve\" id=\"preserve\"/>\n                                    <Label htmlFor=\"preserve\" className=\"text-sm\">Preserve original dates</Label>\n                                </div>\n                                <div className=\"flex items-center space-x-2\">\n                                    <RadioGroupItem value=\"current\" id=\"current\"/>\n                                    <Label htmlFor=\"current\" className=\"text-sm\">Use current date</Label>\n                                </div>\n                                <div className=\"flex items-center space-x-2\">\n                                    <RadioGroupItem value=\"sequence\" id=\"sequence\"/>\n                                    <Label htmlFor=\"sequence\" className=\"text-sm\">Sequential dates</Label>\n                                </div>\n                            </RadioGroup>\n                        </div>\n                    </CardContent>\n                </Card>\n\n                {/* Version Options */}\n                <Card>\n                    <CardHeader>\n                        <CardTitle>Version Options</CardTitle>\n                        <CardDescription>\n                            Handle version numbering\n                        </CardDescription>\n                    </CardHeader>\n                    <CardContent className=\"space-y-4\">\n                        <div className=\"flex items-center space-x-2\">\n                            <Checkbox\n                                id=\"auto-generate-versions\"\n                                checked={options.autoGenerateVersions}\n                                onCheckedChange={(checked) =>\n                                    onOptionsChange({...options, autoGenerateVersions: !!checked})\n                                }\n                            />\n                            <Label htmlFor=\"auto-generate-versions\" className=\"text-sm\">\n                                Auto-generate missing versions\n                            </Label>\n                        </div>\n\n                        <div className=\"space-y-2\">\n                            <Label htmlFor=\"default-tags\" className=\"text-sm font-medium\">\n                                Default Tags (comma-separated)\n                            </Label>\n                            <Textarea\n                                id=\"default-tags\"\n                                placeholder=\"enhancement, imported\"\n                                value={options.defaultTags.join(', ')}\n                                onChange={(e) =>\n                                    onOptionsChange({\n                                        ...options,\n                                        defaultTags: e.target.value.split(',').map(tag => tag.trim()).filter(Boolean)\n                                    })\n                                }\n                                className=\"h-20\"\n                            />\n                        </div>\n                    </CardContent>\n                </Card>\n            </div>\n\n            {/* Summary */}\n            {preview && (\n                <Card>\n                    <CardHeader>\n                        <CardTitle>Import Summary</CardTitle>\n                    </CardHeader>\n                    <CardContent>\n                        <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4 text-center\">\n                            <div>\n                                <div className=\"text-2xl font-bold text-green-600\">\n                                    {preview.validEntries}\n                                </div>\n                                <div className=\"text-sm text-muted-foreground\">Will be imported</div>\n                            </div>\n                            <div>\n                                <div className=\"text-2xl font-bold text-yellow-600\">\n                                    {preview.duplicateVersions.length}\n                                </div>\n                                <div className=\"text-sm text-muted-foreground\">Conflicts</div>\n                            </div>\n                            <div>\n                                <div className=\"text-2xl font-bold text-red-600\">\n                                    {preview.invalidEntries}\n                                </div>\n                                <div className=\"text-sm text-muted-foreground\">Will be skipped</div>\n                            </div>\n                            <div>\n                                <div className=\"text-2xl font-bold\">\n                                    {options.defaultTags.length}\n                                </div>\n                                <div className=\"text-sm text-muted-foreground\">Default tags</div>\n                            </div>\n                        </div>\n                    </CardContent>\n                </Card>\n            )}\n\n            {/* Navigation */}\n            <div className=\"flex justify-between\">\n                <Button variant=\"outline\" onClick={onBack}>\n                    Back to Preview\n                </Button>\n                <Button\n                    onClick={onImport}\n                    disabled={!preview || preview.validEntries === 0}\n                >\n                    Start Import\n                </Button>\n            </div>\n        </motion.div>\n    );\n}\n\nfunction ImportingStep({\n                           progress,\n                           status\n                       }: {\n    progress: number;\n    status: string;\n}) {\n    return (\n        <motion.div\n            initial={{opacity: 0, y: 20}}\n            animate={{opacity: 1, y: 0}}\n            exit={{opacity: 0, y: -20}}\n            className=\"flex flex-col items-center justify-center py-12 space-y-6\"\n        >\n            <div className=\"relative\">\n                <Loader2 className=\"h-16 w-16 animate-spin text-primary\"/>\n                <div className=\"absolute inset-0 h-16 w-16 rounded-full border-4 border-primary/20\"></div>\n            </div>\n\n            <div className=\"text-center space-y-3\">\n                <h3 className=\"text-xl font-semibold\">Importing Changelog Entries</h3>\n                <p className=\"text-muted-foreground max-w-md\">\n                    {status}\n                </p>\n            </div>\n\n            <div className=\"w-full max-w-md space-y-2\">\n                <Progress value={progress} className=\"h-3\"/>\n                <div className=\"flex justify-between text-sm text-muted-foreground\">\n                    <span>Progress</span>\n                    <span>{progress}%</span>\n                </div>\n            </div>\n        </motion.div>\n    );\n}"
  },
  {
    "path": "components/projects/importing/ImportDataPrompt.tsx",
    "content": "// components/projects/importing/ImportDataPrompt.tsx\n\n'use client';\n\nimport {useState} from 'react';\nimport {Upload, FileText, Sparkles, ArrowRight, CheckCircle} from 'lucide-react';\nimport {motion} from 'framer-motion';\n\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card';\nimport {Button} from '@/components/ui/button';\nimport {Badge} from '@/components/ui/badge';\n\nimport {ChangelogImportModal} from './ChangelogImportModal';\nimport {ImportResult} from '@/lib/types/projects/importing';\n\ninterface ImportDataPromptProps {\n    projectId: string;\n    projectName: string;\n    onImportComplete?: (result: ImportResult) => void;\n    className?: string;\n}\n\nexport function ImportDataPrompt({\n                                     projectId,\n                                     onImportComplete,\n                                     className\n                                 }: ImportDataPromptProps) {\n    const [showImportModal, setShowImportModal] = useState(false);\n\n    const handleImportComplete = (result: ImportResult) => {\n        setShowImportModal(false);\n        onImportComplete?.(result);\n    };\n\n    return (\n        <>\n            <motion.div\n                initial={{opacity: 0, y: 20}}\n                animate={{opacity: 1, y: 0}}\n                transition={{duration: 0.5}}\n                className={className}\n            >\n                <Card\n                    className=\"border-dashed border-2 border-muted-foreground/25 bg-gradient-to-br from-background to-muted/20\">\n                    <CardHeader className=\"text-center pb-4\">\n                        <div className=\"mx-auto mb-4 p-3 bg-primary/10 rounded-full w-fit\">\n                            <Sparkles className=\"h-8 w-8 text-primary\"/>\n                        </div>\n                        <CardTitle className=\"text-xl\">Jump-start Your Changelog</CardTitle>\n                        <CardDescription className=\"text-base\">\n                            Already have changelog data? Import it to get started quickly with your existing entries.\n                        </CardDescription>\n                    </CardHeader>\n\n                    <CardContent className=\"space-y-6\">\n                        {/* Import Benefits */}\n                        <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n                            <div className=\"text-center space-y-2\">\n                                <div className=\"mx-auto p-2 bg-blue-100 dark:bg-blue-900/20 rounded-full w-fit\">\n                                    <Upload className=\"h-5 w-5 text-blue-600 dark:text-blue-400\"/>\n                                </div>\n                                <h4 className=\"font-medium text-sm\">Easy Import</h4>\n                                <p className=\"text-xs text-muted-foreground\">\n                                    Upload your CHANGELOG.md or paste content directly\n                                </p>\n                            </div>\n\n                            <div className=\"text-center space-y-2\">\n                                <div className=\"mx-auto p-2 bg-green-100 dark:bg-green-900/20 rounded-full w-fit\">\n                                    <FileText className=\"h-5 w-5 text-green-600 dark:text-green-400\"/>\n                                </div>\n                                <h4 className=\"font-medium text-sm\">Smart Parsing</h4>\n                                <p className=\"text-xs text-muted-foreground\">\n                                    Automatically detects format and extracts entries\n                                </p>\n                            </div>\n\n                            <div className=\"text-center space-y-2\">\n                                <div className=\"mx-auto p-2 bg-purple-100 dark:bg-purple-900/20 rounded-full w-fit\">\n                                    <Sparkles className=\"h-5 w-5 text-purple-600 dark:text-purple-400\"/>\n                                </div>\n                                <h4 className=\"font-medium text-sm\">Instant Setup</h4>\n                                <p className=\"text-xs text-muted-foreground\">\n                                    Start with your complete changelog history\n                                </p>\n                            </div>\n                        </div>\n\n                        {/* Supported Formats */}\n                        <div className=\"space-y-3\">\n                            <h4 className=\"font-medium text-sm text-center\">Supported Formats</h4>\n                            <div className=\"flex flex-wrap justify-center gap-2\">\n                                <Badge variant=\"secondary\" className=\"text-xs\">\n                                    Keep a Changelog\n                                </Badge>\n                                <Badge variant=\"secondary\" className=\"text-xs\">\n                                    GitHub Releases\n                                </Badge>\n                                <Badge variant=\"secondary\" className=\"text-xs\">\n                                    Markdown Lists\n                                </Badge>\n                                <Badge variant=\"secondary\" className=\"text-xs\">\n                                    Custom Formats\n                                </Badge>\n                            </div>\n                        </div>\n\n                        {/* Example Preview */}\n                        <div className=\"bg-muted/30 rounded-lg p-4 space-y-3\">\n                            <h4 className=\"font-medium text-sm\">What gets imported:</h4>\n                            <div className=\"space-y-2 text-xs\">\n                                <div className=\"flex items-center gap-2\">\n                                    <CheckCircle className=\"h-3 w-3 text-green-600\"/>\n                                    <span>Version numbers and dates</span>\n                                </div>\n                                <div className=\"flex items-center gap-2\">\n                                    <CheckCircle className=\"h-3 w-3 text-green-600\"/>\n                                    <span>Entry titles and descriptions</span>\n                                </div>\n                                <div className=\"flex items-center gap-2\">\n                                    <CheckCircle className=\"h-3 w-3 text-green-600\"/>\n                                    <span>Categories and tags</span>\n                                </div>\n                                <div className=\"flex items-center gap-2\">\n                                    <CheckCircle className=\"h-3 w-3 text-green-600\"/>\n                                    <span>Markdown formatting</span>\n                                </div>\n                            </div>\n                        </div>\n\n                        {/* Call to Action */}\n                        <div className=\"flex flex-col sm:flex-row gap-3 pt-2\">\n                            <Button\n                                onClick={() => setShowImportModal(true)}\n                                className=\"flex-1 group\"\n                                size=\"lg\"\n                            >\n                                <Upload className=\"h-4 w-4 mr-2\"/>\n                                Import Existing Data\n                                <ArrowRight className=\"h-4 w-4 ml-2 group-hover:translate-x-1 transition-transform\"/>\n                            </Button>\n\n                            <Button\n                                variant=\"outline\"\n                                className=\"flex-1\"\n                                size=\"lg\"\n                                asChild\n                            >\n                                <a href={`/dashboard/projects/${projectId}/changelog/new`}>\n                                    Start Fresh Instead\n                                </a>\n                            </Button>\n                        </div>\n\n                        {/* Help Text */}\n                        <p className=\"text-xs text-muted-foreground text-center\">\n                            Don&apos;t have existing data? No problem! You can always start fresh and build your\n                            changelog from scratch.\n                        </p>\n                    </CardContent>\n                </Card>\n            </motion.div>\n\n            {/* Import Modal */}\n            <ChangelogImportModal\n                open={showImportModal}\n                onOpenChange={setShowImportModal}\n                projectId={projectId}\n                onImportComplete={handleImportComplete}\n            />\n        </>\n    );\n}\n\n// Empty State Component - Shows when no entries exist and user is admin\nexport function EmptyStateWithImport({\n                                         projectId,\n                                         isAdmin = false,\n                                         onImportComplete\n                                     }: {\n    projectId: string;\n    projectName: string;\n    isAdmin?: boolean;\n    onImportComplete?: (result: ImportResult) => void;\n}) {\n    const [showImportModal, setShowImportModal] = useState(false);\n\n    if (!isAdmin) {\n        return (\n            <motion.div\n                initial={{opacity: 0, y: 20}}\n                animate={{opacity: 1, y: 0}}\n                className=\"text-center py-12\"\n            >\n                <FileText className=\"h-16 w-16 text-muted-foreground mx-auto mb-4\"/>\n                <h3 className=\"text-xl font-semibold mb-2\">No Changelog Entries</h3>\n                <p className=\"text-muted-foreground\">\n                    This project doesn&apos;t have any changelog entries yet.\n                </p>\n            </motion.div>\n        );\n    }\n\n    const handleImportComplete = (result: ImportResult) => {\n        setShowImportModal(false);\n        onImportComplete?.(result);\n    };\n\n    return (\n        <>\n            <motion.div\n                initial={{opacity: 0, y: 20}}\n                animate={{opacity: 1, y: 0}}\n                className=\"text-center py-12 space-y-6\"\n            >\n                <div className=\"space-y-4\">\n                    <div className=\"mx-auto p-4 bg-muted/50 rounded-full w-fit\">\n                        <FileText className=\"h-12 w-12 text-muted-foreground\"/>\n                    </div>\n\n                    <div>\n                        <h3 className=\"text-2xl font-semibold mb-2\">Ready to Start Your Changelog?</h3>\n                        <p className=\"text-muted-foreground max-w-md mx-auto\">\n                            You can either import your existing changelog data or create your first entry from scratch.\n                        </p>\n                    </div>\n                </div>\n\n                <div className=\"flex flex-col sm:flex-row gap-4 justify-center max-w-md mx-auto\">\n                    <Button\n                        onClick={() => setShowImportModal(true)}\n                        className=\"group\"\n                        size=\"lg\"\n                    >\n                        <Upload className=\"h-4 w-4 mr-2\"/>\n                        Import Existing Data\n                        <ArrowRight className=\"h-4 w-4 ml-2 group-hover:translate-x-1 transition-transform\"/>\n                    </Button>\n\n                    <Button\n                        variant=\"outline\"\n                        size=\"lg\"\n                        asChild\n                    >\n                        <a href={`/dashboard/projects/${projectId}/changelog/new`}>\n                            <Sparkles className=\"h-4 w-4 mr-2\"/>\n                            Create First Entry\n                        </a>\n                    </Button>\n                </div>\n\n                {/* Quick Import Info */}\n                <Card className=\"max-w-lg mx-auto\">\n                    <CardContent className=\"pt-6\">\n                        <div className=\"text-sm space-y-2\">\n                            <p className=\"font-medium\">Quick Import Options:</p>\n                            <ul className=\"text-muted-foreground space-y-1\">\n                                <li>• Upload your CHANGELOG.md file</li>\n                                <li>• Paste markdown content directly</li>\n                                <li>• Supports most common changelog formats</li>\n                                <li>• Preview before importing</li>\n                            </ul>\n                        </div>\n                    </CardContent>\n                </Card>\n            </motion.div>\n\n            {/* Import Modal */}\n            <ChangelogImportModal\n                open={showImportModal}\n                onOpenChange={setShowImportModal}\n                projectId={projectId}\n                onImportComplete={handleImportComplete}\n            />\n        </>\n    );\n}"
  },
  {
    "path": "components/projects/importing/integrations/CannyImportStep.tsx",
    "content": "'use client';\n\nimport { useState } from 'react';\nimport { motion } from 'framer-motion';\nimport { Key, Download, CheckCircle, XCircle, Loader2 } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport { Checkbox } from '@/components/ui/checkbox';\nimport { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';\nimport {\n    Card,\n    CardContent,\n    CardHeader,\n    CardTitle,\n} from '@/components/ui/card';\nimport { Alert, AlertDescription } from '@/components/ui/alert';\nimport { useToast } from '@/hooks/use-toast';\nimport { ValidatedEntry } from '@/lib/types/projects/importing';\n\ninterface CannyImportStepProps {\n    onImport: (entries: ValidatedEntry[]) => void;\n    isProcessing: boolean;\n}\n\nexport function CannyImportStep({ onImport, isProcessing }: CannyImportStepProps) {\n    const { toast } = useToast();\n\n    const [apiKey, setApiKey] = useState('');\n    const [isValidating, setIsValidating] = useState(false);\n    const [isValid, setIsValid] = useState<boolean | null>(null);\n    const [validationError, setValidationError] = useState('');\n\n    const [includeLabels, setIncludeLabels] = useState(true);\n    const [includePostTags, setIncludePostTags] = useState(false);\n    const [statusFilter, setStatusFilter] = useState<'all' | 'published' | 'draft'>('published');\n    const [maxEntries, setMaxEntries] = useState(50);\n\n    const [isImporting, setIsImporting] = useState(false);\n\n    const validateApiKey = async () => {\n        if (!apiKey.trim()) {\n            setValidationError('Please enter an API key');\n            return;\n        }\n\n        setIsValidating(true);\n        setValidationError('');\n\n        try {\n            const response = await fetch('/api/projects/import/canny/validate', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({ apiKey })\n            });\n\n            const result = await response.json();\n\n            if (result.valid) {\n                setIsValid(true);\n                toast({\n                    title: 'API key validated',\n                    description: 'Successfully connected to Canny.'\n                });\n            } else {\n                setIsValid(false);\n                setValidationError(result.error || 'Invalid API key');\n            }\n        } catch {\n            setIsValid(false);\n            setValidationError('Failed to validate API key');\n        } finally {\n            setIsValidating(false);\n        }\n    };\n\n    const handleImport = async () => {\n        if (!isValid) {\n            toast({\n                title: 'Invalid API key',\n                description: 'Please validate your API key first.',\n                variant: 'destructive'\n            });\n            return;\n        }\n\n        setIsImporting(true);\n\n        try {\n            const response = await fetch('/api/projects/import/canny/fetch', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({\n                    apiKey,\n                    includeLabels,\n                    includePostTags,\n                    statusFilter,\n                    maxEntries\n                })\n            });\n\n            if (!response.ok) {\n                const errorData = await response.json();\n                throw new Error(errorData.error || 'Failed to fetch from Canny');\n            }\n\n            const data = await response.json();\n\n            if (!data.success || !data.entries || !Array.isArray(data.entries)) {\n                throw new Error('Invalid response from Canny API');\n            }\n\n            if (data.entries.length === 0) {\n                toast({\n                    title: 'No entries found',\n                    description: 'No changelog entries found in your Canny account with the current filters.',\n                    variant: 'destructive'\n                });\n                return;\n            }\n\n            // Pass the validated entries to the parent\n            onImport(data.entries);\n\n            toast({\n                title: 'Canny entries loaded',\n                description: `Found ${data.entries.length} entries from Canny.`\n            });\n\n        } catch (error) {\n            console.error('Canny import error:', error);\n            toast({\n                title: 'Import failed',\n                description: error instanceof Error ? error.message : 'Failed to fetch entries from Canny.',\n                variant: 'destructive'\n            });\n        } finally {\n            setIsImporting(false);\n        }\n    };\n\n    return (\n        <motion.div\n            initial={{ opacity: 0, y: 20 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0, y: -20 }}\n            className=\"space-y-6\"\n        >\n            <div className=\"text-center space-y-2\">\n                <h3 className=\"text-lg font-semibold mt-4\">Import from Canny</h3>\n                <p className=\"text-muted-foreground\">\n                    Import your published changelog entries from Canny\n                </p>\n            </div>\n\n            {/* API Key */}\n            <Card>\n                <CardHeader>\n                    <CardTitle className=\"flex items-center gap-2\">\n                        <Key className=\"h-5 w-5\" />\n                        API Configuration\n                    </CardTitle>\n                </CardHeader>\n                <CardContent className=\"space-y-4\">\n                    <div className=\"space-y-2\">\n                        <Label htmlFor=\"api-key\">Canny API Key</Label>\n                        <div className=\"flex gap-2\">\n                            <Input\n                                id=\"api-key\"\n                                type=\"password\"\n                                placeholder=\"Enter your Canny API key\"\n                                value={apiKey}\n                                onChange={(e) => {\n                                    setApiKey(e.target.value);\n                                    setIsValid(null);\n                                    setValidationError('');\n                                }}\n                                disabled={isProcessing}\n                            />\n                            <Button\n                                onClick={validateApiKey}\n                                disabled={!apiKey.trim() || isValidating || isProcessing}\n                                variant=\"outline\"\n                            >\n                                {isValidating ? (\n                                    <Loader2 className=\"h-4 w-4 animate-spin\" />\n                                ) : (\n                                    'Validate'\n                                )}\n                            </Button>\n                        </div>\n\n                        {isValid === true && (\n                            <div className=\"flex items-center gap-2 text-green-600 text-sm\">\n                                <CheckCircle className=\"h-4 w-4\" />\n                                API key is valid\n                            </div>\n                        )}\n\n                        {isValid === false && (\n                            <div className=\"flex items-center gap-2 text-red-600 text-sm\">\n                                <XCircle className=\"h-4 w-4\" />\n                                {validationError}\n                            </div>\n                        )}\n                    </div>\n\n                    <Alert icon={<Key className=\"h-4 w-4\" />}>\n                        <AlertDescription>\n                            Find your API key in Canny Settings → API. Your key is only used for this import.\n                        </AlertDescription>\n                    </Alert>\n                </CardContent>\n            </Card>\n\n            {/* Options */}\n            <Card>\n                <CardHeader>\n                    <CardTitle>Import Options</CardTitle>\n                </CardHeader>\n                <CardContent className=\"space-y-4\">\n                    {/* Status Filter */}\n                    <div className=\"space-y-3\">\n                        <Label className=\"text-sm font-medium\">Entry Status</Label>\n                        <RadioGroup\n                            value={statusFilter}\n                            onValueChange={(value: 'all' | 'published' | 'draft') => setStatusFilter(value)}\n                            disabled={isProcessing}\n                        >\n                            <div className=\"flex items-center space-x-2\">\n                                <RadioGroupItem value=\"published\" id=\"published\" />\n                                <Label htmlFor=\"published\" className=\"text-sm\">Published only</Label>\n                            </div>\n                            <div className=\"flex items-center space-x-2\">\n                                <RadioGroupItem value=\"all\" id=\"all\" />\n                                <Label htmlFor=\"all\" className=\"text-sm\">All entries</Label>\n                            </div>\n                        </RadioGroup>\n                    </div>\n\n                    {/* Tag Options */}\n                    <div className=\"space-y-3\">\n                        <Label className=\"text-sm font-medium\">Tag Options</Label>\n                        <div className=\"space-y-2\">\n                            <div className=\"flex items-center space-x-2\">\n                                <Checkbox\n                                    id=\"include-labels\"\n                                    checked={includeLabels}\n                                    onCheckedChange={(checked) => setIncludeLabels(!!checked)}\n                                    disabled={isProcessing}\n                                />\n                                <Label htmlFor=\"include-labels\" className=\"text-sm\">\n                                    Include Canny labels as tags\n                                </Label>\n                            </div>\n                            <div className=\"flex items-center space-x-2\">\n                                <Checkbox\n                                    id=\"include-post-tags\"\n                                    checked={includePostTags}\n                                    onCheckedChange={(checked) => setIncludePostTags(!!checked)}\n                                    disabled={isProcessing}\n                                />\n                                <Label htmlFor=\"include-post-tags\" className=\"text-sm\">\n                                    Include feature request tags\n                                </Label>\n                            </div>\n                        </div>\n                    </div>\n\n                    {/* Max Entries */}\n                    <div className=\"space-y-2\">\n                        <Label htmlFor=\"max-entries\" className=\"text-sm font-medium\">\n                            Maximum Entries\n                        </Label>\n                        <Input\n                            id=\"max-entries\"\n                            type=\"number\"\n                            min=\"1\"\n                            max=\"500\"\n                            value={maxEntries}\n                            onChange={(e) => setMaxEntries(parseInt(e.target.value) || 50)}\n                            disabled={isProcessing}\n                        />\n                    </div>\n                </CardContent>\n            </Card>\n\n            {/* Import Button */}\n            <div className=\"flex justify-center\">\n                <Button\n                    onClick={handleImport}\n                    disabled={!isValid || isImporting || isProcessing}\n                    size=\"lg\"\n                >\n                    {isImporting ? (\n                        <>\n                            <Loader2 className=\"h-4 w-4 mr-2 animate-spin\" />\n                            Fetching from Canny...\n                        </>\n                    ) : (\n                        <>\n                            <Download className=\"h-4 w-4 mr-2\" />\n                            Import from Canny\n                        </>\n                    )}\n                </Button>\n            </div>\n        </motion.div>\n    );\n}"
  },
  {
    "path": "components/providers/CommandPaletteProvider.tsx",
    "content": "'use client';\n\nimport { useCommandPalette } from '@/hooks/useCommandPalette';\nimport React from \"react\";\nimport ChangelogCommandPalette from \"@/components/CommandPalette\";\n\ninterface CommandPaletteProviderProps {\n    children: React.ReactNode;\n}\n\nexport function CommandPaletteProvider({ children }: CommandPaletteProviderProps) {\n    const { isOpen, close } = useCommandPalette();\n\n    return (\n        <>\n            {children}\n            <ChangelogCommandPalette isOpen={isOpen} onClose={close} />\n        </>\n    );\n}"
  },
  {
    "path": "components/providers/setup-context.tsx",
    "content": "'use client'\n\nimport { createContext, useContext, useState, useEffect, ReactNode } from 'react'\nimport { useRouter } from 'next/navigation'\n\n// Define the shape of our setup state\nexport type SetupStep = 'welcome' | 'admin' | 'settings' | 'oauth' | 'complete'\n\nexport interface AdminData {\n    name: string\n    email: string\n    password: string\n}\n\nexport interface SystemSettings {\n    defaultInvitationExpiry: number\n    requireApprovalForChangelogs: boolean\n    maxChangelogEntriesPerProject: number\n    enableAnalytics: boolean\n    enableNotifications: boolean\n    enablePasswordReset: boolean\n    smtpConfig?: {\n        host: string\n        port: number\n        user?: string\n        password?: string\n        secure: boolean\n        systemEmail: string\n    }\n}\n\nexport interface OAuthSettings {\n    enabled: boolean\n    provider?: string\n    baseUrl?: string\n    clientId?: string\n    clientSecret?: string\n}\n\nexport interface SetupState {\n    isLoading: boolean\n    currentStep: SetupStep\n    completedSteps: SetupStep[]\n    adminData: AdminData | null\n    systemSettings: SystemSettings | null\n    oauthSettings: OAuthSettings | null\n    error: string | null\n}\n\nexport interface SetupContextType extends SetupState {\n    goToStep: (step: SetupStep) => void\n    setAdminData: (data: AdminData) => void\n    setSystemSettings: (data: SystemSettings) => void\n    setOAuthSettings: (data: OAuthSettings) => void\n    completeSetup: () => void\n    clearError: () => void\n    checkSetupStatus: () => Promise<boolean>\n}\n\n// Default state\nconst defaultState: SetupState = {\n    isLoading: true,\n    currentStep: 'welcome',\n    completedSteps: [],\n    adminData: null,\n    systemSettings: null,\n    oauthSettings: {\n        enabled: false\n    },\n    error: null\n}\n\n// Create the context\nconst SetupContext = createContext<SetupContextType | undefined>(undefined)\n\n// Provider component\nexport function SetupProvider({ children }: { children: ReactNode }) {\n    const [state, setState] = useState<SetupState>(defaultState)\n    const router = useRouter()\n\n    // Load state from session storage if available\n    useEffect(() => {\n        try {\n            const savedState = sessionStorage.getItem('setupState')\n            if (savedState) {\n                const parsedState = JSON.parse(savedState)\n                setState(prev => ({ ...prev, ...parsedState, isLoading: false }))\n            } else {\n                setState(prev => ({ ...prev, isLoading: false }))\n            }\n        } catch {\n            setState(prev => ({ ...prev, isLoading: false }))\n        }\n    }, [])\n\n    // Save state to session storage when it changes\n    useEffect(() => {\n        if (!state.isLoading) {\n            const stateToSave = {\n                currentStep: state.currentStep,\n                completedSteps: state.completedSteps,\n                adminData: state.adminData,\n                systemSettings: state.systemSettings,\n                oauthSettings: state.oauthSettings,\n            }\n            sessionStorage.setItem('setupState', JSON.stringify(stateToSave))\n        }\n    }, [state])\n\n    // Check if setup has already been completed\n    const checkSetupStatus = async (): Promise<boolean> => {\n        try {\n            setState(prev => ({ ...prev, isLoading: true }))\n            const response = await fetch('/api/setup/status')\n            const data = await response.json()\n\n            if (data.isComplete) {\n                router.replace('/login')\n                return true\n            }\n\n            setState(prev => ({ ...prev, isLoading: false }))\n            return false\n        } catch {\n            setState(prev => ({\n                ...prev,\n                isLoading: false,\n                error: 'Failed to check setup status. Please try again.'\n            }))\n            return false\n        }\n    }\n\n    // Navigation between steps\n    const goToStep = (step: SetupStep) => {\n        setState(prev => ({ ...prev, currentStep: step }))\n    }\n\n    // Set admin data\n    const setAdminData = (data: AdminData) => {\n        setState(prev => ({\n            ...prev,\n            adminData: data,\n            completedSteps: [...prev.completedSteps.filter(s => s !== 'admin'), 'admin']\n        }))\n    }\n\n    // Set system settings\n    const setSystemSettings = (data: SystemSettings) => {\n        setState(prev => ({\n            ...prev,\n            systemSettings: data,\n            completedSteps: [...prev.completedSteps.filter(s => s !== 'settings'), 'settings']\n        }))\n    }\n\n    // Set OAuth settings\n    const setOAuthSettings = (data: OAuthSettings) => {\n        setState(prev => ({\n            ...prev,\n            oauthSettings: data,\n            completedSteps: [...prev.completedSteps.filter(s => s !== 'oauth'), 'oauth']\n        }))\n    }\n\n    // Complete the setup process\n    const completeSetup = () => {\n        setState(prev => ({\n            ...prev,\n            currentStep: 'complete',\n            completedSteps: [...prev.completedSteps, 'complete']\n        }))\n        // Clear session storage as setup is complete\n        sessionStorage.removeItem('setupState')\n    }\n\n    // Clear error\n    const clearError = () => {\n        setState(prev => ({ ...prev, error: null }))\n    }\n\n    const value = {\n        ...state,\n        goToStep,\n        setAdminData,\n        setSystemSettings,\n        setOAuthSettings,\n        completeSetup,\n        clearError,\n        checkSetupStatus\n    }\n\n    return <SetupContext.Provider value={value}>{children}</SetupContext.Provider>\n}\n\n// Custom hook to use the setup context\nexport function useSetup() {\n    const context = useContext(SetupContext)\n    if (context === undefined) {\n        throw new Error('useSetup must be used within a SetupProvider')\n    }\n    return context\n}"
  },
  {
    "path": "components/settings/connected-sso-section.tsx",
    "content": "// components/settings/ConnectedSsoProviders.tsx\nimport React from 'react';\nimport {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@/components/ui/card';\nimport {Badge} from '@/components/ui/badge';\nimport {ProviderLogo} from '@/components/sso/ProviderLogo';\nimport {Alert, AlertDescription} from '@/components/ui/alert';\nimport {CheckCircle2, Clock, AlertTriangle, Shield} from 'lucide-react';\nimport {motion} from 'framer-motion';\nimport {useTimezone} from '@/hooks/use-timezone';\n\ninterface OAuthProvider {\n    id: string;\n    name: string;\n    enabled: boolean;\n    isDefault: boolean;\n}\n\ninterface OAuthConnection {\n    id: string;\n    providerId: string;\n    provider: OAuthProvider;\n    providerUserId: string;\n    expiresAt: string | null;\n    createdAt: string;\n    updatedAt: string;\n}\n\ninterface SAMLProvider {\n    id: string;\n    name: string;\n    enabled: boolean;\n    isDefault: boolean;\n}\n\ninterface SAMLConnection {\n    id: string;\n    providerId: string;\n    provider: SAMLProvider;\n    nameId: string;\n    createdAt: string;\n    updatedAt: string;\n}\n\ninterface ConnectedSsoProvidersProps {\n    connections: OAuthConnection[];\n    allProviders: OAuthProvider[];\n    samlConnections?: SAMLConnection[];\n    allSAMLProviders?: SAMLProvider[];\n    isLoading?: boolean;\n}\n\ntype ConnectionStatus = 'connected' | 'expired' | 'disabled';\n\nconst ConnectedSsoProviders: React.FC<ConnectedSsoProvidersProps> = ({\n                                                                         connections,\n                                                                         allProviders,\n                                                                         samlConnections = [],\n                                                                         allSAMLProviders = [],\n                                                                         isLoading = false\n                                                                     }) => {\n    const timezone = useTimezone();\n\n    const getConnectionStatus = (connection: OAuthConnection): ConnectionStatus => {\n        if (!connection.provider.enabled) {\n            return 'disabled';\n        }\n\n        if (connection.expiresAt) {\n            const expirationDate = new Date(connection.expiresAt);\n            const now = new Date();\n            if (expirationDate <= now) {\n                return 'expired';\n            }\n        }\n\n        return 'connected';\n    };\n\n    const getStatusIcon = (status: ConnectionStatus) => {\n        switch (status) {\n            case 'connected':\n                return <CheckCircle2 className=\"h-4 w-4 text-green-600\"/>;\n            case 'expired':\n                return <Clock className=\"h-4 w-4 text-orange-500\"/>;\n            case 'disabled':\n                return <AlertTriangle className=\"h-4 w-4 text-red-500\"/>;\n        }\n    };\n\n    const getStatusBadge = (status: ConnectionStatus) => {\n        switch (status) {\n            case 'connected':\n                return <Badge variant=\"secondary\"\n                              className=\"bg-green-50 text-green-700 hover:bg-green-100\">Connected</Badge>;\n            case 'expired':\n                return <Badge variant=\"secondary\"\n                              className=\"bg-orange-50 text-orange-700 hover:bg-orange-100\">Expired</Badge>;\n            case 'disabled':\n                return <Badge variant=\"destructive\">Provider Disabled</Badge>;\n        }\n    };\n\n    const formatDate = (dateString: string) => {\n        return new Date(dateString).toLocaleDateString('en-US', {\n            year: 'numeric',\n            month: 'short',\n            day: 'numeric',\n            timeZone: timezone,\n        });\n    };\n\n    const formatRelativeTime = (dateString: string) => {\n        const date = new Date(dateString);\n        const now = new Date();\n        const diffInMs = now.getTime() - date.getTime();\n        const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24));\n\n        if (diffInDays === 0) return 'Today';\n        if (diffInDays === 1) return 'Yesterday';\n        if (diffInDays < 30) return `${diffInDays} days ago`;\n        if (diffInDays < 365) return `${Math.floor(diffInDays / 30)} months ago`;\n        return `${Math.floor(diffInDays / 365)} years ago`;\n    };\n\n    // Group connections and include removed providers\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    const connectedProviderIds = new Set(connections.map(conn => conn.providerId));\n    const removedProviders = connections.filter(conn =>\n        !allProviders.some(provider => provider.id === conn.providerId)\n    );\n\n    const hasAnything = connections.length > 0 || samlConnections.length > 0;\n\n    if (isLoading) {\n        return (\n            <Card className=\"border shadow-sm\">\n                <CardHeader className=\"pb-3\">\n                    <CardTitle className=\"text-lg md:text-xl flex items-center\">\n                        <Shield className=\"h-5 w-5 mr-2 text-muted-foreground\"/>\n                        Connected SSO Providers\n                    </CardTitle>\n                    <CardDescription className=\"text-sm\">\n                        Manage your single sign-on connections\n                    </CardDescription>\n                </CardHeader>\n                <CardContent>\n                    <div className=\"flex items-center justify-center py-8\">\n                        <div className=\"animate-pulse text-sm text-muted-foreground\">Loading your connections...</div>\n                    </div>\n                </CardContent>\n            </Card>\n        );\n    }\n\n    return (\n        <Card className=\"border shadow-sm\">\n            <CardHeader className=\"pb-3\">\n                <CardTitle className=\"text-lg md:text-xl flex items-center\">\n                    <Shield className=\"h-5 w-5 mr-2 text-muted-foreground\"/>\n                    Connected SSO Providers\n                </CardTitle>\n                <CardDescription className=\"text-sm\">\n                    View your single sign-on connections. These providers allow you to log in without entering a\n                    password.\n                </CardDescription>\n            </CardHeader>\n            <CardContent className=\"space-y-4\">\n                {!hasAnything ? (\n                    <Alert>\n                        <AlertDescription className=\"text-sm\">\n                            You haven&apos;t connected any SSO providers yet. You can still log in using your email and\n                            password.\n                        </AlertDescription>\n                    </Alert>\n                ) : (\n                    <>\n                        {/* OAuth Active Connections */}\n                        <div className=\"space-y-3\">\n                            {connections\n                                .filter(conn => allProviders.some(provider => provider.id === conn.providerId))\n                                .map((connection, index) => {\n                                    const status = getConnectionStatus(connection);\n                                    return (\n                                        <motion.div\n                                            key={connection.id}\n                                            initial={{opacity: 0, y: 20}}\n                                            animate={{opacity: 1, y: 0}}\n                                            transition={{duration: 0.2, delay: index * 0.1}}\n                                            className=\"flex items-center justify-between p-4 border rounded-lg bg-card/50 hover:bg-card transition-colors\"\n                                        >\n                                            <div className=\"flex items-center gap-3\">\n                                                <ProviderLogo\n                                                    providerName={connection.provider.name}\n                                                    size=\"md\"\n                                                />\n                                                <div>\n                                                    <div className=\"flex items-center gap-2\">\n                                                        <h4 className=\"font-medium text-sm md:text-base\">{connection.provider.name}</h4>\n                                                        {connection.provider.isDefault && (\n                                                            <Badge variant=\"outline\" className=\"text-xs\">Default</Badge>\n                                                        )}\n                                                    </div>\n                                                    <p className=\"text-xs md:text-sm text-muted-foreground\">\n                                                        Connected {formatRelativeTime(connection.createdAt)}\n                                                        {connection.expiresAt && status === 'connected' && (\n                                                            <span> • Expires {formatDate(connection.expiresAt)}</span>\n                                                        )}\n                                                    </p>\n                                                </div>\n                                            </div>\n                                            <div className=\"flex items-center gap-2\">\n                                                {getStatusIcon(status)}\n                                                {getStatusBadge(status)}\n                                            </div>\n                                        </motion.div>\n                                    );\n                                })\n                            }\n                        </div>\n\n                        {/* Removed Provider Warning */}\n                        {removedProviders.length > 0 && (\n                            <motion.div\n                                initial={{opacity: 0, height: 0}}\n                                animate={{opacity: 1, height: 'auto'}}\n                                transition={{duration: 0.3}}\n                            >\n                                <Alert className=\"border-orange-200 bg-orange-50/50\">\n                                    <AlertTriangle className=\"h-4 w-4 text-orange-600\"/>\n                                    <AlertDescription className=\"text-orange-800\">\n                                        <div className=\"font-medium mb-2 text-sm\">Some connected providers are no longer\n                                            available:\n                                        </div>\n                                        <div className=\"space-y-2\">\n                                            {removedProviders.map((connection) => (\n                                                <div key={connection.id}\n                                                     className=\"flex items-center gap-2 text-xs md:text-sm\">\n                                                    <div\n                                                        className=\"w-6 h-6 rounded-md bg-orange-200 flex items-center justify-center\">\n                            <span className=\"text-xs font-semibold text-orange-800\">\n                              {connection.provider.name.substring(0, 2).toUpperCase()}\n                            </span>\n                                                    </div>\n                                                    <span>{connection.provider.name}</span>\n                                                    <span\n                                                        className=\"text-orange-600\">• Connected {formatRelativeTime(connection.createdAt)}</span>\n                                                </div>\n                                            ))}\n                                        </div>\n                                        <p className=\"mt-2 text-xs md:text-sm\">\n                                            These connections are preserved in case the providers are re-enabled. You\n                                            can still log in using other methods.\n                                        </p>\n                                    </AlertDescription>\n                                </Alert>\n                            </motion.div>\n                        )}\n\n                        {/* SAML Connections */}\n                        {samlConnections.length > 0 && (\n                            <div className=\"space-y-3\">\n                                {connections.length > 0 && (\n                                    <p className=\"text-xs font-medium text-muted-foreground uppercase tracking-wide pt-1\">\n                                        SAML / Enterprise SSO\n                                    </p>\n                                )}\n                                {samlConnections.map((connection, index) => {\n                                    const providerEnabled = allSAMLProviders.some(p => p.id === connection.providerId && p.enabled);\n                                    const status: ConnectionStatus = providerEnabled ? 'connected' : 'disabled';\n                                    return (\n                                        <motion.div\n                                            key={connection.id}\n                                            initial={{opacity: 0, y: 20}}\n                                            animate={{opacity: 1, y: 0}}\n                                            transition={{duration: 0.2, delay: index * 0.1}}\n                                            className=\"flex items-center justify-between p-4 border rounded-lg bg-card/50 hover:bg-card transition-colors\"\n                                        >\n                                            <div className=\"flex items-center gap-3\">\n                                                <ProviderLogo providerName={connection.provider.name} size=\"md\"/>\n                                                <div>\n                                                    <div className=\"flex items-center gap-2\">\n                                                        <h4 className=\"font-medium text-sm md:text-base\">{connection.provider.name}</h4>\n                                                        <Badge variant=\"outline\" className=\"text-xs\">SAML</Badge>\n                                                        {connection.provider.isDefault && (\n                                                            <Badge variant=\"outline\" className=\"text-xs\">Default</Badge>\n                                                        )}\n                                                    </div>\n                                                    <p className=\"text-xs md:text-sm text-muted-foreground\">\n                                                        Connected {formatRelativeTime(connection.createdAt)}\n                                                    </p>\n                                                </div>\n                                            </div>\n                                            <div className=\"flex items-center gap-2\">\n                                                {getStatusIcon(status)}\n                                                {getStatusBadge(status)}\n                                            </div>\n                                        </motion.div>\n                                    );\n                                })}\n                            </div>\n                        )}\n                    </>\n                )}\n            </CardContent>\n        </Card>\n    );\n};\n\nexport default ConnectedSsoProviders;"
  },
  {
    "path": "components/settings/passkeys-section.tsx",
    "content": "'use client';\n\nimport { useState, useEffect } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle\n} from '@/components/ui/card';\nimport {\n    startRegistration,\n    browserSupportsWebAuthn,\n} from '@simplewebauthn/browser';\nimport { toast } from '@/hooks/use-toast';\nimport {\n    Fingerprint,\n    Trash2,\n    Loader2,\n    Smartphone,\n    Key,\n    Shield,\n    Plus,\n    Check,\n    AlertCircle,\n    Lock,\n    Laptop,\n    TabletSmartphone,\n    Info\n} from 'lucide-react';\nimport {\n    AlertDialog,\n    AlertDialogAction,\n    AlertDialogCancel,\n    AlertDialogContent,\n    AlertDialogDescription,\n    AlertDialogFooter,\n    AlertDialogHeader,\n    AlertDialogTitle,\n} from \"@/components/ui/alert-dialog\";\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogHeader,\n    DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { useTimezone } from '@/hooks/use-timezone';\nimport {\n    Tooltip,\n    TooltipContent,\n    TooltipProvider,\n    TooltipTrigger,\n} from \"@/components/ui/tooltip\";\n\ninterface Passkey {\n    id: string;\n    name: string;\n    createdAt: string;\n    lastUsedAt: string | null;\n}\n\n// Helper function to get device icon\nconst getDeviceIcon = (name: string) => {\n    const lowercaseName = name.toLowerCase();\n    if (lowercaseName.includes('iphone') || lowercaseName.includes('android') || lowercaseName.includes('phone')) {\n        return <Smartphone className=\"h-4 w-4\" />;\n    } else if (lowercaseName.includes('ipad') || lowercaseName.includes('tablet')) {\n        return <TabletSmartphone className=\"h-4 w-4\" />;\n    } else {\n        return <Laptop className=\"h-4 w-4\" />;\n    }\n};\n\n// Helper function to format date\nconst formatDate = (dateString: string, timeZone = 'UTC') => {\n    const date = new Date(dateString);\n    const now = new Date();\n    const diff = now.getTime() - date.getTime();\n    const days = Math.floor(diff / (1000 * 60 * 60 * 24));\n\n    if (days === 0) {\n        return 'Today';\n    } else if (days === 1) {\n        return 'Yesterday';\n    } else if (days < 7) {\n        return `${days} days ago`;\n    } else {\n        return date.toLocaleDateString('en-US', {\n            year: 'numeric',\n            month: 'short',\n            day: 'numeric',\n            timeZone,\n        });\n    }\n};\n\nexport function PasskeysSection() {\n    const timezone = useTimezone();\n    const [passkeys, setPasskeys] = useState<Passkey[]>([]);\n    const [isLoading, setIsLoading] = useState(true);\n    const [isAdding, setIsAdding] = useState(false);\n    const [newPasskeyName, setNewPasskeyName] = useState('');\n    const [deleteId, setDeleteId] = useState<string | null>(null);\n    const [supportsWebAuthn, setSupportsWebAuthn] = useState(false);\n    const [showAddDialog, setShowAddDialog] = useState(false);\n    const [showInfoDialog, setShowInfoDialog] = useState(false);\n\n    useEffect(() => {\n        setSupportsWebAuthn(browserSupportsWebAuthn());\n        fetchPasskeys();\n    }, []);\n\n    const fetchPasskeys = async () => {\n        try {\n            const response = await fetch('/api/auth/passkeys');\n            if (!response.ok) throw new Error('Failed to fetch passkeys');\n\n            const data = await response.json();\n            setPasskeys(data.passkeys);\n        } catch {\n            toast({\n                title: 'Error',\n                description: 'Failed to load passkeys',\n                variant: 'destructive',\n            });\n        } finally {\n            setIsLoading(false);\n        }\n    };\n\n    const handleAddPasskey = async () => {\n        if (!newPasskeyName.trim()) {\n            toast({\n                title: 'Error',\n                description: 'Please enter a name for your passkey',\n                variant: 'destructive',\n            });\n            return;\n        }\n\n        try {\n            setIsAdding(true);\n\n            // Get registration options\n            const optionsResponse = await fetch('/api/auth/passkeys/register/options', {\n                method: 'POST',\n            });\n\n            if (!optionsResponse.ok) {\n                throw new Error('Failed to get registration options');\n            }\n\n            const options = await optionsResponse.json();\n\n            // Start WebAuthn registration\n            const registrationResponse = await startRegistration(options);\n\n            // Verify with server\n            const verifyResponse = await fetch('/api/auth/passkeys/register/verify', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({\n                    response: registrationResponse,\n                    name: newPasskeyName,\n                }),\n            });\n\n            if (!verifyResponse.ok) {\n                throw new Error('Failed to register passkey');\n            }\n\n            toast({\n                title: 'Success',\n                description: 'Passkey added successfully',\n            });\n\n            setNewPasskeyName('');\n            setShowAddDialog(false);\n            fetchPasskeys();\n        } catch (error) {\n            console.error('Passkey registration error:', error);\n            toast({\n                title: 'Error',\n                description: 'Failed to add passkey. Make sure your device supports passkeys.',\n                variant: 'destructive',\n            });\n        } finally {\n            setIsAdding(false);\n        }\n    };\n\n    const handleDeletePasskey = async (id: string) => {\n        try {\n            const response = await fetch(`/api/auth/passkeys/${id}`, {\n                method: 'DELETE',\n            });\n\n            if (!response.ok) {\n                throw new Error('Failed to delete passkey');\n            }\n\n            toast({\n                title: 'Success',\n                description: 'Passkey deleted successfully',\n            });\n\n            fetchPasskeys();\n        } catch {\n            toast({\n                title: 'Error',\n                description: 'Failed to delete passkey',\n                variant: 'destructive',\n            });\n        } finally {\n            setDeleteId(null);\n        }\n    };\n\n    if (!supportsWebAuthn) {\n        return (\n            <Card className=\"border-warning\">\n                <CardHeader>\n                    <div className=\"flex items-center gap-2\">\n                        <AlertCircle className=\"h-5 w-5 text-warning\" />\n                        <CardTitle>Passkeys Not Supported</CardTitle>\n                    </div>\n                    <CardDescription>\n                        Your browser doesn&apos;t support passkeys. Please use a modern browser to enable this security feature.\n                    </CardDescription>\n                </CardHeader>\n            </Card>\n        );\n    }\n\n    return (\n        <>\n            <Card>\n                <CardHeader>\n                    <div className=\"flex items-center justify-between\">\n                        <div className=\"space-y-1\">\n                            <CardTitle className=\"flex items-center gap-2\">\n                                <Shield className=\"h-5 w-5 text-primary\" />\n                                Passkeys\n                            </CardTitle>\n                            <CardDescription>\n                                Secure, passwordless authentication for your account\n                            </CardDescription>\n                        </div>\n                        <Button\n                            variant=\"ghost\"\n                            size=\"sm\"\n                            className=\"gap-2\"\n                            onClick={() => setShowInfoDialog(true)}\n                        >\n                            <Info className=\"h-4 w-4\" />\n                            Learn More\n                        </Button>\n                    </div>\n                </CardHeader>\n                <CardContent>\n                    {isLoading ? (\n                        <div className=\"flex items-center justify-center py-8\">\n                            <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n                        </div>\n                    ) : passkeys.length === 0 ? (\n                        <div className=\"text-center py-8\">\n                            <div className=\"mx-auto w-fit rounded-full bg-primary/10 p-4 mb-4\">\n                                <Key className=\"h-8 w-8 text-primary\" />\n                            </div>\n                            <h3 className=\"font-semibold text-lg mb-2\">No passkeys yet</h3>\n                            <p className=\"text-muted-foreground mb-6 max-w-sm mx-auto\">\n                                Add a passkey to enable secure, passwordless authentication for your account.\n                            </p>\n                            <Button onClick={() => setShowAddDialog(true)} className=\"gap-2\">\n                                <Plus className=\"h-4 w-4\" />\n                                Add your first passkey\n                            </Button>\n                        </div>\n                    ) : (\n                        <div className=\"space-y-6\">\n                            <div className=\"flex items-center justify-between\">\n                                <div className=\"flex items-center gap-4\">\n                                    <Badge variant=\"secondary\" className=\"gap-1\">\n                                        <Check className=\"h-3 w-3\" />\n                                        {passkeys.length} Active\n                                    </Badge>\n                                </div>\n                                <Button onClick={() => setShowAddDialog(true)} className=\"gap-2\">\n                                    <Plus className=\"h-4 w-4\" />\n                                    Add Passkey\n                                </Button>\n                            </div>\n\n                            <Separator />\n\n                            <AnimatePresence initial={false}>\n                                <div className=\"space-y-3\">\n                                    {passkeys.map((passkey, index) => (\n                                        <motion.div\n                                            key={passkey.id}\n                                            initial={{ opacity: 0, y: 20 }}\n                                            animate={{ opacity: 1, y: 0 }}\n                                            exit={{ opacity: 0, y: -20 }}\n                                            transition={{ duration: 0.2, delay: index * 0.05 }}\n                                        >\n                                            <div className=\"flex items-center gap-4 p-4 border rounded-lg hover:bg-muted/50 transition-colors\">\n                                                <div className=\"flex-shrink-0\">\n                                                    <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center\">\n                                                        {getDeviceIcon(passkey.name)}\n                                                    </div>\n                                                </div>\n                                                <div className=\"flex-grow min-w-0\">\n                                                    <div className=\"flex items-center gap-2\">\n                                                        <h4 className=\"font-medium truncate\">{passkey.name}</h4>\n                                                        {!passkey.lastUsedAt && (\n                                                            <Badge variant=\"outline\" className=\"text-xs\">\n                                                                Never used\n                                                            </Badge>\n                                                        )}\n                                                    </div>\n                                                    <div className=\"flex items-center gap-4 text-sm text-muted-foreground\">\n                                                        <span>Added {formatDate(passkey.createdAt, timezone)}</span>\n                                                        {passkey.lastUsedAt && (\n                                                            <>\n                                                                <span>•</span>\n                                                                <span>Last used {formatDate(passkey.lastUsedAt, timezone)}</span>\n                                                            </>\n                                                        )}\n                                                    </div>\n                                                </div>\n                                                <div className=\"flex-shrink-0\">\n                                                    <TooltipProvider>\n                                                        <Tooltip>\n                                                            <TooltipTrigger asChild>\n                                                                <Button\n                                                                    variant=\"ghost\"\n                                                                    size=\"icon\"\n                                                                    className=\"text-destructive hover:text-destructive\"\n                                                                    onClick={() => setDeleteId(passkey.id)}\n                                                                >\n                                                                    <Trash2 className=\"h-4 w-4\" />\n                                                                </Button>\n                                                            </TooltipTrigger>\n                                                            <TooltipContent>Delete passkey</TooltipContent>\n                                                        </Tooltip>\n                                                    </TooltipProvider>\n                                                </div>\n                                            </div>\n                                        </motion.div>\n                                    ))}\n                                </div>\n                            </AnimatePresence>\n                        </div>\n                    )}\n                </CardContent>\n            </Card>\n\n            {/* Add Passkey Dialog */}\n            <Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>\n                <DialogContent>\n                    <DialogHeader>\n                        <DialogTitle>Add a Passkey</DialogTitle>\n                        <DialogDescription>\n                            Give your passkey a name to help you identify it later.\n                        </DialogDescription>\n                    </DialogHeader>\n                    <div className=\"space-y-4 py-4\">\n                        <div className=\"space-y-2\">\n                            <label htmlFor=\"passkey-name\" className=\"text-sm font-medium\">\n                                Passkey Name\n                            </label>\n                            <Input\n                                id=\"passkey-name\"\n                                placeholder=\"e.g., MacBook Pro, iPhone 15\"\n                                value={newPasskeyName}\n                                onChange={(e) => setNewPasskeyName(e.target.value)}\n                                disabled={isAdding}\n                            />\n                            <p className=\"text-sm text-muted-foreground\">\n                                Choose a name that helps you identify this device or browser.\n                            </p>\n                        </div>\n                    </div>\n                    <div className=\"flex justify-end gap-3\">\n                        <Button\n                            variant=\"outline\"\n                            onClick={() => setShowAddDialog(false)}\n                            disabled={isAdding}\n                        >\n                            Cancel\n                        </Button>\n                        <Button onClick={handleAddPasskey} disabled={isAdding}>\n                            {isAdding ? (\n                                <>\n                                    <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                                    Adding...\n                                </>\n                            ) : (\n                                <>\n                                    <Fingerprint className=\"mr-2 h-4 w-4\" />\n                                    Add Passkey\n                                </>\n                            )}\n                        </Button>\n                    </div>\n                </DialogContent>\n            </Dialog>\n\n            {/* Learn More Dialog */}\n            <Dialog open={showInfoDialog} onOpenChange={setShowInfoDialog}>\n                <DialogContent className=\"max-w-2xl\">\n                    <DialogHeader>\n                        <DialogTitle>About Passkeys</DialogTitle>\n                        <DialogDescription>\n                            Learn how passkeys can make your account more secure\n                        </DialogDescription>\n                    </DialogHeader>\n                    <div className=\"space-y-4 py-4\">\n                        <div className=\"grid gap-4 md:grid-cols-2\">\n                            <Card>\n                                <CardHeader className=\"pb-3\">\n                                    <CardTitle className=\"text-base flex items-center gap-2\">\n                                        <Shield className=\"h-4 w-4 text-primary\" />\n                                        Enhanced Security\n                                    </CardTitle>\n                                </CardHeader>\n                                <CardContent className=\"text-sm text-muted-foreground\">\n                                    Passkeys are resistant to phishing and use strong cryptographic security, making them more secure than passwords.\n                                </CardContent>\n                            </Card>\n                            <Card>\n                                <CardHeader className=\"pb-3\">\n                                    <CardTitle className=\"text-base flex items-center gap-2\">\n                                        <Fingerprint className=\"h-4 w-4 text-primary\" />\n                                        Easy to Use\n                                    </CardTitle>\n                                </CardHeader>\n                                <CardContent className=\"text-sm text-muted-foreground\">\n                                    Sign in with just your fingerprint, face, or device PIN. No need to remember complex passwords.\n                                </CardContent>\n                            </Card>\n                            <Card>\n                                <CardHeader className=\"pb-3\">\n                                    <CardTitle className=\"text-base flex items-center gap-2\">\n                                        <Key className=\"h-4 w-4 text-primary\" />\n                                        Device Specific\n                                    </CardTitle>\n                                </CardHeader>\n                                <CardContent className=\"text-sm text-muted-foreground\">\n                                    Each passkey is tied to a specific device or browser, providing an extra layer of security.\n                                </CardContent>\n                            </Card>\n                            <Card>\n                                <CardHeader className=\"pb-3\">\n                                    <CardTitle className=\"text-base flex items-center gap-2\">\n                                        <Lock className=\"h-4 w-4 text-primary\" />\n                                        No Shared Secrets\n                                    </CardTitle>\n                                </CardHeader>\n                                <CardContent className=\"text-sm text-muted-foreground\">\n                                    Your private key never leaves your device, eliminating the risk of server-side breaches.\n                                </CardContent>\n                            </Card>\n                        </div>\n                        <Separator />\n                        <div>\n                            <h4 className=\"font-medium mb-2\">Supported Devices</h4>\n                            <p className=\"text-sm text-muted-foreground\">\n                                Passkeys work on most modern devices and browsers, including:\n                            </p>\n                            <ul className=\"list-disc list-inside text-sm text-muted-foreground mt-2 space-y-1\">\n                                <li>iPhones and iPads (iOS 16+)</li>\n                                <li>Android phones and tablets (Android 9+)</li>\n                                <li>Windows computers with Windows Hello</li>\n                                <li>Macs with Touch ID or macOS Ventura+</li>\n                                <li>Chrome, Safari, Edge, and Firefox browsers</li>\n                            </ul>\n                        </div>\n                    </div>\n                </DialogContent>\n            </Dialog>\n\n            {/* Delete Confirmation Dialog */}\n            <AlertDialog open={!!deleteId} onOpenChange={() => setDeleteId(null)}>\n                <AlertDialogContent>\n                    <AlertDialogHeader>\n                        <AlertDialogTitle>Delete Passkey</AlertDialogTitle>\n                        <AlertDialogDescription>\n                            Are you sure you want to delete this passkey? You won&apos;t be able to use it to sign in anymore.\n                        </AlertDialogDescription>\n                    </AlertDialogHeader>\n                    <AlertDialogFooter>\n                        <AlertDialogCancel>Cancel</AlertDialogCancel>\n                        <AlertDialogAction\n                            onClick={() => deleteId && handleDeletePasskey(deleteId)}\n                            className=\"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\n                        >\n                            Delete\n                        </AlertDialogAction>\n                    </AlertDialogFooter>\n                </AlertDialogContent>\n            </AlertDialog>\n        </>\n    );\n}"
  },
  {
    "path": "components/settings/security-settings.tsx",
    "content": "'use client';\n\nimport { useState, useEffect } from 'react';\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardHeader,\n    CardTitle\n} from '@/components/ui/card';\nimport { toast } from '@/hooks/use-toast';\nimport {\n    Shield,\n    Lock,\n    Fingerprint,\n    Info,\n    AlertTriangle\n} from 'lucide-react';\nimport {\n    Select,\n    SelectContent,\n    SelectItem,\n    SelectTrigger,\n    SelectValue,\n} from \"@/components/ui/select\";\nimport {\n    AlertDialog,\n    AlertDialogAction,\n    AlertDialogCancel,\n    AlertDialogContent,\n    AlertDialogDescription,\n    AlertDialogFooter,\n    AlertDialogHeader,\n    AlertDialogTitle,\n} from \"@/components/ui/alert-dialog\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { motion } from \"framer-motion\";\nimport { Badge } from \"@/components/ui/badge\";\n\ntype TwoFactorMode = 'NONE' | 'PASSKEY_PLUS_PASSWORD' | 'PASSWORD_PLUS_PASSKEY';\n\ninterface SecuritySettings {\n    twoFactorMode: TwoFactorMode;\n    hasPasskeys: boolean;\n}\n\nconst securityModeDetails = {\n    NONE: {\n        title: 'Standard Security',\n        description: 'Sign in with either password or passkey',\n        icon: <Shield className=\"h-5 w-5\" />,\n        color: 'text-muted-foreground'\n    },\n    PASSKEY_PLUS_PASSWORD: {\n        title: 'Passkey + Password',\n        description: 'Passkey sign-in requires additional password verification',\n        icon: <div className=\"flex gap-1\">\n            <Fingerprint className=\"h-5 w-5\" />\n            <Lock className=\"h-5 w-5\" />\n        </div>,\n        color: 'text-primary'\n    },\n    PASSWORD_PLUS_PASSKEY: {\n        title: 'Password + Passkey',\n        description: 'Password sign-in requires additional passkey verification',\n        icon: <div className=\"flex gap-1\">\n            <Lock className=\"h-5 w-5\" />\n            <Fingerprint className=\"h-5 w-5\" />\n        </div>,\n        color: 'text-primary'\n    }\n};\n\n// Helper function to safely get security mode details\nconst getSecurityModeDetails = (mode: TwoFactorMode | undefined | null) => {\n    if (!mode || !(mode in securityModeDetails)) {\n        return securityModeDetails.NONE;\n    }\n    return securityModeDetails[mode];\n};\n\nexport function SecuritySettings() {\n    const [settings, setSettings] = useState<SecuritySettings>({\n        twoFactorMode: 'NONE',\n        hasPasskeys: false\n    });\n    const [isLoading, setIsLoading] = useState(true);\n    const [isSaving, setIsSaving] = useState(false);\n    const [showConfirmDialog, setShowConfirmDialog] = useState(false);\n    const [pendingMode, setPendingMode] = useState<TwoFactorMode | null>(null);\n\n    useEffect(() => {\n        fetchSettings();\n    }, []);\n\n    const fetchSettings = async () => {\n        try {\n            const [settingsResponse, passkeysResponse] = await Promise.all([\n                fetch('/api/auth/security-settings'),\n                fetch('/api/auth/passkeys')\n            ]);\n\n            if (!settingsResponse.ok || !passkeysResponse.ok) {\n                throw new Error('Failed to fetch settings');\n            }\n\n            const settingsData = await settingsResponse.json();\n            const passkeysData = await passkeysResponse.json();\n\n            // Ensure twoFactorMode is valid\n            const twoFactorMode = settingsData.twoFactorMode as TwoFactorMode | undefined;\n            const validMode = twoFactorMode && twoFactorMode in securityModeDetails\n                ? twoFactorMode\n                : 'NONE';\n\n            setSettings({\n                twoFactorMode: validMode,\n                hasPasskeys: passkeysData.passkeys?.length > 0\n            });\n        } catch (error) {\n            console.error('Failed to fetch settings:', error);\n            toast({\n                title: 'Error',\n                description: 'Failed to load security settings',\n                variant: 'destructive',\n            });\n        } finally {\n            setIsLoading(false);\n        }\n    };\n\n    const handleModeChange = (newMode: TwoFactorMode) => {\n        if (newMode === settings.twoFactorMode) return;\n\n        // Check if user has passkeys before enabling 2FA modes\n        if ((newMode === 'PASSKEY_PLUS_PASSWORD' || newMode === 'PASSWORD_PLUS_PASSKEY') && !settings.hasPasskeys) {\n            toast({\n                title: 'Passkey Required',\n                description: 'Please add at least one passkey before enabling additional security',\n                variant: 'destructive',\n            });\n            return;\n        }\n\n        setPendingMode(newMode);\n        setShowConfirmDialog(true);\n    };\n\n    const confirmModeChange = async () => {\n        if (!pendingMode) return;\n\n        try {\n            setIsSaving(true);\n            const response = await fetch('/api/auth/security-settings', {\n                method: 'PATCH',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify({ twoFactorMode: pendingMode })\n            });\n\n            if (!response.ok) {\n                throw new Error('Failed to update security settings');\n            }\n\n            setSettings(prev => ({ ...prev, twoFactorMode: pendingMode }));\n            toast({\n                title: 'Success',\n                description: 'Security settings updated successfully',\n            });\n        } catch (error) {\n            console.error('Failed to update security settings:', error);\n            toast({\n                title: 'Error',\n                description: 'Failed to update security settings',\n                variant: 'destructive',\n            });\n        } finally {\n            setIsSaving(false);\n            setShowConfirmDialog(false);\n            setPendingMode(null);\n        }\n    };\n\n    if (isLoading) {\n        return (\n            <Card>\n                <CardContent className=\"flex items-center justify-center py-8\">\n                    <div className=\"animate-pulse\">Loading security settings...</div>\n                </CardContent>\n            </Card>\n        );\n    }\n\n    const currentModeDetails = getSecurityModeDetails(settings.twoFactorMode);\n\n    return (\n        <>\n            <Card>\n                <CardHeader>\n                    <div className=\"flex items-center justify-between\">\n                        <div className=\"space-y-1\">\n                            <CardTitle className=\"flex items-center gap-2\">\n                                <Shield className=\"h-5 w-5 text-primary\" />\n                                Additional Security\n                            </CardTitle>\n                            <CardDescription>\n                                Configure two-factor authentication for enhanced account security\n                            </CardDescription>\n                        </div>\n                        {settings.twoFactorMode !== 'NONE' && (\n                            <Badge variant=\"secondary\" className=\"gap-1\">\n                                <Lock className=\"h-3 w-3\" />\n                                2FA Enabled\n                            </Badge>\n                        )}\n                    </div>\n                </CardHeader>\n                <CardContent className=\"space-y-6\">\n                    <div className=\"space-y-4\">\n                        <div className=\"space-y-2\">\n                            <label className=\"text-sm font-medium\">Security Mode</label>\n                            <Select\n                                value={settings.twoFactorMode}\n                                onValueChange={(value) => handleModeChange(value as TwoFactorMode)}\n                            >\n                                <SelectTrigger>\n                                    <SelectValue>\n                                        <div className=\"flex items-center gap-2\">\n                                            <div className={currentModeDetails.color}>\n                                                {currentModeDetails.icon}\n                                            </div>\n                                            <span>{currentModeDetails.title}</span>\n                                        </div>\n                                    </SelectValue>\n                                </SelectTrigger>\n                                <SelectContent>\n                                    {Object.entries(securityModeDetails).map(([mode, details]) => (\n                                        <SelectItem key={mode} value={mode}>\n                                            <div className=\"flex items-center gap-3\">\n                                                <div className={details.color}>\n                                                    {details.icon}\n                                                </div>\n                                                <div>\n                                                    <div className=\"font-medium\">{details.title}</div>\n                                                    <div className=\"text-sm text-muted-foreground\">\n                                                        {details.description}\n                                                    </div>\n                                                </div>\n                                            </div>\n                                        </SelectItem>\n                                    ))}\n                                </SelectContent>\n                            </Select>\n                        </div>\n\n                        {!settings.hasPasskeys && settings.twoFactorMode === 'NONE' && (\n                            <div className=\"flex items-start gap-3 p-4 rounded-lg bg-muted/50\">\n                                <Info className=\"h-5 w-5 text-muted-foreground mt-0.5\" />\n                                <div className=\"space-y-1\">\n                                    <p className=\"text-sm font-medium\">Add a passkey first</p>\n                                    <p className=\"text-sm text-muted-foreground\">\n                                        You need at least one passkey to enable additional security options\n                                    </p>\n                                </div>\n                            </div>\n                        )}\n\n                        <Separator />\n\n                        <div className=\"space-y-4\">\n                            <h4 className=\"font-medium\">How it works</h4>\n\n                            <motion.div\n                                className=\"grid gap-4\"\n                                initial={{ opacity: 0, y: 20 }}\n                                animate={{ opacity: 1, y: 0 }}\n                                transition={{ duration: 0.3 }}\n                            >\n                                <Card className={settings.twoFactorMode === 'PASSKEY_PLUS_PASSWORD' ? 'border-primary' : ''}>\n                                    <CardContent className=\"p-4\">\n                                        <div className=\"flex items-start gap-4\">\n                                            <div className=\"p-2 rounded-lg bg-primary/10\">\n                                                <Fingerprint className=\"h-6 w-6 text-primary\" />\n                                            </div>\n                                            <div className=\"space-y-1\">\n                                                <h5 className=\"font-medium\">Passkey + Password</h5>\n                                                <p className=\"text-sm text-muted-foreground\">\n                                                    When you sign in with a passkey, you&apos;ll also need to enter your password\n                                                </p>\n                                            </div>\n                                        </div>\n                                    </CardContent>\n                                </Card>\n\n                                <Card className={settings.twoFactorMode === 'PASSWORD_PLUS_PASSKEY' ? 'border-primary' : ''}>\n                                    <CardContent className=\"p-4\">\n                                        <div className=\"flex items-start gap-4\">\n                                            <div className=\"p-2 rounded-lg bg-primary/10\">\n                                                <Lock className=\"h-6 w-6 text-primary\" />\n                                            </div>\n                                            <div className=\"space-y-1\">\n                                                <h5 className=\"font-medium\">Password + Passkey</h5>\n                                                <p className=\"text-sm text-muted-foreground\">\n                                                    When you sign in with your password, you&apos;ll also need to use a passkey\n                                                </p>\n                                            </div>\n                                        </div>\n                                    </CardContent>\n                                </Card>\n                            </motion.div>\n                        </div>\n                    </div>\n                </CardContent>\n            </Card>\n\n            <AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>\n                <AlertDialogContent>\n                    <AlertDialogHeader>\n                        <AlertDialogTitle>Update Security Settings</AlertDialogTitle>\n                        <AlertDialogDescription asChild>\n                            {pendingMode === 'NONE' ? (\n                                <div className=\"space-y-4\">\n                                    <div>You&apos;re about to disable additional security on your account.</div>\n                                    <div className=\"flex items-center gap-2 text-warning\">\n                                        <AlertTriangle className=\"h-4 w-4\" />\n                                        <span>This will reduce your account security</span>\n                                    </div>\n                                </div>\n                            ) : pendingMode ? (\n                                <div className=\"space-y-2\">\n                                    <div>You&apos;re about to enable {getSecurityModeDetails(pendingMode).title}.</div>\n                                    <div>This will require additional verification when signing in.</div>\n                                </div>\n                            ) : (\n                                <div>Updating security settings...</div>\n                            )}\n                        </AlertDialogDescription>\n                    </AlertDialogHeader>\n                    <AlertDialogFooter>\n                        <AlertDialogCancel disabled={isSaving}>Cancel</AlertDialogCancel>\n                        <AlertDialogAction\n                            onClick={confirmModeChange}\n                            disabled={isSaving}\n                            className={pendingMode === 'NONE' ? 'bg-warning text-warning-foreground' : ''}\n                        >\n                            {isSaving ? 'Updating...' : 'Confirm'}\n                        </AlertDialogAction>\n                    </AlertDialogFooter>\n                </AlertDialogContent>\n            </AlertDialog>\n        </>\n    );\n}"
  },
  {
    "path": "components/setup/TeamImportModal.tsx",
    "content": "// components/setup/TeamImportModal.tsx\n'use client';\n\nimport React, { useState, useCallback } from 'react';\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogHeader,\n    DialogTitle,\n} from '@/components/ui/dialog';\nimport { Button } from '@/components/ui/button';\nimport { Textarea } from '@/components/ui/textarea';\nimport { Badge } from '@/components/ui/badge';\nimport { Separator } from '@/components/ui/separator';\nimport { toast } from '@/hooks/use-toast';\nimport { FileText, Upload, CheckCircle, AlertCircle, Users } from 'lucide-react';\nimport { motion, AnimatePresence } from 'framer-motion';\n\ninterface TeamImportModalProps {\n    open: boolean;\n    onOpenChange: (open: boolean) => void;\n    onImport: (emails: Array<{ email: string; name?: string }>) => void;\n    existingEmails: string[];\n}\n\ninterface ParsedEmail {\n    email: string;\n    name?: string;\n    isValid: boolean;\n    isDuplicate: boolean;\n    error?: string;\n}\n\nconst TEMPLATE_EXAMPLES = {\n    plaintext: `alice@company.com\nbob@company.com\ncarol@company.com`,\n    csv: `email,name\nalice@company.com,Alice Smith\nbob@company.com,Bob Johnson\ncarol@company.com,Carol Davis`,\n    mixed: `alice@company.com Alice Smith\nbob@company.com\ncarol@company.com \"Carol Davis\"`\n};\n\nexport function TeamImportModal({\n                                    open,\n                                    onOpenChange,\n                                    onImport,\n                                    existingEmails\n                                }: TeamImportModalProps) {\n    const [inputText, setInputText] = useState('');\n    const [parsedEmails, setParsedEmails] = useState<ParsedEmail[]>([]);\n    const [isProcessing, setIsProcessing] = useState(false);\n\n    // Email validation regex\n    const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n    // Parse different input formats\n    const parseEmailInput = useCallback((text: string): ParsedEmail[] => {\n        if (!text.trim()) return [];\n\n        const lines = text.trim().split('\\n').filter(line => line.trim());\n        const results: ParsedEmail[] = [];\n        const existingEmailsLower = existingEmails.map(e => e.toLowerCase());\n        const seenEmails = new Set<string>();\n\n        for (const line of lines) {\n            const trimmedLine = line.trim();\n            if (!trimmedLine) continue;\n\n            // Try different parsing strategies\n            let email = '';\n            let name = '';\n            let error = '';\n\n            // Strategy 1: CSV format (email,name)\n            if (trimmedLine.includes(',')) {\n                const parts = trimmedLine.split(',').map(p => p.trim().replace(/\"/g, ''));\n                email = parts[0];\n                name = parts[1] || '';\n            }\n            // Strategy 2: Email with space-separated name\n            else if (trimmedLine.includes(' ')) {\n                const parts = trimmedLine.split(' ');\n                email = parts[0];\n                // Join remaining parts as name\n                name = parts.slice(1).join(' ').replace(/\"/g, '');\n            }\n            // Strategy 3: Just email\n            else {\n                email = trimmedLine;\n            }\n\n            // Validate email\n            const isValid = emailRegex.test(email);\n            if (!isValid) {\n                error = 'Invalid email format';\n            }\n\n            // Check for duplicates (case insensitive)\n            const emailLower = email.toLowerCase();\n            const isDuplicate = existingEmailsLower.includes(emailLower) || seenEmails.has(emailLower);\n\n            if (isDuplicate && isValid) {\n                error = 'Email already exists';\n            }\n\n            if (isValid) {\n                seenEmails.add(emailLower);\n            }\n\n            results.push({\n                email,\n                name: name || undefined,\n                isValid,\n                isDuplicate,\n                error\n            });\n        }\n\n        return results;\n    }, [existingEmails, emailRegex]);\n\n    // Handle input change with real-time parsing\n    const handleInputChange = useCallback((value: string) => {\n        setInputText(value);\n\n        if (value.trim()) {\n            setIsProcessing(true);\n            // Debounce parsing for better performance\n            const timeoutId = setTimeout(() => {\n                const parsed = parseEmailInput(value);\n                setParsedEmails(parsed);\n                setIsProcessing(false);\n            }, 300);\n\n            return () => clearTimeout(timeoutId);\n        } else {\n            setParsedEmails([]);\n            setIsProcessing(false);\n        }\n    }, [parseEmailInput]);\n\n    // Handle import\n    const handleImport = useCallback(() => {\n        const validEmails = parsedEmails.filter(p => p.isValid && !p.isDuplicate);\n\n        if (validEmails.length === 0) {\n            toast({\n                title: \"No valid emails\",\n                description: \"Please add some valid email addresses to import\",\n                variant: \"destructive\"\n            });\n            return;\n        }\n\n        onImport(validEmails.map(e => ({ email: e.email, name: e.name })));\n\n        toast({\n            title: \"Import successful! 🦖\",\n            description: `${validEmails.length} email${validEmails.length !== 1 ? 's' : ''} imported`,\n        });\n\n        // Reset and close\n        setInputText('');\n        setParsedEmails([]);\n        onOpenChange(false);\n    }, [parsedEmails, onImport, onOpenChange]);\n\n    // Insert template\n    const insertTemplate = useCallback((template: string) => {\n        setInputText(template);\n        handleInputChange(template);\n    }, [handleInputChange]);\n\n    // Stats\n    const validCount = parsedEmails.filter(p => p.isValid && !p.isDuplicate).length;\n    const invalidCount = parsedEmails.filter(p => !p.isValid).length;\n    const duplicateCount = parsedEmails.filter(p => p.isDuplicate && p.isValid).length;\n\n    return (\n        <Dialog open={open} onOpenChange={onOpenChange}>\n            <DialogContent className=\"max-w-2xl max-h-[80vh] flex flex-col\">\n                <DialogHeader>\n                    <DialogTitle className=\"flex items-center gap-2\">\n                        <Upload className=\"h-5 w-5\" />\n                        Import Team Email List\n                    </DialogTitle>\n                    <DialogDescription>\n                        Import multiple email addresses at once. Supports various formats including CSV and plain text.\n                    </DialogDescription>\n                </DialogHeader>\n\n                <div className=\"flex-1 space-y-4 overflow-hidden\">\n                    {/* Templates */}\n                    <div className=\"space-y-2\">\n                        <h4 className=\"text-sm font-medium\">Quick Templates:</h4>\n                        <div className=\"flex flex-wrap gap-2\">\n                            <Button\n                                variant=\"outline\"\n                                size=\"sm\"\n                                onClick={() => insertTemplate(TEMPLATE_EXAMPLES.plaintext)}\n                            >\n                                <FileText className=\"w-3 h-3 mr-1\" />\n                                Plain Text\n                            </Button>\n                            <Button\n                                variant=\"outline\"\n                                size=\"sm\"\n                                onClick={() => insertTemplate(TEMPLATE_EXAMPLES.csv)}\n                            >\n                                <FileText className=\"w-3 h-3 mr-1\" />\n                                CSV Format\n                            </Button>\n                            <Button\n                                variant=\"outline\"\n                                size=\"sm\"\n                                onClick={() => insertTemplate(TEMPLATE_EXAMPLES.mixed)}\n                            >\n                                <FileText className=\"w-3 h-3 mr-1\" />\n                                Mixed Format\n                            </Button>\n                        </div>\n                    </div>\n\n                    <Separator />\n\n                    {/* Input area */}\n                    <div className=\"space-y-2\">\n                        <label className=\"text-sm font-medium\">Email List:</label>\n                        <Textarea\n                            placeholder={`Enter email addresses (one per line):\n\nalice@company.com\nbob@company.com\ncarol@company.com\n\nOr use CSV format:\nemail,name\nalice@company.com,Alice Smith`}\n                            value={inputText}\n                            onChange={(e) => handleInputChange(e.target.value)}\n                            className=\"min-h-[150px] font-mono text-sm\"\n                        />\n                    </div>\n\n                    {/* Preview and stats */}\n                    <AnimatePresence>\n                        {(parsedEmails.length > 0 || isProcessing) && (\n                            <motion.div\n                                initial={{ opacity: 0, height: 0 }}\n                                animate={{ opacity: 1, height: 'auto' }}\n                                exit={{ opacity: 0, height: 0 }}\n                                className=\"space-y-3\"\n                            >\n                                {/* Stats */}\n                                <div className=\"flex items-center gap-4 p-3 bg-muted/50 rounded-lg\">\n                                    <div className=\"flex items-center gap-2\">\n                                        <CheckCircle className=\"w-4 h-4 text-green-600\" />\n                                        <span className=\"text-sm\">\n                      <strong>{validCount}</strong> valid\n                    </span>\n                                    </div>\n\n                                    {invalidCount > 0 && (\n                                        <div className=\"flex items-center gap-2\">\n                                            <AlertCircle className=\"w-4 h-4 text-red-600\" />\n                                            <span className=\"text-sm\">\n                        <strong>{invalidCount}</strong> invalid\n                      </span>\n                                        </div>\n                                    )}\n\n                                    {duplicateCount > 0 && (\n                                        <div className=\"flex items-center gap-2\">\n                                            <Users className=\"w-4 h-4 text-orange-600\" />\n                                            <span className=\"text-sm\">\n                        <strong>{duplicateCount}</strong> duplicate\n                      </span>\n                                        </div>\n                                    )}\n\n                                    {isProcessing && (\n                                        <div className=\"flex items-center gap-2\">\n                                            <motion.div\n                                                animate={{ rotate: 360 }}\n                                                transition={{ duration: 1, repeat: Infinity, ease: \"linear\" }}\n                                                className=\"w-4 h-4\"\n                                            >\n                                                ⚙️\n                                            </motion.div>\n                                            <span className=\"text-sm\">Processing...</span>\n                                        </div>\n                                    )}\n                                </div>\n\n                                {/* Preview list */}\n                                {parsedEmails.length > 0 && (\n                                    <div className=\"max-h-[200px] overflow-y-auto space-y-1 border rounded-lg p-2\">\n                                        {parsedEmails.map((parsed, index) => (\n                                            <motion.div\n                                                key={index}\n                                                initial={{ opacity: 0, x: -10 }}\n                                                animate={{ opacity: 1, x: 0 }}\n                                                transition={{ delay: index * 0.05 }}\n                                                className=\"flex items-center justify-between py-1 px-2 rounded text-sm\"\n                                            >\n                                                <div className=\"flex items-center gap-2 flex-1 min-w-0\">\n                          <span className=\"truncate\">\n                            {parsed.name ? `${parsed.name} (${parsed.email})` : parsed.email}\n                          </span>\n                                                </div>\n\n                                                <div className=\"flex items-center gap-1\">\n                                                    {parsed.isValid && !parsed.isDuplicate && (\n                                                        <Badge variant=\"secondary\" className=\"bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300\">\n                                                            Valid\n                                                        </Badge>\n                                                    )}\n\n                                                    {!parsed.isValid && (\n                                                        <Badge variant=\"destructive\">\n                                                            Invalid\n                                                        </Badge>\n                                                    )}\n\n                                                    {parsed.isDuplicate && parsed.isValid && (\n                                                        <Badge variant=\"outline\" className=\"text-orange-700 dark:text-orange-300\">\n                                                            Duplicate\n                                                        </Badge>\n                                                    )}\n                                                </div>\n                                            </motion.div>\n                                        ))}\n                                    </div>\n                                )}\n                            </motion.div>\n                        )}\n                    </AnimatePresence>\n                </div>\n\n                {/* Actions */}\n                <div className=\"flex items-center justify-between pt-4 border-t\">\n                    <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n                        Cancel\n                    </Button>\n\n                    <Button\n                        onClick={handleImport}\n                        disabled={validCount === 0 || isProcessing}\n                    >\n                        Import {validCount > 0 && `${validCount} Email${validCount !== 1 ? 's' : ''}`}\n                    </Button>\n                </div>\n\n                {/* Format help */}\n                <div className=\"mt-4 p-3 bg-blue-50 dark:bg-blue-950/20 rounded-lg\">\n                    <h5 className=\"text-sm font-medium text-blue-800 dark:text-blue-200 mb-2\">\n                        📋 Supported Formats:\n                    </h5>\n                    <div className=\"text-xs text-blue-700 dark:text-blue-300 space-y-1\">\n                        <div><strong>Plain text:</strong> One email per line</div>\n                        <div><strong>CSV:</strong> email,name format</div>\n                        <div><strong>Mixed:</strong> email followed by optional name</div>\n                        <div><strong>Note:</strong> Duplicate emails will be skipped automatically</div>\n                    </div>\n                </div>\n            </DialogContent>\n        </Dialog>\n    );\n}"
  },
  {
    "path": "components/setup/setup-context.tsx",
    "content": "'use client';\n\nimport React, { createContext, useContext, useState, useEffect, useCallback } from 'react';\nimport { useRouter } from 'next/navigation';\nimport { toast } from '@/hooks/use-toast';\n\nexport type SetupStep = 'welcome' | 'admin' | 'settings' | 'oauth' | 'team' | 'complete';\n\ninterface SetupContextType {\n    currentStep: SetupStep;\n    setCurrentStep: (step: SetupStep) => void;\n    completedSteps: SetupStep[];\n    isLoading: boolean;\n    goToNextStep: () => void;\n    goToPreviousStep: () => void;\n    skipCurrentStep: () => void;\n    isStepCompleted: (step: SetupStep) => boolean;\n    markStepCompleted: (step: SetupStep) => void;\n    checkSetupStatus: () => Promise<void>;\n}\n\nconst SetupContext = createContext<SetupContextType | undefined>(undefined);\n\nconst stepOrder: SetupStep[] = ['welcome', 'admin', 'settings', 'oauth', 'team', 'complete'];\n\nexport const SetupProvider: React.FC<{children: React.ReactNode}> = ({ children }) => {\n    const [currentStep, setCurrentStep] = useState<SetupStep>('welcome');\n    const [completedSteps, setCompletedSteps] = useState<SetupStep[]>([]);\n    const [isLoading, setIsLoading] = useState(true);\n    const router = useRouter();\n\n    const checkSetupStatus = async () => {\n        setIsLoading(true);\n        console.log('🔍 checkSetupStatus: Starting setup status check...');\n\n        try {\n            const response = await fetch('/api/setup');\n            if (!response.ok) {\n                throw new Error('Failed to check setup status');\n            }\n\n            const data = await response.json();\n            console.log('📊 checkSetupStatus: API response:', data);\n\n            if (data.isComplete) {\n                console.log('✅ checkSetupStatus: Setup is complete, redirecting to login');\n                router.replace('/login');\n                return;\n            }\n\n            // Use the completed steps from API (which now enforces proper order)\n            const completed: SetupStep[] = data.completedSteps || [];\n            console.log('📝 checkSetupStatus: Completed steps from API:', completed);\n            setCompletedSteps(completed);\n\n            // TRUST THE API's currentStep - don't recalculate it here!\n            const apiCurrentStep = data.currentStep as SetupStep;\n            console.log(`🎯 checkSetupStatus: Using API's current step: ${apiCurrentStep}`);\n            setCurrentStep(apiCurrentStep);\n\n        } catch (error) {\n            console.error('❌ checkSetupStatus: Error occurred:', error);\n            console.log('🎬 checkSetupStatus: Error fallback - setting to welcome');\n            setCurrentStep('welcome');\n            toast({\n                title: 'Error',\n                description: 'Failed to check setup status',\n                variant: 'destructive',\n            });\n        } finally {\n            setIsLoading(false);\n            console.log('🏁 checkSetupStatus: Finished setup status check');\n        }\n    };\n\n    useEffect(() => {\n        checkSetupStatus();\n    }, []);\n\n    const goToNextStep = () => {\n        const currentIndex = stepOrder.indexOf(currentStep);\n        if (currentIndex < stepOrder.length - 1) {\n            setCurrentStep(stepOrder[currentIndex + 1]);\n        }\n    };\n\n    const goToPreviousStep = () => {\n        const currentIndex = stepOrder.indexOf(currentStep);\n        if (currentIndex > 0) {\n            setCurrentStep(stepOrder[currentIndex - 1]);\n        }\n    };\n\n    const skipCurrentStep = useCallback(() => {\n        // Same as goToNextStep, but semantically different for skippable steps\n        goToNextStep();\n    }, [goToNextStep]);\n\n    const isStepCompleted = (step: SetupStep) => {\n        return completedSteps.includes(step);\n    };\n\n    const markStepCompleted = (step: SetupStep) => {\n        if (!completedSteps.includes(step)) {\n            setCompletedSteps([...completedSteps, step]);\n        }\n    };\n\n    return (\n        <SetupContext.Provider\n            value={{\n                currentStep,\n                setCurrentStep,\n                completedSteps,\n                isLoading,\n                goToNextStep,\n                goToPreviousStep,\n                skipCurrentStep,\n                isStepCompleted,\n                markStepCompleted,\n                checkSetupStatus\n            }}\n        >\n            {children}\n        </SetupContext.Provider>\n    );\n};\n\nexport const useSetup = () => {\n    const context = useContext(SetupContext);\n    if (context === undefined) {\n        throw new Error('useSetup must be used within a SetupProvider');\n    }\n    return context;\n};"
  },
  {
    "path": "components/setup/setup-step.tsx",
    "content": "'use client';\n\nimport React from 'react';\nimport {\n    Card,\n    CardContent,\n    CardDescription,\n    CardFooter,\n    CardHeader,\n    CardTitle,\n} from \"@/components/ui/card\";\nimport { Button } from '@/components/ui/button';\nimport { ArrowLeft, ArrowRight, Loader2 } from 'lucide-react';\nimport { cn } from '@/lib/utils';\n\ninterface SetupStepProps {\n    title: string;\n    description?: string;\n    icon?: React.ReactNode;\n    onNext?: () => void;\n    onBack?: () => void;\n    isLoading?: boolean;\n    isComplete?: boolean;\n    disableNext?: boolean;\n    disableBack?: boolean;\n    hideFooter?: boolean;\n    nextLabel?: string;\n    backLabel?: string;\n    className?: string;\n    children: React.ReactNode;\n}\n\nexport function SetupStep({\n                              title,\n                              description,\n                              icon,\n                              onNext,\n                              onBack,\n                              isLoading = false,\n                              isComplete = false,\n                              disableNext = false,\n                              disableBack = false,\n                              hideFooter = false,\n                              nextLabel = 'Continue',\n                              backLabel = 'Back',\n                              className,\n                              children,\n                          }: SetupStepProps) {\n    return (\n        <Card className={cn(\"w-full max-w-lg\", className)}>\n            <CardHeader>\n                {icon && <div className=\"flex justify-center mb-4\">{icon}</div>}\n                <CardTitle className=\"text-2xl\">{title}</CardTitle>\n                {description && <CardDescription>{description}</CardDescription>}\n            </CardHeader>\n            <CardContent>{children}</CardContent>\n            {!hideFooter && (\n                <CardFooter className=\"flex justify-between\">\n                    <Button\n                        variant=\"outline\"\n                        onClick={onBack}\n                        disabled={disableBack || isLoading || !onBack}\n                    >\n                        <ArrowLeft className=\"mr-2 h-4 w-4\" />\n                        {backLabel}\n                    </Button>\n                    <Button\n                        onClick={onNext}\n                        disabled={disableNext || isLoading || isComplete || !onNext}\n                    >\n                        {isLoading ? (\n                            <>\n                                <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                                Processing...\n                            </>\n                        ) : isComplete ? (\n                            'Completed'\n                        ) : (\n                            <>\n                                {nextLabel}\n                                <ArrowRight className=\"ml-2 h-4 w-4\" />\n                            </>\n                        )}\n                    </Button>\n                </CardFooter>\n            )}\n        </Card>\n    );\n}"
  },
  {
    "path": "components/setup/steps/admin-step.tsx",
    "content": "'use client';\n\nimport React, {useState, useEffect} from 'react';\nimport {zodResolver} from '@hookform/resolvers/zod';\nimport {useForm} from 'react-hook-form';\nimport {z} from 'zod';\nimport {SetupStep} from '@/components/setup/setup-step';\nimport {Label} from '@/components/ui/label';\nimport {Input} from '@/components/ui/input';\nimport {Button} from '@/components/ui/button';\nimport {useSetup} from '@/components/setup/setup-context';\nimport {toast} from '@/hooks/use-toast';\nimport {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from \"@/components/ui/tooltip\";\nimport {\n    Shield,\n    Eye,\n    EyeOff,\n    User,\n    Mail,\n    Lock,\n    AlertCircle,\n    Key\n} from 'lucide-react';\nimport {motion, AnimatePresence} from 'framer-motion';\nimport {cn} from '@/lib/utils';\n\ninterface AdminStepProps {\n    onNext: () => void;\n    onBack: () => void;\n}\n\nconst adminSchema = z.object({\n    name: z.string().min(2, 'Name must be at least 2 characters'),\n    email: z.string().email('Please enter a valid email'),\n    password: z.string().min(8, 'Password must be at least 8 characters'),\n    confirmPassword: z.string()\n}).refine((data) => data.password === data.confirmPassword, {\n    message: \"Passwords don't match\",\n    path: [\"confirmPassword\"],\n});\n\ntype AdminFormValues = z.infer<typeof adminSchema>;\n\nexport function AdminStep({onNext, onBack}: AdminStepProps) {\n    const [isSubmitting, setIsSubmitting] = useState(false);\n    const [showPassword, setShowPassword] = useState(false);\n    const [passwordStrength, setPasswordStrength] = useState(0);\n    const {markStepCompleted, isStepCompleted} = useSetup();\n    const isCompleted = isStepCompleted('admin');\n\n    const {\n        register,\n        handleSubmit,\n        watch,\n        formState: {errors, isValid}\n    } = useForm<AdminFormValues>({\n        resolver: zodResolver(adminSchema),\n        mode: 'onChange'\n    });\n\n    const password = watch('password', '');\n\n    // Calculate password strength (same logic as registration)\n    useEffect(() => {\n        if (!password) {\n            setPasswordStrength(0);\n            return;\n        }\n\n        let strength = 0;\n\n        // Length check\n        if (password.length >= 8) strength += 1;\n        if (password.length >= 12) strength += 1;\n\n        // Character variety\n        if (/[A-Z]/.test(password)) strength += 1;\n        if (/[a-z]/.test(password)) strength += 1;\n        if (/[0-9]/.test(password)) strength += 1;\n        if (/[^A-Za-z0-9]/.test(password)) strength += 1;\n\n        // Normalize to a scale of 0-3\n        setPasswordStrength(Math.min(3, Math.floor(strength / 2)));\n    }, [password]);\n\n    const onSubmit = async (data: AdminFormValues) => {\n        if (isCompleted) {\n            onNext();\n            return;\n        }\n\n        setIsSubmitting(true);\n        try {\n            const response = await fetch('/api/setup/admin', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify(data)\n            });\n\n            if (!response.ok) {\n                const errorData = await response.json();\n                throw new Error(errorData.error || 'Failed to create admin account');\n            }\n\n            markStepCompleted('admin');\n            toast({\n                title: 'Success',\n                description: 'Admin account created successfully',\n            });\n            onNext();\n        } catch (error) {\n            toast({\n                title: 'Error',\n                description: error instanceof Error ? error.message : 'Failed to create admin account',\n                variant: 'destructive',\n            });\n        } finally {\n            setIsSubmitting(false);\n        }\n    };\n\n    // Get strength label and color (same as registration)\n    const getStrengthLabel = () => {\n        if (!password) return '';\n        const labels = ['Weak', 'Fair', 'Good', 'Strong'];\n        return labels[passwordStrength];\n    };\n\n    const getStrengthColor = () => {\n        if (!password) return 'bg-muted';\n        const colors = ['bg-destructive', 'bg-orange-500', 'bg-yellow-500', 'bg-green-500'];\n        return colors[passwordStrength];\n    };\n\n    const togglePasswordVisibility = () => {\n        setShowPassword(!showPassword);\n    };\n\n    return (\n        <SetupStep\n            title=\"Create Admin Account\"\n            description=\"Set up your administrator account to manage the system\"\n            icon={<Shield className=\"h-10 w-10 text-primary\"/>}\n            onNext={isCompleted ? onNext : undefined}\n            onBack={onBack}\n            isLoading={isSubmitting}\n            isComplete={isCompleted}\n            hideFooter={!isCompleted}\n        >\n            <form id=\"adminForm\" onSubmit={handleSubmit(onSubmit)} className=\"space-y-4\">\n                {/* Name Field */}\n                <motion.div\n                    className=\"space-y-2\"\n                    initial={{x: -10, opacity: 0}}\n                    animate={{x: 0, opacity: 1}}\n                    transition={{duration: 0.3}}\n                >\n                    <Label htmlFor=\"name\">Full Name</Label>\n                    <div className=\"relative group\">\n                        <User\n                            className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors duration-200\"/>\n                        <Input\n                            id=\"name\"\n                            {...register('name')}\n                            type=\"text\"\n                            placeholder=\"John Doe\"\n                            autoComplete=\"name\"\n                            className={cn(\n                                \"h-12 pl-10 transition-all duration-200\",\n                                errors.name ? 'border-destructive focus-visible:ring-destructive/20' : 'focus-visible:ring-primary/20'\n                            )}\n                            autoFocus\n                            startIcon={<User/>}\n                        />\n                    </div>\n                    <AnimatePresence>\n                        {errors.name && (\n                            <motion.p\n                                className=\"text-sm text-destructive flex items-center gap-1 mt-1\"\n                                initial={{opacity: 0, height: 0, y: -10}}\n                                animate={{opacity: 1, height: 'auto', y: 0}}\n                                exit={{opacity: 0, height: 0, y: -10}}\n                            >\n                                <span className=\"inline-block\">⚠️</span>\n                                {errors.name.message}\n                            </motion.p>\n                        )}\n                    </AnimatePresence>\n                </motion.div>\n\n                {/* Email Field */}\n                <motion.div\n                    className=\"space-y-2\"\n                    initial={{x: -10, opacity: 0}}\n                    animate={{x: 0, opacity: 1}}\n                    transition={{duration: 0.3, delay: 0.1}}\n                >\n                    <Label htmlFor=\"email\">Email</Label>\n                    <div className=\"relative group\">\n                        <Mail\n                            className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors duration-200\"/>\n                        <Input\n                            id=\"email\"\n                            {...register('email')}\n                            type=\"email\"\n                            placeholder=\"admin@company.com\"\n                            autoComplete=\"email\"\n                            className={cn(\n                                \"h-12 pl-10 transition-all duration-200\",\n                                errors.email ? 'border-destructive focus-visible:ring-destructive/20' : 'focus-visible:ring-primary/20'\n                            )}\n                            startIcon={<Mail/>}\n                        />\n                    </div>\n                    <AnimatePresence>\n                        {errors.email && (\n                            <motion.p\n                                className=\"text-sm text-destructive flex items-center gap-1 mt-1\"\n                                initial={{opacity: 0, height: 0, y: -10}}\n                                animate={{opacity: 1, height: 'auto', y: 0}}\n                                exit={{opacity: 0, height: 0, y: -10}}\n                            >\n                                <span className=\"inline-block\">⚠️</span>\n                                {errors.email.message}\n                            </motion.p>\n                        )}\n                    </AnimatePresence>\n                </motion.div>\n\n                {/* Password Field */}\n                <motion.div\n                    className=\"space-y-2\"\n                    initial={{x: -10, opacity: 0}}\n                    animate={{x: 0, opacity: 1}}\n                    transition={{duration: 0.3, delay: 0.2}}\n                >\n                    <div className=\"flex justify-between items-center\">\n                        <Label htmlFor=\"password\">Password</Label>\n                        <TooltipProvider>\n                            <Tooltip>\n                                <TooltipTrigger asChild>\n                                    <Button variant=\"ghost\" size=\"sm\" className=\"h-6 w-6 p-0\">\n                                        <AlertCircle className=\"h-4 w-4 text-muted-foreground\"/>\n                                    </Button>\n                                </TooltipTrigger>\n                                <TooltipContent>\n                                    <p className=\"max-w-xs\">Password should be at least 8 characters. Strong passwords\n                                        include uppercase letters, numbers, and symbols.</p>\n                                </TooltipContent>\n                            </Tooltip>\n                        </TooltipProvider>\n                    </div>\n\n                    <div className=\"relative group\">\n                        <Lock\n                            className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors duration-200\"/>\n                        <Input\n                            id=\"password\"\n                            {...register('password')}\n                            type={showPassword ? 'text' : 'password'}\n                            placeholder=\"••••••••\"\n                            autoComplete=\"new-password\"\n                            className={cn(\n                                \"h-12 pl-10 pr-10 transition-all duration-200\",\n                                errors.password ? 'border-destructive focus-visible:ring-destructive/20' : 'focus-visible:ring-primary/20'\n                            )}\n                            startIcon={<Key/>}\n                        />\n                        <Button\n                            type=\"button\"\n                            variant=\"ghost\"\n                            size=\"sm\"\n                            className=\"absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent\"\n                            onClick={togglePasswordVisibility}\n                        >\n                            {showPassword ? (\n                                <EyeOff className=\"h-4 w-4 text-muted-foreground\"/>\n                            ) : (\n                                <Eye className=\"h-4 w-4 text-muted-foreground\"/>\n                            )}\n                        </Button>\n                    </div>\n\n                    {/* Password strength indicator */}\n                    {password && (\n                        <motion.div\n                            className=\"pt-1\"\n                            initial={{opacity: 0, height: 0}}\n                            animate={{opacity: 1, height: 'auto'}}\n                            transition={{duration: 0.2}}\n                        >\n                            <div className=\"flex justify-between items-center text-xs mb-1\">\n                                <span>Password strength:</span>\n                                <span className={cn(\n                                    passwordStrength === 0 ? \"text-destructive\" :\n                                        passwordStrength === 1 ? \"text-orange-500\" :\n                                            passwordStrength === 2 ? \"text-yellow-500\" :\n                                                \"text-green-500\"\n                                )}>\n                                    {getStrengthLabel()}\n                                </span>\n                            </div>\n                            <div className=\"h-1.5 w-full bg-muted rounded-full overflow-hidden flex\">\n                                <motion.div\n                                    className={cn(\"h-full transition-all duration-300 ease-out\", getStrengthColor())}\n                                    initial={{width: 0}}\n                                    animate={{width: `${(passwordStrength + 1) * 25}%`}}\n                                    transition={{duration: 0.3}}\n                                />\n                            </div>\n                        </motion.div>\n                    )}\n\n                    <AnimatePresence>\n                        {errors.password && (\n                            <motion.p\n                                className=\"text-sm text-destructive flex items-center gap-1 mt-1\"\n                                initial={{opacity: 0, height: 0, y: -10}}\n                                animate={{opacity: 1, height: 'auto', y: 0}}\n                                exit={{opacity: 0, height: 0, y: -10}}\n                            >\n                                <span className=\"inline-block\">⚠️</span>\n                                {errors.password.message}\n                            </motion.p>\n                        )}\n                    </AnimatePresence>\n                </motion.div>\n\n                {/* Confirm Password Field */}\n                <motion.div\n                    className=\"space-y-2\"\n                    initial={{x: -10, opacity: 0}}\n                    animate={{x: 0, opacity: 1}}\n                    transition={{duration: 0.3, delay: 0.3}}\n                >\n                    <Label htmlFor=\"confirmPassword\">Confirm Password</Label>\n                    <div className=\"relative\">\n                        <Lock\n                            className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground\"/>\n                        <Input\n                            id=\"confirmPassword\"\n                            {...register('confirmPassword')}\n                            type={showPassword ? 'text' : 'password'}\n                            placeholder=\"••••••••\"\n                            autoComplete=\"new-password\"\n                            className={cn(\n                                \"h-12 pl-10 transition-all duration-200\",\n                                errors.confirmPassword ? 'border-destructive focus-visible:ring-destructive/20' : 'focus-visible:ring-primary/20'\n                            )}\n                            startIcon={<Key/>}\n                        />\n                    </div>\n                    <AnimatePresence>\n                        {errors.confirmPassword && (\n                            <motion.p\n                                className=\"text-sm text-destructive flex items-center gap-1 mt-1\"\n                                initial={{opacity: 0, height: 0, y: -10}}\n                                animate={{opacity: 1, height: 'auto', y: 0}}\n                                exit={{opacity: 0, height: 0, y: -10}}\n                            >\n                                <span className=\"inline-block\">⚠️</span>\n                                {errors.confirmPassword.message}\n                            </motion.p>\n                        )}\n                    </AnimatePresence>\n                </motion.div>\n\n                {!isCompleted && (\n                    <motion.div\n                        className=\"pt-4\"\n                        initial={{y: 10, opacity: 0}}\n                        animate={{y: 0, opacity: 1}}\n                        transition={{duration: 0.3, delay: 0.4}}\n                    >\n                        <Button\n                            type=\"submit\"\n                            className={cn(\n                                \"w-full h-12 relative overflow-hidden transition-all duration-300\",\n                                isValid ? 'bg-primary hover:bg-primary/90' : 'bg-primary/70'\n                            )}\n                            disabled={isSubmitting || !isValid}\n                        >\n                            {isSubmitting ? (\n                                <span className=\"flex items-center gap-2\">\n                                    <motion.div\n                                        animate={{rotate: 360}}\n                                        transition={{duration: 1, repeat: Infinity, ease: \"linear\"}}\n                                        className=\"w-4 h-4 border-2 border-current border-t-transparent rounded-full\"\n                                    />\n                                    Creating account...\n                                </span>\n                            ) : (\n                                <>\n                                    <Shield className=\"mr-2 h-4 w-4\"/>\n                                    Create Admin Account\n                                </>\n                            )}\n\n                            {/* Shine effect for valid form */}\n                            {isValid && !isSubmitting && (\n                                <span\n                                    className=\"absolute right-0 top-0 h-full w-12 -skew-x-12 overflow-hidden flex justify-center items-center\">\n                                    <motion.div\n                                        className=\"bg-white/20 h-8 w-8 rounded-full\"\n                                        initial={{x: -100}}\n                                        animate={{x: 150}}\n                                        transition={{\n                                            repeat: Infinity,\n                                            duration: 2,\n                                            ease: \"easeInOut\",\n                                            repeatDelay: 1\n                                        }}\n                                    />\n                                </span>\n                            )}\n                        </Button>\n                    </motion.div>\n                )}\n            </form>\n        </SetupStep>\n    );\n}"
  },
  {
    "path": "components/setup/steps/completion-step.tsx",
    "content": "'use client';\n\nimport React from 'react';\nimport { SetupStep } from '@/components/setup/setup-step';\nimport { Button } from '@/components/ui/button';\nimport { useRouter } from 'next/navigation';\nimport { CheckCircle2, Bell, ArrowRight, Copy, Check } from 'lucide-react';\nimport { motion } from 'framer-motion';\nimport confetti from 'canvas-confetti';\n\n// Use a type alias instead of an interface ( migration )\ntype CompletionStepProps = Record<string, never>;\n\nexport function CompletionStep({}: CompletionStepProps) {\n    const router = useRouter();\n    const [copied, setCopied] = React.useState(false);\n\n    const envVariable = 'SETUP_COMPLETE=true';\n\n    const copyToClipboard = async () => {\n        try {\n            await navigator.clipboard.writeText(envVariable);\n            setCopied(true);\n            setTimeout(() => setCopied(false), 2000);\n        } catch (err) {\n            console.error('Failed to copy:', err);\n        }\n    };\n\n    // Trigger confetti on mount\n    React.useEffect(() => {\n        // Only run in browser\n        if (typeof window !== 'undefined') {\n            const duration = 3000;\n            const end = Date.now() + duration;\n\n            const frame = () => {\n                confetti({\n                    particleCount: 2,\n                    angle: 60,\n                    spread: 55,\n                    origin: { x: 0 },\n                    colors: ['#16a34a', '#3b82f6', '#8b5cf6']\n                });\n\n                confetti({\n                    particleCount: 2,\n                    angle: 120,\n                    spread: 55,\n                    origin: { x: 1 },\n                    colors: ['#16a34a', '#3b82f6', '#8b5cf6']\n                });\n\n                if (Date.now() < end) {\n                    requestAnimationFrame(frame);\n                }\n            };\n\n            frame();\n        }\n    }, []);\n\n    return (\n        <SetupStep\n            title=\"Setup Complete!\"\n            description=\"Your system has been configured successfully\"\n            icon={\n                <motion.div\n                    initial={{ scale: 0 }}\n                    animate={{ scale: 1 }}\n                    transition={{\n                        type: 'spring',\n                        stiffness: 200,\n                        damping: 10,\n                        delay: 0.2\n                    }}\n                >\n                    <CheckCircle2 className=\"h-16 w-16 text-primary\" />\n                </motion.div>\n            }\n            hideFooter={true}\n        >\n            <div className=\"space-y-6\">\n                <motion.div\n                    initial={{ opacity: 0, y: 20 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    transition={{ delay: 0.5 }}\n                    className=\"space-y-4\"\n                >\n                    <div className=\"p-4 border border-green-200 bg-green-50 dark:bg-green-900/20 dark:border-green-900 rounded-lg\">\n                        <p className=\"text-green-800 dark:text-green-300\">\n                            Your setup is complete! You can now access all features of Changerawr.\n                        </p>\n                    </div>\n\n                    <div className=\"flex items-start space-x-4 p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-900 rounded-lg\">\n                        <Bell className=\"h-6 w-6 mt-1 text-amber-600 dark:text-amber-400\" />\n                        <div className=\"flex-1\">\n                            <h3 className=\"font-medium text-amber-900 dark:text-amber-100\">Important: Update Your Environment</h3>\n                            <p className=\"text-sm text-amber-800 dark:text-amber-200 mt-1\">\n                                Add the following variable to your <code className=\"px-1 py-0.5 bg-amber-100 dark:bg-amber-900 rounded text-xs\">.env</code> file:\n                            </p>\n\n                            <div className=\"mt-3 flex items-center gap-2\">\n                                <code className=\"flex-1 px-3 py-2 bg-amber-100 dark:bg-amber-900 text-amber-900 dark:text-amber-100 rounded font-mono text-sm\">\n                                    {envVariable}\n                                </code>\n                                <Button\n                                    onClick={copyToClipboard}\n                                    variant=\"outline\"\n                                    size=\"sm\"\n                                    className=\"shrink-0\"\n                                >\n                                    {copied ? (\n                                        <>\n                                            <Check className=\"h-4 w-4 mr-1\" />\n                                            Copied!\n                                        </>\n                                    ) : (\n                                        <>\n                                            <Copy className=\"h-4 w-4 mr-1\" />\n                                            Copy\n                                        </>\n                                    )}\n                                </Button>\n                            </div>\n\n                            <p className=\"text-xs text-amber-700 dark:text-amber-300 mt-2\">\n                                After adding this to your .env file, restart your service for the changes to take effect.\n                            </p>\n                        </div>\n                    </div>\n                </motion.div>\n\n                <motion.div\n                    initial={{ opacity: 0 }}\n                    animate={{ opacity: 1 }}\n                    transition={{ delay: 1 }}\n                >\n                    <Button\n                        onClick={() => router.push('/login')}\n                        className=\"w-full\"\n                        size=\"lg\"\n                    >\n                        Go to Login\n                        <ArrowRight className=\"ml-2 h-4 w-4\" />\n                    </Button>\n                </motion.div>\n            </div>\n        </SetupStep>\n    );\n}"
  },
  {
    "path": "components/setup/steps/oauth-step.tsx",
    "content": "'use client';\n\nimport React, {useState, useEffect} from 'react';\nimport {SetupStep} from '@/components/setup/setup-step';\nimport {Label} from '@/components/ui/label';\nimport {Input} from '@/components/ui/input';\nimport {Switch} from '@/components/ui/switch';\nimport {Alert, AlertDescription} from '@/components/ui/alert';\nimport {Badge} from '@/components/ui/badge';\nimport {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs';\nimport {\n    Select,\n    SelectContent,\n    SelectItem,\n    SelectTrigger,\n    SelectValue\n} from '@/components/ui/select';\nimport {Button} from '@/components/ui/button';\nimport {useSetup} from '@/components/setup/setup-context';\nimport {toast} from '@/hooks/use-toast';\nimport {\n    Shield,\n    Copy,\n    Zap,\n    CheckCircle,\n    AlertCircle,\n    Settings,\n    Loader2,\n    Server,\n    Wand2\n} from 'lucide-react';\nimport {motion, AnimatePresence} from 'framer-motion';\n\ninterface OAuthStepProps {\n    onNext: () => void;\n    onBack: () => void;\n}\n\ninterface AutoSetupStatus {\n    available: boolean;\n    connected: boolean;\n    serverInfo: {\n        serverUrl?: string;\n        hasApiKey: boolean;\n        isConfigured: boolean;\n    };\n    error?: string;\n}\n\ninterface AutoSetupResult {\n    success: boolean;\n    client?: {\n        id: string;\n        name: string;\n        clientId: string;\n        redirectUri: string;\n    };\n    error?: string;\n    details?: string;\n}\n\nexport function OAuthStep({onNext, onBack}: OAuthStepProps) {\n    const [isSubmitting, setIsSubmitting] = useState(false);\n    const {markStepCompleted, isStepCompleted} = useSetup();\n    const isCompleted = isStepCompleted('oauth');\n\n    // Auto setup state\n    const [autoSetupStatus, setAutoSetupStatus] = useState<AutoSetupStatus | null>(null);\n    const [isCheckingAutoSetup, setIsCheckingAutoSetup] = useState(true);\n    const [autoSetupAppName, setAutoSetupAppName] = useState('');\n\n    // Manual setup state\n    const [enableOAuth, setEnableOAuth] = useState(false);\n    const [providerType, setProviderType] = useState('easypanel');\n    const [baseUrl, setBaseUrl] = useState('');\n    const [clientId, setClientId] = useState('');\n    const [clientSecret, setClientSecret] = useState('');\n\n    // Setup mode\n    const [setupMode, setSetupMode] = useState<'auto' | 'manual'>('auto');\n    const [error, setError] = useState('');\n\n    // Get the callback URL for the selected provider\n    const getCallbackUrl = (provider: string) => {\n        const baseUrl = typeof window !== 'undefined'\n            ? (process.env.NEXT_PUBLIC_APP_URL || window.location.origin)\n            : 'http://localhost:3000';\n        return `${baseUrl}/api/auth/oauth/callback/${provider.toLowerCase()}`;\n    };\n\n    // Check auto setup availability on mount\n    useEffect(() => {\n        const checkAutoSetup = async () => {\n            try {\n                const response = await fetch('/api/setup/oauth/auto');\n                if (response.ok) {\n                    const data: AutoSetupStatus = await response.json();\n                    setAutoSetupStatus(data);\n\n                    // Pre-fill base URL if available\n                    if (data.serverInfo.serverUrl) {\n                        setBaseUrl(data.serverInfo.serverUrl);\n                    }\n\n                    // Default to manual if auto setup isn't available\n                    if (!data.available) {\n                        setSetupMode('manual');\n                    }\n                } else {\n                    setAutoSetupStatus({\n                        available: false,\n                        connected: false,\n                        serverInfo: {\n                            hasApiKey: false,\n                            isConfigured: false\n                        },\n                        error: 'Failed to check auto setup status'\n                    });\n                    setSetupMode('manual');\n                }\n            } catch (err) {\n                console.error('Error checking auto setup:', err);\n                setAutoSetupStatus({\n                    available: false,\n                    connected: false,\n                    serverInfo: {\n                        hasApiKey: false,\n                        isConfigured: false\n                    },\n                    error: 'Network error'\n                });\n                setSetupMode('manual');\n            } finally {\n                setIsCheckingAutoSetup(false);\n            }\n        };\n\n        checkAutoSetup();\n    }, []);\n\n    const handleAutoSetup = async () => {\n        setIsSubmitting(true);\n        setError('');\n\n        try {\n            const response = await fetch('/api/setup/oauth/auto', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({\n                    appName: autoSetupAppName.trim() || undefined,\n                    persistent: true\n                })\n            });\n\n            const result: AutoSetupResult = await response.json();\n\n            if (!result.success) {\n                throw new Error(result.details || result.error || 'Auto setup failed');\n            }\n\n            markStepCompleted('oauth');\n            toast({\n                title: 'Success! 🎉',\n                description: `OAuth client \"${result.client!.name}\" created and configured automatically`,\n            });\n\n            onNext();\n        } catch (err) {\n            const errorMessage = err instanceof Error ? err.message : 'Auto setup failed';\n            setError(errorMessage);\n            toast({\n                title: 'Auto Setup Failed',\n                description: errorMessage,\n                variant: 'destructive',\n            });\n        } finally {\n            setIsSubmitting(false);\n        }\n    };\n\n    const handleManualSubmit = async (e: React.FormEvent) => {\n        e.preventDefault();\n\n        if (!enableOAuth) {\n            // Skip OAuth setup\n            markStepCompleted('oauth');\n            onNext();\n            return;\n        }\n\n        setIsSubmitting(true);\n        setError('');\n\n        try {\n            // Validate inputs\n            if (!baseUrl.trim() || !clientId.trim() || !clientSecret.trim()) {\n                throw new Error('All fields are required');\n            }\n\n            if (!baseUrl.startsWith('http://') && !baseUrl.startsWith('https://')) {\n                throw new Error('Base URL must start with http:// or https://');\n            }\n\n            // Save OAuth provider configuration\n            const response = await fetch('/api/setup/oauth', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({\n                    provider: providerType,\n                    baseUrl,\n                    clientId,\n                    clientSecret\n                })\n            });\n\n            if (!response.ok) {\n                const data = await response.json();\n                throw new Error(data.error || 'Failed to set up OAuth provider');\n            }\n\n            markStepCompleted('oauth');\n            toast({\n                title: 'Success',\n                description: 'OAuth provider configured successfully',\n            });\n\n            onNext();\n        } catch (err) {\n            const errorMessage = err instanceof Error ? err.message : 'An error occurred';\n            setError(errorMessage);\n            toast({\n                title: 'Error',\n                description: errorMessage,\n                variant: 'destructive',\n            });\n        } finally {\n            setIsSubmitting(false);\n        }\n    };\n\n    const handleSkip = () => {\n        markStepCompleted('oauth');\n        onNext();\n    };\n\n    const copyToClipboard = (text: string) => {\n        navigator.clipboard.writeText(text);\n        toast({\n            title: 'Copied',\n            description: 'Callback URL copied to clipboard',\n        });\n    };\n\n    if (isCheckingAutoSetup) {\n        return (\n            <SetupStep\n                title=\"Single Sign-On Setup\"\n                description=\"Checking automatic setup options...\"\n                icon={<Loader2 className=\"h-10 w-10 text-primary animate-spin\"/>}\n                onBack={onBack}\n                hideFooter={true}\n            >\n                <div className=\"text-center py-8\">\n                    <p className=\"text-muted-foreground\">\n                        Checking OAuth server connection...\n                    </p>\n                </div>\n            </SetupStep>\n        );\n    }\n\n    return (\n        <SetupStep\n            title=\"Single Sign-On Setup\"\n            description=\"Configure OAuth for single sign-on with your authentication provider\"\n            icon={<Shield className=\"h-10 w-10 text-primary\"/>}\n            onNext={isCompleted ? onNext : undefined}\n            onBack={onBack}\n            isLoading={isSubmitting}\n            isComplete={isCompleted}\n            hideFooter={!isCompleted}\n        >\n            <div className=\"space-y-6\">\n                {/* Setup Mode Selection */}\n                <Tabs\n                    value={setupMode}\n                    onValueChange={(value) => setSetupMode(value as 'auto' | 'manual')}\n                    className=\"w-full\"\n                >\n                    <TabsList className=\"grid w-full grid-cols-3\">\n                        <TabsTrigger\n                            value=\"auto\"\n                            disabled={!autoSetupStatus?.available}\n                            className=\"flex items-center gap-2\"\n                        >\n                            <Wand2 className=\"h-4 w-4\"/>\n                            Automatic\n                            {autoSetupStatus?.available && autoSetupStatus.connected && (\n                                <Badge variant=\"success\" className=\"ml-1 h-4 text-xs\">Ready</Badge>\n                            )}\n                        </TabsTrigger>\n                        <TabsTrigger value=\"manual\" className=\"flex items-center gap-2\">\n                            <Settings className=\"h-4 w-4\"/>\n                            Manual\n                        </TabsTrigger>\n                        <TabsTrigger value=\"skip\" className=\"flex items-center gap-2\">\n                            Skip Setup\n                        </TabsTrigger>\n                    </TabsList>\n\n                    {/* Automatic Setup */}\n                    <TabsContent value=\"auto\" className=\"space-y-4\">\n                        {autoSetupStatus?.available ? (\n                            <motion.div\n                                initial={{opacity: 0, y: 10}}\n                                animate={{opacity: 1, y: 0}}\n                                className=\"space-y-4\"\n                            >\n                                {/* Status Banner */}\n                                <Alert hasIcon={false}\n                                       className={autoSetupStatus.connected ? 'border-green-200 bg-green-50 dark:bg-green-950/20' : 'border-yellow-200 bg-yellow-50 dark:bg-yellow-950/20'}>\n                                    <div className=\"flex items-center gap-2\">\n                                        {autoSetupStatus.connected ? (\n                                            <CheckCircle className=\"h-4 w-4 text-green-600\"/>\n                                        ) : (\n                                            <AlertCircle className=\"h-4 w-4 text-yellow-600\"/>\n                                        )}\n                                        <AlertDescription className=\"font-medium\">\n                                            {autoSetupStatus.connected ? (\n                                                'OAuth server connected and ready for automatic setup'\n                                            ) : (\n                                                `OAuth server configuration detected but ${autoSetupStatus.error || 'connection failed'}`\n                                            )}\n                                        </AlertDescription>\n                                    </div>\n                                </Alert>\n\n                                {/* Server Info */}\n                                <div className=\"bg-muted/50 rounded-lg p-4 space-y-3\">\n                                    <div className=\"flex items-center gap-2\">\n                                        <Server className=\"h-4 w-4\"/>\n                                        <span className=\"font-medium\">OAuth Server Configuration</span>\n                                    </div>\n                                    <div className=\"space-y-2 text-sm\">\n                                        <div className=\"flex justify-between\">\n                                            <span className=\"text-muted-foreground\">Server URL:</span>\n                                            <span\n                                                className=\"font-mono\">{autoSetupStatus.serverInfo.serverUrl || 'Not configured'}</span>\n                                        </div>\n                                        <div className=\"flex justify-between\">\n                                            <span className=\"text-muted-foreground\">API Key:</span>\n                                            <Badge\n                                                variant={autoSetupStatus.serverInfo.hasApiKey ? 'success' : 'destructive'}>\n                                                {autoSetupStatus.serverInfo.hasApiKey ? 'Configured' : 'Missing'}\n                                            </Badge>\n                                        </div>\n                                        <div className=\"flex justify-between\">\n                                            <span className=\"text-muted-foreground\">Connection:</span>\n                                            <Badge variant={autoSetupStatus.connected ? 'success' : 'destructive'}>\n                                                {autoSetupStatus.connected ? 'Connected' : 'Failed'}\n                                            </Badge>\n                                        </div>\n                                    </div>\n                                </div>\n\n                                {autoSetupStatus.connected && (\n                                    <>\n                                        {/* App Name Input */}\n                                        <div className=\"space-y-2\">\n                                            <Label htmlFor=\"autoAppName\">Application Name (Optional)</Label>\n                                            <Input\n                                                id=\"autoAppName\"\n                                                value={autoSetupAppName}\n                                                onChange={(e) => setAutoSetupAppName(e.target.value)}\n                                                placeholder=\"Changerawr Instance\"\n                                                className=\"h-12\"\n                                            />\n                                            <p className=\"text-xs text-muted-foreground\">\n                                                Custom name for your OAuth client. If empty, a unique name will be\n                                                generated.\n                                            </p>\n                                        </div>\n\n                                        {/* Auto Setup Info */}\n                                        <div className=\"bg-blue-50 dark:bg-blue-950/20 rounded-lg p-4\">\n                                            <h4 className=\"font-medium text-blue-800 dark:text-blue-200 mb-2 flex items-center gap-2\">\n                                                <Zap className=\"h-4 w-4\"/>\n                                                What happens during automatic setup:\n                                            </h4>\n                                            <ul className=\"text-sm text-blue-700 dark:text-blue-300 space-y-1\">\n                                                <li>• Creates OAuth client on your server automatically</li>\n                                                <li>• Uses fixed redirect URI: <code\n                                                    className=\"bg-blue-100 dark:bg-blue-900/50 px-1 rounded text-xs\">{getCallbackUrl('easypanel')}</code>\n                                                </li>\n                                                <li>• Configures required scopes: openid, profile, email</li>\n                                                <li>• Sets up local OAuth provider configuration</li>\n                                                <li>• No manual client creation or secret copying needed</li>\n                                            </ul>\n                                        </div>\n\n                                        {/* Debug section for troubleshooting */}\n                                        {process.env.NODE_ENV === 'development' && (\n                                            <div className=\"bg-muted/30 rounded-lg p-3\">\n                                                <h4 className=\"text-sm font-medium mb-2\">Debug Info:</h4>\n                                                <div className=\"text-xs space-y-1\">\n                                                    <div>App Name: {autoSetupAppName || '(empty)'}</div>\n                                                    <div>Server\n                                                        URL: {autoSetupStatus.serverInfo.serverUrl || 'not set'}</div>\n                                                    <div>Has API\n                                                        Key: {autoSetupStatus.serverInfo.hasApiKey ? 'Yes' : 'No'}</div>\n                                                    <div>Connected: {autoSetupStatus.connected ? 'Yes' : 'No'}</div>\n                                                </div>\n                                                <Button\n                                                    type=\"button\"\n                                                    variant=\"outline\"\n                                                    size=\"sm\"\n                                                    className=\"mt-2\"\n                                                    onClick={async () => {\n                                                        try {\n                                                            const response = await fetch('/api/setup/oauth/debug');\n                                                            const data = await response.json();\n                                                            console.log('🦖 Debug info:', data);\n                                                            toast({\n                                                                title: \"Debug info logged\",\n                                                                description: \"Check browser console for details\"\n                                                            });\n                                                        } catch (err) {\n                                                            console.error('Debug failed:', err);\n                                                        }\n                                                    }}\n                                                >\n                                                    Get Debug Info\n                                                </Button>\n                                            </div>\n                                        )}\n\n                                        {/* Error Display */}\n                                        {error && (\n                                            <Alert variant=\"destructive\">\n                                                <AlertDescription>{error}</AlertDescription>\n                                            </Alert>\n                                        )}\n\n                                        {/* Auto Setup Button */}\n                                        {!isCompleted && (\n                                            <Button\n                                                onClick={handleAutoSetup}\n                                                disabled={isSubmitting}\n                                                className=\"w-full\"\n                                                size=\"lg\"\n                                            >\n                                                {isSubmitting ? (\n                                                    <>\n                                                        <Loader2 className=\"mr-2 h-4 w-4 animate-spin\"/>\n                                                        Setting up OAuth automatically...\n                                                    </>\n                                                ) : (\n                                                    <>\n                                                        <Zap className=\"mr-2 h-4 w-4\"/>\n                                                        Set Up OAuth Automatically\n                                                    </>\n                                                )}\n                                            </Button>\n                                        )}\n                                    </>\n                                )}\n                            </motion.div>\n                        ) : (\n                            <Alert variant=\"destructive\">\n                                <AlertCircle className=\"h-4 w-4\"/>\n                                <AlertDescription>\n                                    Automatic setup is not available. Please ensure CHR_EPOA2_SERV_URL and\n                                    CHR_EPOA2_SERV_API_KEY environment variables are configured.\n                                </AlertDescription>\n                            </Alert>\n                        )}\n                    </TabsContent>\n\n                    {/* Manual Setup */}\n                    <TabsContent value=\"manual\" className=\"space-y-4\">\n                        <form onSubmit={handleManualSubmit} className=\"space-y-6\">\n                            <div className=\"flex items-center space-x-2\">\n                                <Switch\n                                    id=\"enableOAuth\"\n                                    checked={enableOAuth}\n                                    onCheckedChange={setEnableOAuth}\n                                />\n                                <Label htmlFor=\"enableOAuth\">Enable Single Sign-On</Label>\n                            </div>\n\n                            <AnimatePresence>\n                                {enableOAuth && (\n                                    <motion.div\n                                        initial={{opacity: 0, height: 0}}\n                                        animate={{opacity: 1, height: 'auto'}}\n                                        exit={{opacity: 0, height: 0}}\n                                        transition={{duration: 0.3}}\n                                        className=\"space-y-4\"\n                                    >\n                                        <div className=\"space-y-2\">\n                                            <Label htmlFor=\"providerType\">Provider</Label>\n                                            <Select\n                                                value={providerType}\n                                                onValueChange={setProviderType}\n                                            >\n                                                <SelectTrigger>\n                                                    <SelectValue placeholder=\"Select a provider\"/>\n                                                </SelectTrigger>\n                                                <SelectContent>\n                                                    <SelectItem value=\"easypanel\">\n                                                        <div className=\"flex items-center gap-2\">\n                                                            <div className=\"w-2 h-2 bg-blue-500 rounded-full\"></div>\n                                                            Easypanel\n                                                        </div>\n                                                    </SelectItem>\n                                                    <SelectItem value=\"pocketid\">\n                                                        <div className=\"flex items-center gap-2\">\n                                                            <div className=\"w-2 h-2 bg-green-500 rounded-full\"></div>\n                                                            PocketID\n                                                        </div>\n                                                    </SelectItem>\n                                                </SelectContent>\n                                            </Select>\n                                            <p className=\"text-xs text-muted-foreground\">\n                                                {providerType === 'easypanel'\n                                                    ? 'OAuth provider for Easypanel authentication servers'\n                                                    : providerType === 'pocketid'\n                                                        ? 'OpenID Connect provider for PocketID authentication'\n                                                        : 'Choose your authentication provider type'\n                                                }\n                                            </p>\n                                        </div>\n\n                                        {/* Callback URL Display */}\n                                        <div className=\"space-y-2 border rounded-md p-4 bg-muted/30\">\n                                            <Label>Callback URL (Redirect URI)</Label>\n                                            <div className=\"flex items-center gap-2\">\n                                                <code\n                                                    className=\"flex-1 p-2 text-xs bg-background rounded border overflow-x-auto\">\n                                                    {getCallbackUrl(providerType)}\n                                                </code>\n                                                <Button\n                                                    type=\"button\"\n                                                    variant=\"outline\"\n                                                    size=\"sm\"\n                                                    onClick={() => copyToClipboard(getCallbackUrl(providerType))}\n                                                >\n                                                    <Copy className=\"h-4 w-4\"/>\n                                                </Button>\n                                            </div>\n                                            <p className=\"text-xs text-muted-foreground\">\n                                                Configure your {providerType === 'pocketid' ? 'PocketID' : 'Easypanel'} server to use this URL as the redirect URI.\n                                            </p>\n                                        </div>\n\n                                        <div className=\"space-y-2\">\n                                            <Label htmlFor=\"baseUrl\">\n                                                {providerType === 'pocketid' ? 'PocketID Server URL' : 'Authentication Server URL'}\n                                            </Label>\n                                            <Input\n                                                id=\"baseUrl\"\n                                                value={baseUrl}\n                                                onChange={(e) => setBaseUrl(e.target.value)}\n                                                placeholder={\n                                                    providerType === 'pocketid'\n                                                        ? 'https://id.yourserver.com'\n                                                        : 'https://auth.example.com'\n                                                }\n                                                className=\"h-12\"\n                                            />\n                                            <p className=\"text-xs text-muted-foreground\">\n                                                {providerType === 'pocketid'\n                                                    ? 'The base URL of your PocketID server instance.'\n                                                    : 'The base URL of your authentication server.'\n                                                }\n                                            </p>\n                                        </div>\n\n                                        <div className=\"space-y-2\">\n                                            <Label htmlFor=\"clientId\">Client ID</Label>\n                                            <Input\n                                                id=\"clientId\"\n                                                value={clientId}\n                                                onChange={(e) => setClientId(e.target.value)}\n                                                placeholder=\"client_id\"\n                                                className=\"h-12\"\n                                            />\n                                        </div>\n\n                                        <div className=\"space-y-2\">\n                                            <Label htmlFor=\"clientSecret\">Client Secret</Label>\n                                            <Input\n                                                id=\"clientSecret\"\n                                                value={clientSecret}\n                                                onChange={(e) => setClientSecret(e.target.value)}\n                                                type=\"password\"\n                                                placeholder=\"client_secret\"\n                                                className=\"h-12\"\n                                            />\n                                        </div>\n                                    </motion.div>\n                                )}\n                            </AnimatePresence>\n\n                            {error && (\n                                <Alert variant=\"destructive\">\n                                    <AlertCircle className=\"h-4 w-4\"/>\n                                    <AlertDescription>{error}</AlertDescription>\n                                </Alert>\n                            )}\n\n                            {!isCompleted && (\n                                <Button\n                                    type=\"submit\"\n                                    className=\"w-full\"\n                                    disabled={isSubmitting}\n                                    size=\"lg\"\n                                >\n                                    {isSubmitting ? (\n                                        <>\n                                            <Loader2 className=\"mr-2 h-4 w-4 animate-spin\"/>\n                                            Saving Configuration...\n                                        </>\n                                    ) : enableOAuth ? (\n                                        'Save OAuth Configuration'\n                                    ) : (\n                                        'Skip OAuth Setup'\n                                    )}\n                                </Button>\n                            )}\n                        </form>\n                    </TabsContent>\n\n                    {/* Skip Setup */}\n                    <TabsContent value=\"skip\" className=\"space-y-4\">\n                        <Alert>\n                            <AlertDescription>\n                                You can skip OAuth setup for now and configure it later in the admin settings.\n                                Users will need to create accounts manually without single sign-on.\n                            </AlertDescription>\n                        </Alert>\n\n                        {!isCompleted && (\n                            <Button\n                                onClick={handleSkip}\n                                variant=\"outline\"\n                                className=\"w-full\"\n                                size=\"lg\"\n                            >\n                                Skip OAuth Setup\n                            </Button>\n                        )}\n                    </TabsContent>\n                </Tabs>\n            </div>\n        </SetupStep>\n    );\n}"
  },
  {
    "path": "components/setup/steps/settings-step.tsx",
    "content": "// components/setup/steps/settings-step.tsx\n'use client';\n\nimport React, { useState } from 'react';\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport { useForm } from 'react-hook-form';\nimport { z } from 'zod';\nimport { SetupStep } from '@/components/setup/setup-step';\nimport { Label } from '@/components/ui/label';\nimport { Input } from '@/components/ui/input';\nimport { Switch } from '@/components/ui/switch';\nimport { Separator } from '@/components/ui/separator';\nimport { useSetup } from '@/components/setup/setup-context';\nimport { toast } from '@/hooks/use-toast';\nimport { Settings, Globe } from 'lucide-react';\nimport { SearchableSelect } from '@/components/ui/searchable-select';\nimport { getTimezonesByRegion } from '@/lib/constants/timezones';\n\ninterface SettingsStepProps {\n    onNext: () => void;\n    onBack: () => void;\n}\n\nconst settingsSchema = z.object({\n    defaultInvitationExpiry: z.number().min(1).max(30).default(7),\n    requireApprovalForChangelogs: z.boolean().default(true),\n    maxChangelogEntriesPerProject: z.number().min(10).max(10000).default(100),\n    enableAnalytics: z.boolean().default(true),\n    enableNotifications: z.boolean().default(true),\n    timezone: z.string().min(1).max(100).default('UTC'),\n});\n\ntype SettingsFormValues = z.infer<typeof settingsSchema>;\n\nexport function SettingsStep({ onNext, onBack }: SettingsStepProps) {\n    const [isSubmitting, setIsSubmitting] = useState(false);\n    const { markStepCompleted, isStepCompleted } = useSetup();\n    const isCompleted = isStepCompleted('settings');\n\n    const {\n        register,\n        handleSubmit,\n        setValue,\n        watch,\n        formState: { errors }\n    } = useForm<SettingsFormValues>({\n        resolver: zodResolver(settingsSchema),\n        defaultValues: {\n            defaultInvitationExpiry: 7,\n            requireApprovalForChangelogs: true,\n            maxChangelogEntriesPerProject: 100,\n            enableAnalytics: true,\n            enableNotifications: true,\n            timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',\n        }\n    });\n\n    // For the switches we need to watch the values\n    const requireApprovalForChangelogs = watch('requireApprovalForChangelogs');\n    const enableAnalytics = watch('enableAnalytics');\n    const enableNotifications = watch('enableNotifications');\n\n    const onSubmit = async (data: SettingsFormValues) => {\n        if (isCompleted) {\n            onNext();\n            return;\n        }\n\n        setIsSubmitting(true);\n        try {\n            const response = await fetch('/api/setup/settings', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify(data)\n            });\n\n            if (!response.ok) {\n                const errorData = await response.json();\n                throw new Error(errorData.error || 'Failed to save system settings');\n            }\n\n            markStepCompleted('settings');\n            toast({\n                title: 'Success',\n                description: 'System settings saved successfully',\n            });\n            onNext();\n        } catch (error) {\n            toast({\n                title: 'Error',\n                description: error instanceof Error ? error.message : 'Failed to save system settings',\n                variant: 'destructive',\n            });\n        } finally {\n            setIsSubmitting(false);\n        }\n    };\n\n    return (\n        <SetupStep\n            title=\"System Settings\"\n            description=\"Configure your system's default behavior\"\n            icon={<Settings className=\"h-10 w-10 text-primary\" />}\n            onNext={isCompleted ? onNext : undefined}\n            onBack={onBack}\n            isLoading={isSubmitting}\n            isComplete={isCompleted}\n            hideFooter={!isCompleted}\n        >\n            <form id=\"settingsForm\" onSubmit={handleSubmit(onSubmit)} className=\"space-y-6\">\n                <div className=\"space-y-2\">\n                    <Label htmlFor=\"defaultInvitationExpiry\">\n                        Default Invitation Expiry (days)\n                    </Label>\n                    <Input\n                        id=\"defaultInvitationExpiry\"\n                        type=\"number\"\n                        {...register('defaultInvitationExpiry', { valueAsNumber: true })}\n                    />\n                    {errors.defaultInvitationExpiry && (\n                        <p className=\"text-sm text-destructive\">\n                            {errors.defaultInvitationExpiry.message}\n                        </p>\n                    )}\n                </div>\n\n                <div className=\"space-y-2\">\n                    <Label htmlFor=\"maxChangelogEntriesPerProject\">\n                        Max Changelog Entries per Project\n                    </Label>\n                    <Input\n                        id=\"maxChangelogEntriesPerProject\"\n                        type=\"number\"\n                        {...register('maxChangelogEntriesPerProject', { valueAsNumber: true })}\n                    />\n                    {errors.maxChangelogEntriesPerProject && (\n                        <p className=\"text-sm text-destructive\">\n                            {errors.maxChangelogEntriesPerProject.message}\n                        </p>\n                    )}\n                </div>\n\n                <div className=\"space-y-2\">\n                    <Label className=\"flex items-center gap-2\">\n                        <Globe className=\"h-4 w-4 text-muted-foreground\" />\n                        Timezone\n                    </Label>\n                    <SearchableSelect\n                        value={watch('timezone')}\n                        onValueChange={(value) => setValue('timezone', value)}\n                        placeholder=\"Select timezone\"\n                        searchPlaceholder=\"Search timezones...\"\n                        groups={Object.entries(getTimezonesByRegion()).map(([region, tzs]) => ({\n                            heading: region,\n                            items: tzs.map(tz => ({\n                                value: tz.value,\n                                label: `${tz.label} (${tz.value})`,\n                                searchValue: `${tz.label} ${tz.value} ${region}`,\n                            })),\n                        }))}\n                    />\n                    <p className=\"text-sm text-muted-foreground\">\n                        Used for date-based version templates and scheduling\n                    </p>\n                </div>\n\n                <Separator />\n\n                <div className=\"space-y-4\">\n                    <div className=\"flex items-center justify-between\">\n                        <div className=\"space-y-0.5\">\n                            <Label>Require Changelog Approval</Label>\n                            <p className=\"text-sm text-muted-foreground\">\n                                Require approval for new changelog entries\n                            </p>\n                        </div>\n                        <Switch\n                            checked={requireApprovalForChangelogs}\n                            onCheckedChange={(checked) => setValue('requireApprovalForChangelogs', checked)}\n                        />\n                    </div>\n\n                    <div className=\"flex items-center justify-between\">\n                        <div className=\"space-y-0.5\">\n                            <Label>Enable Analytics</Label>\n                            <p className=\"text-sm text-muted-foreground\">\n                                Collect usage statistics and analytics\n                            </p>\n                        </div>\n                        <Switch\n                            checked={enableAnalytics}\n                            onCheckedChange={(checked) => setValue('enableAnalytics', checked)}\n                        />\n                    </div>\n\n                    <div className=\"flex items-center justify-between\">\n                        <div className=\"space-y-0.5\">\n                            <Label>Enable Notifications</Label>\n                            <p className=\"text-sm text-muted-foreground\">\n                                Send notifications for important events\n                            </p>\n                        </div>\n                        <Switch\n                            checked={enableNotifications}\n                            onCheckedChange={(checked) => setValue('enableNotifications', checked)}\n                        />\n                    </div>\n                </div>\n\n                {!isCompleted && (\n                    <div className=\"pt-4\">\n                        <button\n                            type=\"submit\"\n                            className=\"w-full bg-primary text-primary-foreground hover:bg-primary/90 py-2 px-4 rounded-md font-medium\"\n                            disabled={isSubmitting}\n                        >\n                            {isSubmitting ? 'Saving Settings...' : 'Save System Settings'}\n                        </button>\n                    </div>\n                )}\n            </form>\n        </SetupStep>\n    );\n}"
  },
  {
    "path": "components/setup/steps/team-step.tsx",
    "content": "// components/setup/steps/team-step.tsx\n'use client';\n\nimport React, {useCallback, useState} from 'react';\nimport {AnimatePresence, motion} from 'framer-motion';\nimport {Button} from '@/components/ui/button';\nimport {Input} from '@/components/ui/input';\nimport {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card';\nimport {Badge} from '@/components/ui/badge';\nimport {Separator} from '@/components/ui/separator';\nimport {toast} from '@/hooks/use-toast';\nimport {Check, Clock, Copy, Download, ExternalLink, FileText, Mail, Plus, Trash2, Users} from 'lucide-react';\nimport {cn} from '@/lib/utils';\nimport {TeamImportModal} from '@/components/setup/TeamImportModal';\n\ninterface TeamInviteStepProps {\n    onNext: () => void;\n    onBack: () => void;\n    onSkip?: () => void;\n}\n\ninterface Invitation {\n    id: string;\n    email: string;\n    name?: string;\n    token: string;\n    link: string;\n    expiresAt: Date;\n    used: boolean;\n}\n\ninterface TeamInviteState {\n    currentEmail: string;\n    currentName: string;\n    invitations: Invitation[];\n    isGenerating: boolean;\n    allLinksGenerated: boolean;\n    copiedLinks: Set<string>;\n}\n\nexport function TeamStep({onNext, onSkip}: TeamInviteStepProps) {\n    const [state, setState] = useState<TeamInviteState>({\n        currentEmail: '',\n        currentName: '',\n        invitations: [],\n        isGenerating: false,\n        allLinksGenerated: false,\n        copiedLinks: new Set()\n    });\n\n    const [showImportModal, setShowImportModal] = useState(false);\n\n    // Email validation\n    const isValidEmail = useCallback((email: string): boolean => {\n        const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n        return emailRegex.test(email.trim());\n    }, []);\n\n    // Check if email already exists\n    const emailExists = useCallback((email: string): boolean => {\n        return state.invitations.some(inv =>\n            inv.email.toLowerCase() === email.toLowerCase()\n        );\n    }, [state.invitations]);\n\n    // Generate invitation link\n    const generateInvitationLink = useCallback(async (email: string, name?: string): Promise<Invitation | null> => {\n        try {\n            const response = await fetch('/api/setup/invitations', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({\n                    email: email.trim(),\n                    name: name?.trim(),\n                    role: 'STAFF' // Always STAFF for team invitations\n                })\n            });\n\n            if (!response.ok) {\n                const error = await response.json();\n                throw new Error(error.message || 'Failed to generate invitation');\n            }\n\n            const data = await response.json();\n\n            return {\n                id: data.id,\n                email: email.trim(),\n                name: name?.trim(),\n                token: data.token,\n                link: `${window.location.origin}/register/${data.token}`,\n                expiresAt: new Date(data.expiresAt),\n                used: false\n            };\n        } catch (error) {\n            console.error('Error generating invitation:', error);\n            toast({\n                title: \"Generation failed\",\n                description: error instanceof Error ? error.message : \"Failed to generate invitation link\",\n                variant: \"destructive\"\n            });\n            return null;\n        }\n    }, []);\n\n    // Add single invitation\n    const handleAddInvitation = useCallback(async () => {\n        const email = state.currentEmail.trim();\n        const name = state.currentName.trim();\n\n        if (!email) {\n            toast({\n                title: \"Email required\",\n                description: \"Please enter an email address\",\n                variant: \"destructive\"\n            });\n            return;\n        }\n\n        if (!isValidEmail(email)) {\n            toast({\n                title: \"Invalid email\",\n                description: \"Please enter a valid email address\",\n                variant: \"destructive\"\n            });\n            return;\n        }\n\n        if (emailExists(email)) {\n            toast({\n                title: \"Email already added\",\n                description: \"This email has already been added to the list\",\n                variant: \"destructive\"\n            });\n            return;\n        }\n\n        setState(prev => ({...prev, isGenerating: true}));\n\n        const invitation = await generateInvitationLink(email, name);\n\n        if (invitation) {\n            setState(prev => ({\n                ...prev,\n                invitations: [...prev.invitations, invitation],\n                currentEmail: '',\n                currentName: '',\n                isGenerating: false\n            }));\n\n            toast({\n                title: \"Invitation created! 🦖\",\n                description: `Link generated for ${email}`,\n            });\n        } else {\n            setState(prev => ({...prev, isGenerating: false}));\n        }\n    }, [state.currentEmail, state.currentName, isValidEmail, emailExists, generateInvitationLink]);\n\n    // Remove invitation\n    const handleRemoveInvitation = useCallback((id: string) => {\n        setState(prev => ({\n            ...prev,\n            invitations: prev.invitations.filter(inv => inv.id !== id)\n        }));\n    }, []);\n\n    // Copy single link\n    const handleCopyLink = useCallback(async (invitation: Invitation) => {\n        try {\n            await navigator.clipboard.writeText(invitation.link);\n            setState(prev => ({\n                ...prev,\n                copiedLinks: new Set([...prev.copiedLinks, invitation.id])\n            }));\n\n            toast({\n                title: \"Link copied! 📋\",\n                description: `Invitation link for ${invitation.email} copied to clipboard`,\n            });\n\n            // Reset copied state after 2 seconds\n            setTimeout(() => {\n                setState(prev => ({\n                    ...prev,\n                    copiedLinks: new Set([...prev.copiedLinks].filter(id => id !== invitation.id))\n                }));\n            }, 2000);\n        } catch {\n            toast({\n                title: \"Copy failed\",\n                description: \"Failed to copy link to clipboard\",\n                variant: \"destructive\"\n            });\n        }\n    }, []);\n\n    // Copy all links\n    const handleCopyAllLinks = useCallback(async () => {\n        if (state.invitations.length === 0) return;\n\n        const allLinks = state.invitations\n            .map(inv => `${inv.email}: ${inv.link}`)\n            .join('\\n');\n\n        try {\n            await navigator.clipboard.writeText(allLinks);\n            toast({\n                title: \"All links copied! 🎉\",\n                description: `${state.invitations.length} invitation links copied to clipboard`,\n            });\n        } catch {\n            toast({\n                title: \"Copy failed\",\n                description: \"Failed to copy links to clipboard\",\n                variant: \"destructive\"\n            });\n        }\n    }, [state.invitations]);\n\n    // Export as CSV\n    const handleExportCSV = useCallback(() => {\n        if (state.invitations.length === 0) return;\n\n        const csvContent = [\n            'email,name,link,expires',\n            ...state.invitations.map(inv =>\n                `\"${inv.email}\",\"${inv.name || ''}\",\"${inv.link}\",\"${inv.expiresAt.toLocaleDateString()}\"`\n            )\n        ].join('\\n');\n\n        const blob = new Blob([csvContent], {type: 'text/csv'});\n        const url = URL.createObjectURL(blob);\n        const a = document.createElement('a');\n        a.href = url;\n        a.download = 'team-invitations.csv';\n        document.body.appendChild(a);\n        a.click();\n        document.body.removeChild(a);\n        URL.revokeObjectURL(url);\n\n        toast({\n            title: \"Export complete! 📤\",\n            description: \"Team invitations exported as CSV\",\n        });\n    }, [state.invitations]);\n\n    // Handle bulk import from modal\n    const handleBulkImport = useCallback(async (emailData: Array<{ email: string; name?: string }>) => {\n        setState(prev => ({...prev, isGenerating: true}));\n\n        const results = await Promise.all(\n            emailData.map(data => generateInvitationLink(data.email, data.name))\n        );\n\n        const successfulInvitations = results.filter(Boolean) as Invitation[];\n\n        setState(prev => ({\n            ...prev,\n            invitations: [...prev.invitations, ...successfulInvitations],\n            isGenerating: false\n        }));\n\n        toast({\n            title: \"Bulk import complete! 🦖\",\n            description: `${successfulInvitations.length} invitation links generated`,\n        });\n    }, [generateInvitationLink]);\n    const handleKeyPress = useCallback((e: React.KeyboardEvent) => {\n        if (e.key === 'Enter' && state.currentEmail.trim()) {\n            e.preventDefault();\n            handleAddInvitation();\n        }\n    }, [state.currentEmail, handleAddInvitation]);\n\n    return (\n        <div className=\"w-full max-w-2xl mx-auto space-y-6\">\n            {/* Header with T-Rex theme */}\n            <motion.div\n                initial={{opacity: 0, y: -20}}\n                animate={{opacity: 1, y: 0}}\n                className=\"text-center space-y-4\"\n            >\n                <div className=\"text-6xl\">🦖</div>\n                <div className=\"space-y-2\">\n                    <h1 className=\"text-3xl font-bold\">Don&apos;t Go Solo!</h1>\n                    <p className=\"text-lg text-muted-foreground\">\n                        Even T-Rex&apos;s knew teamwork beats tiny arms!\n                    </p>\n                    <p className=\"text-sm text-muted-foreground\">\n                        Don&apos;t let small arms hold you back - invite your pack!\n                    </p>\n                </div>\n            </motion.div>\n\n            {/* Add Invitation Form */}\n            <Card>\n                <CardHeader>\n                    <CardTitle className=\"flex items-center gap-2\">\n                        <Users className=\"h-5 w-5\"/>\n                        Create Invitations\n                    </CardTitle>\n                </CardHeader>\n                <CardContent className=\"space-y-4\">\n                    <div className=\"grid grid-cols-2 gap-3\">\n                        <div>\n                            <Input\n                                type=\"email\"\n                                placeholder=\"team@company.com\"\n                                value={state.currentEmail}\n                                onChange={(e) => setState(prev => ({...prev, currentEmail: e.target.value}))}\n                                onKeyPress={handleKeyPress}\n                                disabled={state.isGenerating}\n                            />\n                        </div>\n                        <div>\n                            <Input\n                                placeholder=\"Name (optional)\"\n                                value={state.currentName}\n                                onChange={(e) => setState(prev => ({...prev, currentName: e.target.value}))}\n                                onKeyPress={handleKeyPress}\n                                disabled={state.isGenerating}\n                            />\n                        </div>\n                    </div>\n\n                    <div className=\"flex gap-2\">\n                        <Button\n                            onClick={handleAddInvitation}\n                            disabled={!state.currentEmail.trim() || state.isGenerating}\n                            className=\"flex-1\"\n                        >\n                            {state.isGenerating ? (\n                                <>\n                                    <motion.div\n                                        animate={{rotate: 360}}\n                                        transition={{duration: 1, repeat: Infinity, ease: \"linear\"}}\n                                        className=\"w-4 h-4 mr-2\"\n                                    >\n                                        ⚙️\n                                    </motion.div>\n                                    Generating...\n                                </>\n                            ) : (\n                                <>\n                                    <Plus className=\"w-4 h-4 mr-2\"/>\n                                    Generate Link\n                                </>\n                            )}\n                        </Button>\n\n                        <Button\n                            variant=\"outline\"\n                            onClick={() => setShowImportModal(true)}\n                            disabled={state.isGenerating}\n                        >\n                            <FileText className=\"w-4 h-4 mr-2\"/>\n                            Import List\n                        </Button>\n                    </div>\n                </CardContent>\n            </Card>\n\n            {/* Generated Invitations */}\n            <AnimatePresence>\n                {state.invitations.length > 0 && (\n                    <motion.div\n                        initial={{opacity: 0, height: 0}}\n                        animate={{opacity: 1, height: 'auto'}}\n                        exit={{opacity: 0, height: 0}}\n                        className=\"space-y-4\"\n                    >\n                        <div className=\"flex items-center justify-between\">\n                            <h3 className=\"text-lg font-semibold flex items-center gap-2\">\n                                🎉 Your pack is ready! Share these links:\n                            </h3>\n                            <Badge variant=\"secondary\">\n                                {state.invitations.length} invitation{state.invitations.length !== 1 ? 's' : ''}\n                            </Badge>\n                        </div>\n\n                        {/* Bulk actions */}\n                        <div className=\"flex gap-2\">\n                            <Button\n                                variant=\"outline\"\n                                size=\"sm\"\n                                onClick={handleCopyAllLinks}\n                                className=\"flex-1\"\n                            >\n                                <Copy className=\"w-4 h-4 mr-2\"/>\n                                Copy All Links\n                            </Button>\n                            <Button\n                                variant=\"outline\"\n                                size=\"sm\"\n                                onClick={handleExportCSV}\n                            >\n                                <Download className=\"w-4 h-4 mr-2\"/>\n                                Export CSV\n                            </Button>\n                        </div>\n\n                        {/* Individual invitation cards */}\n                        <div className=\"space-y-3\">\n                            {state.invitations.map((invitation, index) => (\n                                <motion.div\n                                    key={invitation.id}\n                                    initial={{opacity: 0, x: -20}}\n                                    animate={{opacity: 1, x: 0}}\n                                    transition={{delay: index * 0.1}}\n                                >\n                                    <Card>\n                                        <CardContent className=\"p-4\">\n                                            <div className=\"flex items-center justify-between\">\n                                                <div className=\"flex-1 min-w-0\">\n                                                    <div className=\"flex items-center gap-2 mb-1\">\n                                                        <Mail className=\"w-4 h-4 text-muted-foreground\"/>\n                                                        <span className=\"font-medium truncate\">\n                              {invitation.name || invitation.email}\n                            </span>\n                                                        {invitation.name && (\n                                                            <span className=\"text-sm text-muted-foreground\">\n                                ({invitation.email})\n                              </span>\n                                                        )}\n                                                    </div>\n\n                                                    <div\n                                                        className=\"flex items-center gap-4 text-xs text-muted-foreground\">\n                                                        <div className=\"flex items-center gap-1\">\n                                                            <Clock className=\"w-3 h-3\"/>\n                                                            Expires: {invitation.expiresAt.toLocaleDateString()}\n                                                        </div>\n                                                        <div className=\"flex items-center gap-1\">\n                                                            <ExternalLink className=\"w-3 h-3\"/>\n                                                            STAFF access\n                                                        </div>\n                                                    </div>\n\n                                                    <div\n                                                        className=\"mt-2 p-2 bg-muted rounded text-xs font-mono truncate\">\n                                                        {invitation.link}\n                                                    </div>\n                                                </div>\n\n                                                <div className=\"flex items-center gap-2 ml-4\">\n                                                    <Button\n                                                        variant=\"outline\"\n                                                        size=\"sm\"\n                                                        onClick={() => handleCopyLink(invitation)}\n                                                        className={cn(\n                                                            \"transition-colors\",\n                                                            state.copiedLinks.has(invitation.id) && \"bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400\"\n                                                        )}\n                                                    >\n                                                        {state.copiedLinks.has(invitation.id) ? (\n                                                            <Check className=\"w-4 h-4\"/>\n                                                        ) : (\n                                                            <Copy className=\"w-4 h-4\"/>\n                                                        )}\n                                                    </Button>\n\n                                                    <Button\n                                                        variant=\"outline\"\n                                                        size=\"sm\"\n                                                        onClick={() => handleRemoveInvitation(invitation.id)}\n                                                        className=\"text-destructive hover:text-destructive\"\n                                                    >\n                                                        <Trash2 className=\"w-4 h-4\"/>\n                                                    </Button>\n                                                </div>\n                                            </div>\n                                        </CardContent>\n                                    </Card>\n                                </motion.div>\n                            ))}\n                        </div>\n                    </motion.div>\n                )}\n            </AnimatePresence>\n\n            <Separator/>\n\n            {/* Navigation */}\n            <div className=\"flex items-center justify-between pt-4\">\n                {onSkip && (\n                    <Button variant=\"ghost\" onClick={onSkip}>\n                        Skip for Now\n                    </Button>\n                )}\n\n                <div className=\"flex gap-2\">\n                    <Button onClick={onNext}>\n                        Continue →\n                    </Button>\n                </div>\n            </div>\n\n            {/* How to Share Instructions */}\n            {state.invitations.length > 0 && (\n                <motion.div\n                    initial={{opacity: 0}}\n                    animate={{opacity: 1}}\n                    className=\"mt-6 p-4 bg-green-50 dark:bg-green-950/20 rounded-lg\"\n                >\n                    <h4 className=\"font-medium text-green-800 dark:text-green-200 mb-2 flex items-center gap-2\">\n                        📋 How to Share Your Links:\n                    </h4>\n                    <ul className=\"text-sm text-green-700 dark:text-green-300 space-y-1\">\n                        <li>• Copy each link and send to your team members</li>\n                        <li>• They&apos;ll create accounts and join your project automatically</li>\n                        <li>• Links expire in 7 days for security</li>\n                        <li>• Each person gets STAFF access to create and edit entries</li>\n                    </ul>\n                </motion.div>\n            )}\n\n            {/* Import Modal */}\n            <TeamImportModal\n                open={showImportModal}\n                onOpenChange={setShowImportModal}\n                onImport={handleBulkImport}\n                existingEmails={state.invitations.map(inv => inv.email)}\n            />\n        </div>\n    );\n}"
  },
  {
    "path": "components/setup/steps/welcome-step.tsx",
    "content": "'use client';\n\nimport React from 'react';\nimport { User, Settings, Shield } from 'lucide-react';\nimport { SetupStep } from '@/components/setup/setup-step';\n\ninterface WelcomeStepProps {\n    onNext: () => void;\n}\n\nexport function WelcomeStep({ onNext }: WelcomeStepProps) {\n    return (\n        <SetupStep\n            title=\"Welcome to Changerawr\"\n            description=\"Let's get your system set up in just a few steps\"\n            onNext={onNext}\n            hideFooter={false}\n            disableBack={true}\n            nextLabel=\"Get Started\"\n        >\n            <div className=\"space-y-4\">\n                <div className=\"flex items-start space-x-4 p-4 bg-muted rounded-lg\">\n                    <User className=\"h-6 w-6 mt-1 text-primary\" />\n                    <div>\n                        <h3 className=\"font-medium\">Admin Account</h3>\n                        <p className=\"text-sm text-muted-foreground\">\n                            Create your administrator account\n                        </p>\n                    </div>\n                </div>\n\n                <div className=\"flex items-start space-x-4 p-4 bg-muted rounded-lg\">\n                    <Settings className=\"h-6 w-6 mt-1 text-primary\" />\n                    <div>\n                        <h3 className=\"font-medium\">System Settings</h3>\n                        <p className=\"text-sm text-muted-foreground\">\n                            Configure your system preferences\n                        </p>\n                    </div>\n                </div>\n\n                <div className=\"flex items-start space-x-4 p-4 bg-muted rounded-lg\">\n                    <Shield className=\"h-6 w-6 mt-1 text-primary\" />\n                    <div>\n                        <h3 className=\"font-medium\">Security</h3>\n                        <p className=\"text-sm text-muted-foreground\">\n                            Set up your security preferences\n                        </p>\n                    </div>\n                </div>\n            </div>\n        </SetupStep>\n    );\n}"
  },
  {
    "path": "components/sso/ProviderLogo.tsx",
    "content": "import React from \"react\";\n\ninterface ProviderLogoProps {\n    providerName: string\n    size?: \"sm\" | \"md\" | \"lg\"\n}\n\nexport const ProviderLogo: React.FC<ProviderLogoProps> = ({providerName, size = \"md\"}) => {\n    // Calculate size classes based on the size prop\n    const sizeClasses = {\n        sm: \"w-6 h-6\",\n        md: \"w-8 h-8\",\n        lg: \"w-10 h-10\"\n    }\n\n    const iconSizes = {\n        sm: 14,\n        md: 18,\n        lg: 20\n    }\n\n    // Normalize provider name for lookup\n    const normalizedName = providerName.toLowerCase()\n\n    // Render provider logo based on the name\n    if (normalizedName === 'easypanel') {\n        return (\n            <div className={`${sizeClasses[size]} rounded-md flex items-center justify-center text-primary`}>\n                <svg width=\"100%\" height=\"100%\" viewBox=\"0 0 84 83\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n                    <g clipPath=\"url(#clip0_3198_32507)\">\n                        <path\n                            d=\"M40.4278 56.5316C37.7545 56.5316 35.2145 55.3644 33.4736 53.3358L11.3931 27.6051L2.22541 49.3065C0.863584 52.5302 1.45665 56.2479 3.75384 58.8878L21.5371 79.3243C23.2775 81.3246 25.7988 82.4727 28.45 82.4727H54.9487C58.6367 82.4727 61.965 80.262 63.3953 76.8631L71.9496 56.5316H40.4278Z\"\n                            fill=\"url(#paint0_linear_3198_32507)\"/>\n                        <path\n                            d=\"M43.5229 25.941C46.1906 25.941 48.7259 27.1035 50.4666 29.125L72.6346 54.8677L81.7368 33.1564C83.0861 29.9374 82.4897 26.2312 80.1984 23.5981L62.4038 3.14831C60.6635 1.14828 58.1423 2.83976e-05 55.491 2.58119e-05L29.0241 0C25.3203 -3.61217e-06 21.9806 2.22967 20.5606 5.65052L12.1382 25.941H43.5229Z\"\n                            fill=\"url(#paint1_linear_3198_32507)\"/>\n                    </g>\n                    <defs>\n                        <linearGradient id=\"paint0_linear_3198_32507\" x1=\"38.7226\" y1=\"24.3596\" x2=\"39.2942\" y2=\"94.996\"\n                                        gradientUnits=\"userSpaceOnUse\">\n                            <stop stopColor=\"#0BA864\"/>\n                            <stop offset=\"1\" stopColor=\"#19BFBF\"/>\n                        </linearGradient>\n                        <linearGradient id=\"paint1_linear_3198_32507\" x1=\"50.7816\" y1=\"-3.24546\" x2=\"51.3544\"\n                                        y2=\"67.3909\" gradientUnits=\"userSpaceOnUse\">\n                            <stop stopColor=\"#0BA864\"/>\n                            <stop offset=\"1\" stopColor=\"#19BFBF\"/>\n                        </linearGradient>\n                        <clipPath id=\"clip0_3198_32507\">\n                            <rect width=\"100%\" height=\"100%\" fill=\"white\"/>\n                        </clipPath>\n                    </defs>\n                </svg>\n            </div>\n        )\n    } else if (normalizedName === 'github') {\n        return (\n            <div className={`${sizeClasses[size]} rounded-md bg-slate-900 flex items-center justify-center text-white`}>\n                <svg viewBox=\"0 0 24 24\" width={iconSizes[size]} height={iconSizes[size]} stroke=\"currentColor\"\n                     strokeWidth=\"2\" fill=\"none\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n                    <path\n                        d=\"M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22\"/>\n                </svg>\n            </div>\n        )\n    } else if (normalizedName === 'google') {\n        return (\n            <div className={`${sizeClasses[size]} rounded-md bg-white border flex items-center justify-center`}>\n                <svg viewBox=\"0 0 24 24\" width={iconSizes[size]} height={iconSizes[size]}>\n                    <path\n                        d=\"M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z\"\n                        fill=\"#4285F4\"/>\n                </svg>\n            </div>\n        )\n    } else if (normalizedName === 'auth0') {\n        return (\n            <div className={`${sizeClasses[size]} rounded-md bg-orange-50 flex items-center justify-center`}>\n                <div\n                    className={`${size === \"sm\" ? \"w-3 h-3\" : size === \"md\" ? \"w-4 h-4\" : \"w-6 h-6\"} rounded-full bg-orange-500`}></div>\n            </div>\n        )\n    } else if (normalizedName === 'okta') {\n        return (\n            <div\n                className={`${sizeClasses[size]} rounded-md bg-blue-50 flex items-center justify-center text-blue-600`}>\n                <svg viewBox=\"0 0 24 24\" width={iconSizes[size]} height={iconSizes[size]} stroke=\"currentColor\"\n                     strokeWidth=\"2\" fill=\"none\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n                    <circle cx=\"12\" cy=\"12\" r=\"10\"/>\n                    <circle cx=\"12\" cy=\"12\" r=\"4\"/>\n                </svg>\n            </div>\n        )\n    } else if (normalizedName === 'pocketid') {\n        return (\n            <div\n                className={`${sizeClasses[size]} rounded-md bg-blue-50 flex items-center justify-center text-blue-600`}>\n                <svg viewBox=\"0 0 24 24\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" width={iconSizes[size]} height={iconSizes[size]}>\n                    <g transform=\"scale(0.125)\">\n                        <path d=\"M0 0 C1.22240387 -0.00852997 2.44480774 -0.01705994 3.70425415 -0.02584839 C5.0560304 -0.02514938 6.40780655 -0.02422633 7.75958252 -0.02310181 C9.18560132 -0.02908737 10.61161687 -0.03589298 12.03762817 -0.04345703 C15.90972313 -0.06112467 19.78172968 -0.06581282 23.65386105 -0.0670886 C26.07480474 -0.06851069 28.495726 -0.07278145 30.91666412 -0.07808304 C39.36789882 -0.09658807 47.81907043 -0.10475334 56.27032471 -0.10317993 C64.13881545 -0.10197942 72.0070315 -0.12306266 79.87545276 -0.15466726 C86.63739901 -0.1808562 93.39925571 -0.19154608 100.16125226 -0.19026911 C104.19690178 -0.18976011 108.23230733 -0.19540854 112.26790619 -0.21662521 C116.06668745 -0.23605112 119.8649932 -0.23610277 123.66379166 -0.22159195 C125.05369133 -0.21950789 126.4436211 -0.22418075 127.83346939 -0.23631287 C139.69821631 -0.33312596 146.28346497 2.07529934 155.04962158 10.17758179 C157.75025927 13.02736712 159.33021801 15.7316911 161.03009033 19.26742554 C161.38680124 19.8713274 161.74351215 20.47522926 162.11103249 21.09743118 C163.3042125 23.91465743 163.28786842 26.17883227 163.29751587 29.23733521 C163.31031082 31.07094101 163.31031082 31.07094101 163.32336426 32.94158936 C163.32266525 34.29336561 163.3217422 35.64514176 163.32061768 36.99691772 C163.32660323 38.42293652 163.33340885 39.84895208 163.3409729 41.27496338 C163.35864054 45.14705833 163.36332869 49.01906489 163.36460447 52.89119625 C163.36602656 55.31213995 163.37029732 57.73306121 163.37559891 60.15399933 C163.39410394 68.60523402 163.40226921 77.05640563 163.4006958 85.50765991 C163.39949529 93.37615066 163.42057853 101.2443667 163.45218313 109.11278796 C163.47837207 115.87473421 163.48906195 122.63659091 163.48778498 129.39858747 C163.48727598 133.43423698 163.49292441 137.46964254 163.51414108 141.50524139 C163.53356699 145.30402265 163.53361864 149.10232841 163.51910782 152.90112686 C163.51702376 154.29102654 163.52169662 155.68095631 163.53382874 157.0708046 C163.63064183 168.93555152 161.22221653 175.52080017 153.11993408 184.28695679 C150.27014875 186.98759447 147.56582477 188.56755321 144.03009033 190.26742554 C143.42618847 190.62413645 142.82228661 190.98084736 142.20008469 191.34836769 C139.38285843 192.5415477 137.1186836 192.52520363 134.06018066 192.53485107 C132.22657486 192.54764603 132.22657486 192.54764603 130.35592651 192.56069946 C129.00415026 192.56000045 127.65237411 192.5590774 126.30059814 192.55795288 C124.87457935 192.56393844 123.44856379 192.57074405 122.02255249 192.57830811 C118.15045754 192.59597575 114.27845098 192.60066389 110.40631962 192.60193968 C107.98537592 192.60336177 105.56445466 192.60763253 103.14351654 192.61293411 C94.69228185 192.63143915 86.24111024 192.63960441 77.78985596 192.63803101 C69.92136521 192.63683049 62.05314917 192.65791373 54.18472791 192.68951833 C47.42278166 192.71570727 40.66092496 192.72639716 33.8989284 192.72512019 C29.86327888 192.72461118 25.82787333 192.73025962 21.79227448 192.75147629 C17.99349322 192.77090219 14.19518746 192.77095385 10.39638901 192.75644302 C9.00648933 192.75435896 7.61655956 192.75903183 6.22671127 192.77116394 C-5.63803565 192.86797704 -12.22328431 190.45955173 -20.98944092 182.35726929 C-23.69007861 179.50748396 -25.27003734 176.80315997 -26.96990967 173.26742554 C-27.32662058 172.66352367 -27.68333149 172.05962181 -28.05085182 171.43741989 C-29.24403183 168.62019364 -29.22768776 166.3560188 -29.23733521 163.29751587 C-29.24586517 162.075112 -29.25439514 160.85270813 -29.26318359 159.59326172 C-29.26248459 158.24148547 -29.26156153 156.88970931 -29.26043701 155.53793335 C-29.26642257 154.11191455 -29.27322818 152.68589899 -29.28079224 151.2598877 C-29.29845988 147.38779274 -29.30314802 143.51578619 -29.30442381 139.64365482 C-29.3058459 137.22271113 -29.31011666 134.80178987 -29.31541824 132.38085175 C-29.33392328 123.92961705 -29.34208854 115.47844544 -29.34051514 107.02719116 C-29.33931462 99.15870042 -29.36039786 91.29048437 -29.39200246 83.42206311 C-29.4181914 76.66011686 -29.42888129 69.89826016 -29.42760432 63.13626361 C-29.42709531 59.10061409 -29.43274375 55.06520853 -29.45396042 51.02960968 C-29.47338632 47.23082842 -29.47343798 43.43252267 -29.45892715 39.63372421 C-29.45684309 38.24382454 -29.46151596 36.85389477 -29.47364807 35.46404648 C-29.57046117 23.59929956 -27.16203586 17.0140509 -19.05975342 8.24789429 C-16.20996809 5.5472566 -13.5056441 3.96729786 -9.96990967 2.26742554 C-9.3660078 1.91071463 -8.76210594 1.55400372 -8.13990402 1.18648338 C-5.32267777 -0.00669663 -3.05850293 0.00964745 0 0 Z\" fill=\"#040707\" transform=\"translate(28.96990966796875,-0.267425537109375)\"/>\n                        <path d=\"M0 0 C69.15867003 0 69.15867003 0 86.60546875 16.3203125 C96.2607962 27.12545871 100.6588566 40.1036397 100.28125 54.515625 C99.09555807 69.2050306 92.6403778 80.94218717 82 91 C75.97607376 95.9592323 69.10089652 101 61 101 C59.15135737 93.85751712 57.41626671 86.68704743 55.75 79.5 C55.56276367 78.72285645 55.37552734 77.94571289 55.18261719 77.14501953 C53.6605808 70.47337821 53.6605808 70.47337821 55 67 C55.928125 66.443125 56.85625 65.88625 57.8125 65.3125 C62.54142834 61.88170885 64.35238285 57.58171936 65.3828125 51.8828125 C65.67786326 46.8276094 64.2577339 42.79148692 61 39 C57.08312453 35.42607676 53.31213672 33.67738317 48 33.375 C42.70291537 33.68467572 38.84385735 35.33327893 35 39 C31.51693504 43.70312162 30.25350858 48.13471025 31 54 C33.12803967 60.49612109 36.3592823 64.23952153 42 68 C41.67381545 74.14126771 41.07899677 80.06545141 39.6875 86.0625 C38.07994321 93.13890194 36.98907922 100.25885726 35.9375 107.4375 C35.77531982 108.54424072 35.61313965 109.65098145 35.44604492 110.79125977 C34.23281433 119.18661851 33.12120708 127.5909469 32 136 C21.44 136 10.88 136 0 136 C0 91.12 0 46.24 0 0 Z\" fill=\"#FBFBFB\" transform=\"translate(51,28)\"/>\n                    </g>\n                </svg>\n\n            </div>\n        )\n    } else {\n        // Default fallback for unknown providers\n        return (\n            <div\n                className={`${sizeClasses[size]} rounded-md bg-secondary flex items-center justify-center text-secondary-foreground`}>\n                <span\n                    className={size === \"sm\" ? \"text-xs font-semibold\" : size === \"md\" ? \"text-sm font-semibold\" : \"text-lg font-semibold\"}>\n                    {providerName.substring(0, 2).toUpperCase()}\n                </span>\n            </div>\n        )\n    }\n}\n"
  },
  {
    "path": "components/subscription-form.tsx",
    "content": "'use client';\n\nimport {useState, useRef} from 'react';\nimport {useForm} from 'react-hook-form';\nimport {zodResolver} from '@hookform/resolvers/zod';\nimport {z} from 'zod';\nimport {motion, AnimatePresence} from 'framer-motion';\nimport {ArrowRight, Check, AlertCircle, Mail, Clock, Zap, CheckCircle2, Bell} from 'lucide-react';\nimport {Form, FormControl, FormField, FormItem, FormMessage} from '@/components/ui/form';\nimport {Input} from '@/components/ui/input';\nimport {Button} from '@/components/ui/button';\nimport {Alert, AlertDescription} from '@/components/ui/alert';\nimport {cn} from '@/lib/utils';\nimport confetti from 'canvas-confetti';\n\nconst formSchema = z.object({\n    email: z.string().email('Please enter a valid email address'),\n    name: z.string().optional(),\n    subscriptionType: z.enum(['DIGEST_ONLY', 'ALL_UPDATES', 'MAJOR_ONLY']).default('ALL_UPDATES'),\n});\n\ntype SubscriptionFormValues = z.infer<typeof formSchema>;\n\ninterface Update {\n    title: string;\n    date: string;\n}\n\ninterface SubscriptionFormProps {\n    projectId: string;\n    projectName: string;\n    recentUpdates?: Update[];\n}\n\ntype Step = 'email' | 'name' | 'preferences' | 'success';\n\nexport default function SubscriptionForm({\n                                             projectId,\n                                         }: SubscriptionFormProps) {\n    const [currentStep, setCurrentStep] = useState<Step>('email');\n    const [isSubmitting, setIsSubmitting] = useState(false);\n    const [error, setError] = useState<string | null>(null);\n    const successRef = useRef<HTMLDivElement>(null);\n\n    // Detect if we're on a custom domain\n    const getCustomDomain = (): string | null => {\n        if (typeof window === 'undefined') return null;\n\n        const hostname = window.location.hostname;\n        const appUrl = process.env.NEXT_PUBLIC_APP_URL || '';\n\n        try {\n            const appDomain = new URL(appUrl).hostname;\n\n            // Skip localhost and development domains\n            if (hostname.includes('localhost') || hostname.includes('127.0.0.1')) {\n                return null;\n            }\n\n            // If hostname is different from app domain and not a subdomain, it's a custom domain\n            if (hostname !== appDomain && !hostname.endsWith(`.${appDomain}`)) {\n                return hostname;\n            }\n        } catch (error) {\n            console.error('Error parsing app URL:', error);\n        }\n\n        return null;\n    };\n\n    const form = useForm<SubscriptionFormValues>({\n        resolver: zodResolver(formSchema),\n        defaultValues: {\n            email: '',\n            name: '',\n            subscriptionType: 'ALL_UPDATES',\n        },\n        mode: 'onChange',\n    });\n\n    const triggerConfetti = () => {\n        // Get the position of the success message\n        if (successRef.current) {\n            const rect = successRef.current.getBoundingClientRect();\n            const x = rect.left + rect.width / 2;\n            const y = rect.top + rect.height / 2;\n\n            // Convert to relative position (0-1)\n            const xRelative = x / window.innerWidth;\n            const yRelative = y / window.innerHeight;\n\n            // Fire confetti from the success message position\n            confetti({\n                particleCount: 100,\n                spread: 70,\n                origin: {x: xRelative, y: yRelative},\n                colors: ['#818cf8', '#c4b5fd', '#a78bfa', '#8b5cf6', '#7c3aed'],\n                disableForReducedMotion: true,\n            });\n        } else {\n            // Fallback to center if ref is not available\n            confetti({\n                particleCount: 100,\n                spread: 70,\n                origin: {x: 0.5, y: 0.4},\n                colors: ['#818cf8', '#c4b5fd', '#a78bfa', '#8b5cf6', '#7c3aed'],\n                disableForReducedMotion: true,\n            });\n        }\n    };\n\n    const onSubmit = async (values: SubscriptionFormValues) => {\n        setIsSubmitting(true);\n        setError(null);\n\n        try {\n            const customDomain = getCustomDomain();\n\n            const response = await fetch('/api/changelog/subscribe', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({\n                    email: values.email,\n                    name: values.name,\n                    projectId: projectId,\n                    subscriptionType: values.subscriptionType,\n                    customDomain\n                }),\n            });\n\n            if (!response.ok) {\n                const data = await response.json();\n                throw new Error(data.error || 'Failed to subscribe');\n            }\n\n            setCurrentStep('success');\n            // Trigger confetti after a short delay to allow the success animation to start\n            setTimeout(triggerConfetti, 300);\n        } catch (err) {\n            setError(err instanceof Error ? err.message : 'Failed to subscribe');\n        } finally {\n            setIsSubmitting(false);\n        }\n    };\n\n    const handleNext = () => {\n        if (currentStep === 'email') {\n            const emailValue = form.getValues('email');\n            const emailError = form.formState.errors.email;\n\n            if (emailValue && !emailError) {\n                setCurrentStep('name');\n            } else {\n                form.trigger('email');\n            }\n        } else if (currentStep === 'name') {\n            setCurrentStep('preferences');\n        }\n    };\n\n    const skipNameStep = () => {\n        setCurrentStep('preferences');\n    };\n\n    const restartForm = () => {\n        form.reset();\n        setCurrentStep('email');\n    };\n\n    // Get the domain name for display\n    const displayDomain = getCustomDomain();\n\n    return (\n        <div\n            className={cn(\n                \"w-full max-w-md mx-auto mt-16 mb-8\",\n                \"transition-all duration-300\"\n            )}\n        >\n            {/* More subtle and integrated notification section */}\n            <div className=\"flex items-center justify-center gap-3 mb-8\">\n                <div className=\"h-px flex-1 bg-border/40\"/>\n                <div className=\"flex items-center gap-2 text-muted-foreground\">\n                    <Bell className=\"h-4 w-4\"/>\n                    <span className=\"text-sm font-medium\">\n                        {displayDomain ? 'Get Updates' : 'Subscribe for Updates'}\n                    </span>\n                </div>\n                <div className=\"h-px flex-1 bg-border/40\"/>\n            </div>\n\n            {/* Show custom domain indicator if present */}\n            {displayDomain && (\n                <div className=\"text-center mb-6\">\n                    <div className=\"inline-flex items-center gap-2 px-3 py-1.5 bg-blue-50 text-blue-700 rounded-full text-sm\">\n                        <div className=\"w-2 h-2 bg-blue-500 rounded-full\"></div>\n                        <span className=\"font-medium\">{displayDomain}</span>\n                    </div>\n                    <p className=\"text-xs text-muted-foreground mt-2\">\n                        Subscribing to updates from this changelog\n                    </p>\n                </div>\n            )}\n\n            <div className=\"relative\">\n                <Form {...form}>\n                    <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-4\">\n                        <AnimatePresence mode=\"wait\">\n                            {currentStep === 'email' && (\n                                <motion.div\n                                    key=\"email-step\"\n                                    initial={{opacity: 0, y: 10}}\n                                    animate={{opacity: 1, y: 0}}\n                                    exit={{opacity: 0, y: -10}}\n                                    transition={{duration: 0.2}}\n                                    className=\"space-y-3\"\n                                >\n                                    {error && (\n                                        <Alert variant=\"destructive\" className=\"mb-3\">\n                                            <AlertCircle className=\"h-4 w-4\"/>\n                                            <AlertDescription>{error}</AlertDescription>\n                                        </Alert>\n                                    )}\n\n                                    <FormField\n                                        control={form.control}\n                                        name=\"email\"\n                                        render={({field}) => (\n                                            <FormItem>\n                                                <FormControl>\n                                                    <div className=\"relative\">\n                                                        <Mail\n                                                            className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground/60\"/>\n                                                        <Input\n                                                            placeholder=\"your@email.com\"\n                                                            className=\"pl-10 h-10 bg-background/50 border-border/50 placeholder:text-muted-foreground/50\"\n                                                            onKeyDown={(e) => {\n                                                                if (e.key === 'Enter') {\n                                                                    e.preventDefault();\n                                                                    handleNext();\n                                                                }\n                                                            }}\n                                                            {...field}\n                                                        />\n                                                    </div>\n                                                </FormControl>\n                                                <FormMessage className=\"text-xs\"/>\n                                            </FormItem>\n                                        )}\n                                    />\n\n                                    <Button\n                                        type=\"button\"\n                                        onClick={handleNext}\n                                        className=\"w-full h-10\"\n                                        variant=\"secondary\"\n                                    >\n                                        Continue\n                                        <ArrowRight className=\"ml-2 h-4 w-4\"/>\n                                    </Button>\n                                </motion.div>\n                            )}\n\n                            {currentStep === 'name' && (\n                                <motion.div\n                                    key=\"name-step\"\n                                    initial={{opacity: 0, y: 10}}\n                                    animate={{opacity: 1, y: 0}}\n                                    exit={{opacity: 0, y: -10}}\n                                    transition={{duration: 0.2}}\n                                    className=\"space-y-3\"\n                                >\n                                    <div className=\"text-center mb-4\">\n                                        <p className=\"text-sm text-muted-foreground\">\n                                            Add your name for personalized updates\n                                        </p>\n                                    </div>\n\n                                    <FormField\n                                        control={form.control}\n                                        name=\"name\"\n                                        render={({field}) => (\n                                            <FormItem>\n                                                <FormControl>\n                                                    <Input\n                                                        placeholder=\"Your name (optional)\"\n                                                        className=\"h-10 bg-background/50 border-border/50\"\n                                                        onKeyDown={(e) => {\n                                                            if (e.key === 'Enter') {\n                                                                e.preventDefault();\n                                                                handleNext();\n                                                            }\n                                                        }}\n                                                        {...field}\n                                                    />\n                                                </FormControl>\n                                                <FormMessage className=\"text-xs\"/>\n                                            </FormItem>\n                                        )}\n                                    />\n\n                                    <div className=\"flex gap-2\">\n                                        <Button\n                                            type=\"button\"\n                                            onClick={skipNameStep}\n                                            variant=\"ghost\"\n                                            className=\"flex-1 h-10\"\n                                        >\n                                            Skip\n                                        </Button>\n                                        <Button\n                                            type=\"button\"\n                                            onClick={handleNext}\n                                            variant=\"secondary\"\n                                            className=\"flex-1 h-10\"\n                                        >\n                                            Continue\n                                        </Button>\n                                    </div>\n                                </motion.div>\n                            )}\n\n                            {currentStep === 'preferences' && (\n                                <motion.div\n                                    key=\"preferences-step\"\n                                    initial={{opacity: 0, y: 10}}\n                                    animate={{opacity: 1, y: 0}}\n                                    exit={{opacity: 0, y: -10}}\n                                    transition={{duration: 0.2}}\n                                    className=\"space-y-3\"\n                                >\n                                    <div className=\"text-center mb-4\">\n                                        <p className=\"text-sm text-muted-foreground\">\n                                            How often should we notify you?\n                                        </p>\n                                    </div>\n\n                                    <FormField\n                                        control={form.control}\n                                        name=\"subscriptionType\"\n                                        render={({field}) => (\n                                            <FormItem>\n                                                <FormControl>\n                                                    <div className=\"grid gap-2\">\n                                                        {[\n                                                            {\n                                                                value: 'ALL_UPDATES',\n                                                                icon: Zap,\n                                                                title: 'All Updates',\n                                                                description: 'Every change'\n                                                            },\n                                                            {\n                                                                value: 'MAJOR_ONLY',\n                                                                icon: CheckCircle2,\n                                                                title: 'Major Only',\n                                                                description: 'Important updates'\n                                                            },\n                                                            {\n                                                                value: 'DIGEST_ONLY',\n                                                                icon: Clock,\n                                                                title: 'Weekly Digest',\n                                                                description: 'Weekly summary'\n                                                            }\n                                                        ].map((option) => (\n                                                            <button\n                                                                key={option.value}\n                                                                type=\"button\"\n                                                                className={cn(\n                                                                    \"flex items-center p-3 rounded-md text-left transition-all text-sm\",\n                                                                    field.value === option.value\n                                                                        ? \"bg-muted/50 text-foreground\"\n                                                                        : \"text-muted-foreground hover:bg-muted/30\",\n                                                                )}\n                                                                onClick={() => field.onChange(option.value)}\n                                                            >\n                                                                <option.icon className={cn(\n                                                                    \"h-4 w-4 mr-3\",\n                                                                    field.value === option.value ? \"text-primary\" : \"text-muted-foreground\"\n                                                                )}/>\n                                                                <div className=\"flex-1\">\n                                                                    <p className=\"font-medium\">{option.title}</p>\n                                                                    <p className=\"text-xs\">{option.description}</p>\n                                                                </div>\n                                                                {field.value === option.value && (\n                                                                    <Check className=\"h-4 w-4 text-primary\"/>\n                                                                )}\n                                                            </button>\n                                                        ))}\n                                                    </div>\n                                                </FormControl>\n                                                <FormMessage className=\"text-xs\"/>\n                                            </FormItem>\n                                        )}\n                                    />\n\n                                    <Button\n                                        type=\"submit\"\n                                        className=\"w-full h-10\"\n                                        disabled={isSubmitting}\n                                    >\n                                        {isSubmitting ? (\n                                            <>\n                                                <div\n                                                    className=\"mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent\"/>\n                                                Subscribing...\n                                            </>\n                                        ) : (\n                                            'Subscribe'\n                                        )}\n                                    </Button>\n                                </motion.div>\n                            )}\n\n                            {currentStep === 'success' && (\n                                <motion.div\n                                    key=\"success-step\"\n                                    ref={successRef}\n                                    initial={{opacity: 0, scale: 0.95}}\n                                    animate={{opacity: 1, scale: 1}}\n                                    transition={{duration: 0.2}}\n                                    className=\"text-center py-6\"\n                                >\n                                    <motion.div\n                                        initial={{scale: 0.8}}\n                                        animate={{scale: 1}}\n                                        transition={{type: \"spring\", stiffness: 200, damping: 15}}\n                                        className=\"inline-flex items-center justify-center w-12 h-12 rounded-full bg-primary/10 mb-4\"\n                                    >\n                                        <Check className=\"h-6 w-6 text-primary\"/>\n                                    </motion.div>\n\n                                    <h3 className=\"text-lg font-medium mb-1\">You&apos;re all set!</h3>\n                                    <p className=\"text-sm text-muted-foreground mb-2\">\n                                        We&apos;ll notify you about updates.\n                                    </p>\n\n                                    {displayDomain && (\n                                        <p className=\"text-xs text-muted-foreground mb-6\">\n                                            Unsubscribe links will redirect to <span className=\"font-medium\">{displayDomain}</span>\n                                        </p>\n                                    )}\n\n                                    <Button\n                                        type=\"button\"\n                                        onClick={restartForm}\n                                        variant=\"ghost\"\n                                        size=\"sm\"\n                                        className=\"text-muted-foreground\"\n                                    >\n                                        Subscribe another email\n                                    </Button>\n                                </motion.div>\n                            )}\n                        </AnimatePresence>\n                    </form>\n                </Form>\n            </div>\n        </div>\n    );\n}"
  },
  {
    "path": "components/telemetry/PromptModal.tsx",
    "content": "import React, {useState} from 'react';\nimport {Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter} from '@/components/ui/dialog';\nimport {Button} from '@/components/ui/button';\nimport {Card, CardContent} from '@/components/ui/card';\nimport {Badge} from '@/components/ui/badge';\nimport {Input} from '@/components/ui/input';\nimport {Shield, CheckCircle, XCircle, BarChart3, Heart, ArrowLeft, Frown} from 'lucide-react';\nimport {TelemetryState} from '@/lib/types/telemetry';\nimport {appInfo, getVersionString} from '@/lib/app-info';\n\ninterface TelemetryPromptModalProps {\n    isOpen: boolean;\n    onChoice: (choice: Extract<TelemetryState, 'enabled' | 'disabled'>) => Promise<void>;\n    disableClose?: boolean;\n}\n\ntype ModalStep = 'initial' | 'confirmation' | 'final-plea';\n\nexport const TelemetryPromptModal: React.FC<TelemetryPromptModalProps> = ({\n                                                                              isOpen,\n                                                                              onChoice,\n                                                                              disableClose = true,\n                                                                          }) => {\n    const [isLoading, setIsLoading] = useState(false);\n    const [currentStep, setCurrentStep] = useState<ModalStep>('initial');\n    const [confirmationText, setConfirmationText] = useState('');\n\n    const handleChoice = async (choice: Extract<TelemetryState, 'enabled' | 'disabled'>) => {\n        setIsLoading(true);\n        try {\n            await onChoice(choice);\n        } finally {\n            setIsLoading(false);\n        }\n    };\n\n    const handleNoThanks = () => {\n        setCurrentStep('confirmation');\n    };\n\n    const handleConfirmOptOut = () => {\n        setCurrentStep('final-plea');\n    };\n\n    const handleFinalOptOut = () => {\n        handleChoice('disabled');\n    };\n\n    const handleGoBack = () => {\n        setCurrentStep('initial');\n    };\n\n    const handleGoBackToConfirmation = () => {\n        setCurrentStep('confirmation');\n        setConfirmationText('');\n    };\n\n    const renderInitialStep = () => (\n        <>\n            <DialogHeader className=\"text-center pb-2\">\n                <div className=\"mx-auto w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mb-4\">\n                    <BarChart3 className=\"w-6 h-6 text-primary\"/>\n                </div>\n                <DialogTitle className=\"text-xl\">\n                    Help Improve {appInfo.name}\n                </DialogTitle>\n                <p className=\"text-muted-foreground text-sm mt-2\">\n                    Share anonymous usage data to help us make {appInfo.name} better for everyone\n                </p>\n            </DialogHeader>\n\n            <div className=\"space-y-4\">\n                {/* What we collect */}\n                <Card className=\"border-green-200 bg-green-50/50 dark:border-green-800 dark:bg-green-950/20\">\n                    <CardContent className=\"pt-4\">\n                        <div className=\"flex items-start gap-3\">\n                            <CheckCircle\n                                className=\"w-5 h-5 text-green-600 dark:text-green-400 mt-0.5 flex-shrink-0\"\n                            />\n                            <div className=\"space-y-2\">\n                                <h4 className=\"font-medium text-green-900 dark:text-green-100\">\n                                    What we collect\n                                </h4>\n                                <div className=\"flex flex-wrap gap-2\">\n                                    <Badge variant=\"secondary\" className=\"text-xs\">\n                                        Version: {getVersionString()}\n                                    </Badge>\n                                    <Badge variant=\"secondary\" className=\"text-xs\">\n                                        Environment: {appInfo.environment}\n                                    </Badge>\n                                    <Badge variant=\"secondary\" className=\"text-xs\">\n                                        Anonymous instance ID\n                                    </Badge>\n                                </div>\n                            </div>\n                        </div>\n                    </CardContent>\n                </Card>\n\n                {/* What we don't collect */}\n                <Card className=\"border-red-200 bg-red-50/50 dark:border-red-800 dark:bg-red-950/20\">\n                    <CardContent className=\"pt-4\">\n                        <div className=\"flex items-start gap-3\">\n                            <XCircle className=\"w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0\"/>\n                            <div>\n                                <h4 className=\"font-medium text-red-900 dark:text-red-100 mb-2\">\n                                    What we don&apos;t collect\n                                </h4>\n                                <p className=\"text-sm text-red-700 dark:text-red-300\">\n                                    Personal data, logs, file contents, or any sensitive information\n                                </p>\n                            </div>\n                        </div>\n                    </CardContent>\n                </Card>\n\n                {/* Privacy notice */}\n                <div className=\"flex items-center gap-2 text-xs text-muted-foreground bg-muted/50 p-3 rounded-lg\">\n                    <Shield className=\"w-4 h-4\"/>\n                    <span>All data is anonymized and used solely for product improvement</span>\n                </div>\n            </div>\n\n            <DialogFooter className=\"flex-col sm:flex-row gap-2 pt-6\">\n                <Button\n                    variant=\"outline\"\n                    onClick={handleNoThanks}\n                    disabled={isLoading}\n                    className=\"w-full sm:w-auto\"\n                >\n                    No Thanks\n                </Button>\n                <Button\n                    onClick={() => handleChoice('enabled')}\n                    disabled={isLoading}\n                    className=\"w-full sm:w-auto\"\n                >\n                    {isLoading ? (\n                        <>\n                            <div\n                                className=\"w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin mr-2\"/>\n                            Setting up...\n                        </>\n                    ) : (\n                        'Enable Telemetry'\n                    )}\n                </Button>\n            </DialogFooter>\n        </>\n    );\n\n    const renderConfirmationStep = () => (\n        <>\n            <DialogHeader className=\"text-center pb-2\">\n                <div\n                    className=\"mx-auto w-12 h-12 bg-orange-100 dark:bg-orange-950/30 rounded-full flex items-center justify-center mb-4\">\n                    <Heart className=\"w-6 h-6 text-orange-600 dark:text-orange-400\"/>\n                </div>\n                <DialogTitle className=\"text-xl\">\n                    Are you sure?\n                </DialogTitle>\n                <p className=\"text-muted-foreground text-sm mt-2\">\n                    Here&apos;s why telemetry matters to us\n                </p>\n            </DialogHeader>\n\n            <Card className=\"border-orange-200 bg-orange-50/50 dark:border-orange-800 dark:bg-orange-950/20\">\n                <CardContent className=\"pt-4\">\n                    <div className=\"space-y-3\">\n                        <h4 className=\"font-medium text-orange-900 dark:text-orange-100\">\n                            Why this helps\n                        </h4>\n                        <p className=\"text-sm text-orange-800 dark:text-orange-200 leading-relaxed\">\n                            Telemetry was created to keep track of installations and is used on the website\n                            mainly to display active installations. It&apos;s nice to know that I&apos;m making\n                            a difference, which is what telemetry keeps track of.\n                        </p>\n                        <p className=\"text-sm text-orange-800 dark:text-orange-200 leading-relaxed font-medium\">\n                            By opting out, I won&apos;t know if you&apos;re using {appInfo.name} at all, which sucks.\n                        </p>\n                    </div>\n                </CardContent>\n            </Card>\n\n            <div className=\"flex items-center gap-2 text-xs text-muted-foreground bg-muted/50 p-3 rounded-lg\">\n                <Shield className=\"w-4 h-4\"/>\n                <span>Your privacy is still protected - no personal data is ever collected</span>\n            </div>\n\n            <DialogFooter className=\"flex-col sm:flex-row gap-2 pt-6\">\n                <Button\n                    variant=\"ghost\"\n                    onClick={handleGoBack}\n                    disabled={isLoading}\n                    className=\"w-full sm:w-auto\"\n                >\n                    <ArrowLeft className=\"w-4 h-4 mr-2\"/>\n                    Go Back\n                </Button>\n                <div className=\"flex flex-col sm:flex-row gap-2 flex-1\">\n                    <Button\n                        variant=\"outline\"\n                        onClick={handleConfirmOptOut}\n                        disabled={isLoading}\n                        className=\"w-full sm:w-auto\"\n                    >\n                        Still Opt Out\n                    </Button>\n                    <Button\n                        onClick={() => handleChoice('enabled')}\n                        disabled={isLoading}\n                        className=\"w-full sm:w-auto\"\n                    >\n                        Enable Telemetry\n                    </Button>\n                </div>\n            </DialogFooter>\n        </>\n    );\n\n    const renderFinalPleaStep = () => {\n        const requiredText = \"I'm sure\";\n        const isConfirmationValid = confirmationText.trim().toLowerCase() === requiredText.toLowerCase();\n\n        return (\n            <>\n                <DialogHeader className=\"text-center pb-3\">\n                    <div\n                        className=\"mx-auto w-12 h-12 bg-gray-100 dark:bg-gray-800/50 rounded-full flex items-center justify-center mb-3\">\n                        <Frown className=\"w-6 h-6 text-gray-600 dark:text-gray-400\"/>\n                    </div>\n                    <DialogTitle className=\"text-xl\">\n                        Okay, I understand...\n                    </DialogTitle>\n                    <p className=\"text-muted-foreground text-sm mt-1\">\n                        Just one tiny favor before you go?\n                    </p>\n                </DialogHeader>\n\n                <div className=\"space-y-4\">\n                    <Card className=\"border-blue-200 bg-blue-50/50 dark:border-blue-800 dark:bg-blue-950/20\">\n                        <CardContent className=\"pt-4 pb-4\">\n                            <div className=\"space-y-3\">\n                                <h4 className=\"font-medium text-blue-900 dark:text-blue-100\">\n                                    One last thing\n                                </h4>\n                                <p className=\"text-sm text-blue-800 dark:text-blue-200 leading-relaxed\">\n                                    If {appInfo.name} ever helps you out or saves you some time, maybe consider\n                                    leaving a star on GitHub or telling a friend about it?\n                                </p>\n                                <p className=\"text-sm text-blue-800 dark:text-blue-200 leading-relaxed\">\n                                    It would mean the world to me and helps other developers discover the project. 🌟\n                                </p>\n                            </div>\n                        </CardContent>\n                    </Card>\n\n                    <div className=\"space-y-3\">\n                        <div\n                            className=\"flex items-center gap-2 text-xs text-muted-foreground bg-muted/50 p-3 rounded-lg\">\n                            <Heart className=\"w-4 h-4\"/>\n                            <span>Thanks for trying {appInfo.name} - I hope it serves you well!</span>\n                        </div>\n\n                        <div className=\"space-y-2\">\n                            <p className=\"text-sm text-muted-foreground\">\n                                To confirm you want to opt out, please type: <span\n                                className=\"font-mono font-medium\">{requiredText}</span>\n                            </p>\n                            <Input\n                                placeholder={`Type \"${requiredText}\" to confirm`}\n                                value={confirmationText}\n                                onChange={(e) => setConfirmationText(e.target.value)}\n                                disabled={isLoading}\n                                className=\"text-sm\"\n                            />\n                        </div>\n                    </div>\n                </div>\n\n                <DialogFooter className=\"flex-col sm:flex-row gap-2 pt-4\">\n                    <Button\n                        variant=\"ghost\"\n                        onClick={handleGoBackToConfirmation}\n                        disabled={isLoading}\n                        className=\"w-full sm:w-auto\"\n                    >\n                        <ArrowLeft className=\"w-4 h-4 mr-2\"/>\n                        Go Back\n                    </Button>\n                    <div className=\"flex flex-col sm:flex-row gap-2 flex-1\">\n                        <Button\n                            variant=\"outline\"\n                            onClick={handleFinalOptOut}\n                            disabled={isLoading || !isConfirmationValid}\n                            className=\"w-full sm:w-auto\"\n                        >\n                            {isLoading ? (\n                                <>\n                                    <div\n                                        className=\"w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin mr-2\"/>\n                                    Opting out...\n                                </>\n                            ) : (\n                                'Got it, thanks anyway'\n                            )}\n                        </Button>\n                        <Button\n                            onClick={() => handleChoice('enabled')}\n                            disabled={isLoading}\n                            className=\"w-full sm:w-auto\"\n                        >\n                            Fine, I&apos;ll enable it\n                        </Button>\n                    </div>\n                </DialogFooter>\n            </>\n        );\n    };\n\n    return (\n        <Dialog open={isOpen}>\n            <DialogContent\n                className=\"sm:max-w-lg\"\n                disableClose={disableClose}\n            >\n                {currentStep === 'initial' && renderInitialStep()}\n                {currentStep === 'confirmation' && renderConfirmationStep()}\n                {currentStep === 'final-plea' && renderFinalPleaStep()}\n            </DialogContent>\n        </Dialog>\n    );\n};"
  },
  {
    "path": "components/theme-provider.tsx",
    "content": "'use client'\n\nimport * as React from \"react\"\nimport { ThemeProvider as NextThemesProvider, useTheme } from \"next-themes\"\nimport { useAuth } from \"@/context/auth\"\nimport { usePathname } from \"next/navigation\"\n\n// Theme sync component that handles user settings integration\nfunction ThemeSync() {\n    const { theme, setTheme } = useTheme()\n    const { user, isLoading } = useAuth()\n    const [hasSynced, setHasSynced] = React.useState(false)\n    const pathname = usePathname()\n\n    // Check if user is authenticated (not loading and has user)\n    const isAuthenticated = !isLoading && !!user\n\n    // Don't sync for public changelog pages - they use per-project storage\n    const isPublicChangelog = pathname?.startsWith('/changelog/')\n\n    // Sync theme with user settings when authenticated\n    React.useEffect(() => {\n        async function syncTheme() {\n            if (!isAuthenticated || !user || hasSynced || isPublicChangelog) return\n\n            try {\n                const response = await fetch('/api/auth/settings')\n                if (response.ok) {\n                    const settings = await response.json()\n                    if (settings.theme && settings.theme !== theme) {\n                        setTheme(settings.theme)\n                    }\n                }\n            } catch (error) {\n                console.error('Failed to sync theme from user settings:', error)\n            } finally {\n                setHasSynced(true)\n            }\n        }\n\n        syncTheme()\n    }, [isAuthenticated, user, theme, setTheme, hasSynced, isPublicChangelog])\n\n    // Save theme changes to user settings when authenticated\n    React.useEffect(() => {\n        async function saveTheme() {\n            if (!isAuthenticated || !user || !hasSynced || !theme || isPublicChangelog) return\n\n            try {\n                await fetch('/api/auth/settings', {\n                    method: 'PATCH',\n                    headers: { 'Content-Type': 'application/json' },\n                    body: JSON.stringify({ theme })\n                })\n            } catch (error) {\n                console.error('Failed to save theme to user settings:', error)\n            }\n        }\n\n        // Debounce theme saves\n        const timeoutId = setTimeout(saveTheme, 500)\n        return () => clearTimeout(timeoutId)\n    }, [theme, isAuthenticated, user, hasSynced, isPublicChangelog])\n\n    return null\n}\n\nexport function ThemeProvider({\n                                  children,\n                                  ...props\n                              }: React.ComponentProps<typeof NextThemesProvider>) {\n    return (\n        <NextThemesProvider\n            {...props}\n            attribute=\"class\"\n            defaultTheme=\"light\"\n            enableSystem={false}\n            disableTransitionOnChange={false}\n            storageKey=\"theme\"\n        >\n            <ThemeSync />\n            {children}\n        </NextThemesProvider>\n    )\n}\n\n// Custom hook for theme with loading state\nexport function useThemeWithLoading() {\n    const { theme, setTheme, resolvedTheme } = useTheme()\n    const [mounted, setMounted] = React.useState(false)\n\n    React.useEffect(() => {\n        setMounted(true)\n    }, [])\n\n    return {\n        theme,\n        setTheme,\n        resolvedTheme,\n        isLoading: !mounted\n    }\n}"
  },
  {
    "path": "components/ui/accordion.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\"\nimport { ChevronDown } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Accordion = AccordionPrimitive.Root\n\nconst AccordionItem = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>\n>(({ className, ...props }, ref) => (\n  <AccordionPrimitive.Item\n    ref={ref}\n    className={cn(\"border-b\", className)}\n    {...props}\n  />\n))\nAccordionItem.displayName = \"AccordionItem\"\n\nconst AccordionTrigger = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <AccordionPrimitive.Header className=\"flex\">\n    <AccordionPrimitive.Trigger\n      ref={ref}\n      className={cn(\n        \"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronDown className=\"h-4 w-4 shrink-0 transition-transform duration-200\" />\n    </AccordionPrimitive.Trigger>\n  </AccordionPrimitive.Header>\n))\nAccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName\n\nconst AccordionContent = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <AccordionPrimitive.Content\n    ref={ref}\n    className=\"overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down\"\n    {...props}\n  >\n    <div className={cn(\"pb-4 pt-0\", className)}>{children}</div>\n  </AccordionPrimitive.Content>\n))\n\nAccordionContent.displayName = AccordionPrimitive.Content.displayName\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent }\n"
  },
  {
    "path": "components/ui/alert-dialog.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\"\n\nimport {cn} from \"@/lib/utils\"\nimport {buttonVariants} from \"@/components/ui/button\"\n\nconst AlertDialog = AlertDialogPrimitive.Root\n\nconst AlertDialogTrigger = AlertDialogPrimitive.Trigger\n\nconst AlertDialogPortal = AlertDialogPrimitive.Portal\n\nconst AlertDialogOverlay = React.forwardRef<\n    React.ElementRef<typeof AlertDialogPrimitive.Overlay>,\n    React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>\n>(({className, ...props}, ref) => (\n    <AlertDialogPrimitive.Overlay\n        className={cn(\n            \"fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n            className\n        )}\n        {...props}\n        ref={ref}\n    />\n))\nAlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName\n\nconst AlertDialogContent = React.forwardRef<\n    React.ElementRef<typeof AlertDialogPrimitive.Content>,\n    React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>\n>(({className, ...props}, ref) => (\n    <AlertDialogPortal>\n        <AlertDialogOverlay/>\n        <AlertDialogPrimitive.Content\n            ref={ref}\n            className={cn(\n                \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg\",\n                className\n            )}\n            {...props}\n        />\n    </AlertDialogPortal>\n))\nAlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName\n\nconst AlertDialogHeader = ({\n                               className,\n                               ...props\n                           }: React.HTMLAttributes<HTMLDivElement>) => (\n    <div\n        className={cn(\n            \"flex flex-col space-y-2 text-center sm:text-left\",\n            className\n        )}\n        {...props}\n    />\n)\nAlertDialogHeader.displayName = \"AlertDialogHeader\"\n\nconst AlertDialogFooter = ({\n                               className,\n                               ...props\n                           }: React.HTMLAttributes<HTMLDivElement>) => (\n    <div\n        className={cn(\n            \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n            className\n        )}\n        {...props}\n    />\n)\nAlertDialogFooter.displayName = \"AlertDialogFooter\"\n\nconst AlertDialogTitle = React.forwardRef<\n    React.ElementRef<typeof AlertDialogPrimitive.Title>,\n    React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>\n>(({className, ...props}, ref) => (\n    <AlertDialogPrimitive.Title\n        ref={ref}\n        className={cn(\"text-lg font-semibold\", className)}\n        {...props}\n    />\n))\nAlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName\n\nconst AlertDialogDescription = React.forwardRef<\n    React.ElementRef<typeof AlertDialogPrimitive.Description>,\n    React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>\n>(({className, ...props}, ref) => (\n    <AlertDialogPrimitive.Description\n        ref={ref}\n        className={cn(\"text-sm text-muted-foreground\", className)}\n        {...props}\n    />\n))\nAlertDialogDescription.displayName =\n    AlertDialogPrimitive.Description.displayName\n\nconst AlertDialogAction = React.forwardRef<\n    React.ElementRef<typeof AlertDialogPrimitive.Action>,\n    React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>\n>(({className, ...props}, ref) => (\n    <AlertDialogPrimitive.Action\n        ref={ref}\n        className={cn(buttonVariants(), className)}\n        {...props}\n    />\n))\nAlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName\n\nconst AlertDialogCancel = React.forwardRef<\n    React.ElementRef<typeof AlertDialogPrimitive.Cancel>,\n    React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>\n>(({className, ...props}, ref) => (\n    <AlertDialogPrimitive.Cancel\n        ref={ref}\n        className={cn(\n            buttonVariants({variant: \"outline\"}),\n            \"mt-2 sm:mt-0\",\n            className\n        )}\n        {...props}\n    />\n))\nAlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName\n\nexport {\n    AlertDialog,\n    AlertDialogPortal,\n    AlertDialogOverlay,\n    AlertDialogTrigger,\n    AlertDialogContent,\n    AlertDialogHeader,\n    AlertDialogFooter,\n    AlertDialogTitle,\n    AlertDialogDescription,\n    AlertDialogAction,\n    AlertDialogCancel,\n}\n"
  },
  {
    "path": "components/ui/alert.tsx",
    "content": "import * as React from \"react\"\nimport {cva, type VariantProps} from \"class-variance-authority\"\nimport {AlertCircle, Info, CheckCircle2, AlertTriangle, XCircle} from \"lucide-react\"\nimport {cn} from \"@/lib/utils\"\n\nconst alertVariants = cva(\n    \"relative isolate w-full rounded-lg border p-4 transition-all duration-300 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground\",\n    {\n        variants: {\n            variant: {\n                default: [\n                    // Base styling with an optical border\n                    \"border border-muted-foreground/20 bg-background text-foreground\",\n                    // Background layer for depth\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.lg)-1px)] before:bg-gradient-to-b before:from-background/80 before:to-background/40\",\n                    // Subtle highlight\n                    \"after:absolute after:inset-0 after:-z-10 after:rounded-[calc(theme(borderRadius.lg)-1px)]\",\n                    \"after:shadow-[inset_0_1px_theme(colors.white/8%)]\",\n                    \"shadow-sm shadow-slate-100 dark:shadow-slate-800/50\",\n                ],\n                destructive: [\n                    \"border border-destructive/30 bg-destructive/5 text-destructive dark:border-destructive/20\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.lg)-1px)]\",\n                    \"before:bg-gradient-to-b before:from-destructive/10 before:to-destructive/5\",\n                    \"after:absolute after:inset-0 after:-z-10 after:rounded-[calc(theme(borderRadius.lg)-1px)]\",\n                    \"after:shadow-[inset_0_1px_theme(colors.red.200/20%)]\",\n                    \"[&>svg]:text-destructive shadow-sm shadow-red-100 dark:shadow-red-900/20\",\n                ],\n                warning: [\n                    \"border border-orange-500/30 bg-orange-50 text-orange-600 dark:border-orange-500/20 dark:bg-orange-950/20 dark:text-orange-400\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.lg)-1px)]\",\n                    \"before:bg-gradient-to-b before:from-orange-500/10 before:to-orange-500/5\",\n                    \"after:absolute after:inset-0 after:-z-10 after:rounded-[calc(theme(borderRadius.lg)-1px)]\",\n                    \"after:shadow-[inset_0_1px_theme(colors.orange.200/20%)]\",\n                    \"[&>svg]:text-orange-600 dark:[&>svg]:text-orange-500 shadow-sm shadow-orange-100 dark:shadow-orange-900/20\",\n                ],\n                success: [\n                    \"border border-green-500/30 bg-green-50 text-green-600 dark:border-green-500/20 dark:bg-green-950/20 dark:text-green-400\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.lg)-1px)]\",\n                    \"before:bg-gradient-to-b before:from-green-500/10 before:to-green-500/5\",\n                    \"after:absolute after:inset-0 after:-z-10 after:rounded-[calc(theme(borderRadius.lg)-1px)]\",\n                    \"after:shadow-[inset_0_1px_theme(colors.green.200/20%)]\",\n                    \"[&>svg]:text-green-600 dark:[&>svg]:text-green-500 shadow-sm shadow-green-100 dark:shadow-green-900/20\",\n                ],\n                info: [\n                    \"border border-blue-500/30 bg-blue-50 text-blue-600 dark:border-blue-500/20 dark:bg-blue-950/20 dark:text-blue-400\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.lg)-1px)]\",\n                    \"before:bg-gradient-to-b before:from-blue-500/10 before:to-blue-500/5\",\n                    \"after:absolute after:inset-0 after:-z-10 after:rounded-[calc(theme(borderRadius.lg)-1px)]\",\n                    \"after:shadow-[inset_0_1px_theme(colors.blue.200/20%)]\",\n                    \"[&>svg]:text-blue-600 dark:[&>svg]:text-blue-500 shadow-sm shadow-blue-100 dark:shadow-blue-900/20\",\n                ],\n                glass: [\n                    \"border border-white/20 bg-white/10 backdrop-blur-md text-foreground\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.lg)-1px)]\",\n                    \"before:bg-gradient-to-b before:from-white/15 before:to-white/5\",\n                    \"shadow-lg shadow-black/10\",\n                    \"dark:border-white/10 dark:bg-white/5 dark:before:from-white/10 dark:before:to-white/[0.02]\",\n                ],\n            },\n            borderStyle: {\n                default: \"\",\n                accent: \"border-l-4 pl-6\",\n                solid: \"border-2\",\n                highlight: [\n                    \"after:absolute after:top-0 after:left-0 after:h-full after:w-1 after:bg-current after:rounded-l-md after:z-10\",\n                    \"pl-6\",\n                ],\n            },\n            rounded: {\n                default: \"rounded-lg\",\n                none: \"rounded-none\",\n                full: \"rounded-xl\",\n                pill: \"rounded-2xl\",\n            },\n            hasIcon: {\n                true: \"[&>svg]:inline-block\",\n                false: \"[&>svg]:hidden pl-4 [&>div]:pl-0\",\n            },\n            depth: {\n                none: \"shadow-none\",\n                sm: \"shadow-sm\",\n                md: \"shadow-md\",\n                lg: \"shadow-lg\",\n                xl: \"shadow-xl\",\n            },\n            animation: {\n                none: \"\",\n                fade: \"animate-in fade-in-0 duration-300\",\n                slide: \"animate-in slide-in-from-top-2 duration-300\",\n                scale: \"animate-in zoom-in-95 duration-300\",\n                bounce: \"animate-bounce\",\n            },\n        },\n        defaultVariants: {\n            variant: \"default\",\n            borderStyle: \"default\",\n            rounded: \"default\",\n            hasIcon: true,\n            depth: \"sm\",\n            animation: \"none\",\n        },\n    }\n)\n\nconst iconMap = {\n    default: AlertCircle,\n    destructive: XCircle,\n    warning: AlertTriangle,\n    success: CheckCircle2,\n    info: Info,\n    glass: Info,\n}\n\nexport interface AlertProps\n    extends React.HTMLAttributes<HTMLDivElement>,\n        VariantProps<typeof alertVariants> {\n    icon?: React.ReactNode\n    dismissible?: boolean\n    onDismiss?: () => void\n}\n\nconst Alert = React.forwardRef<HTMLDivElement, AlertProps>(\n    ({\n         className,\n         variant = \"default\",\n         borderStyle,\n         rounded,\n         hasIcon = true,\n         depth,\n         animation,\n         icon,\n         dismissible,\n         onDismiss,\n         children,\n         ...props\n     }, ref) => {\n        // Determine which icon to use\n        const shouldShowIcon = hasIcon\n        let alertIcon = null\n\n        if (shouldShowIcon) {\n            if (icon) {\n                alertIcon = icon\n            } else {\n                const IconComponent = iconMap[variant as keyof typeof iconMap]\n                alertIcon = <IconComponent className=\"h-4 w-4\"/>\n            }\n        }\n\n        return (\n            <div\n                ref={ref}\n                role=\"alert\"\n                className={cn(\n                    alertVariants({\n                        variant,\n                        borderStyle,\n                        rounded,\n                        hasIcon: shouldShowIcon,\n                        depth,\n                        animation,\n                    }),\n                    dismissible && \"pr-12\",\n                    className\n                )}\n                {...props}\n            >\n                {alertIcon}\n                <div className={cn(!shouldShowIcon && \"pl-0\")}>\n                    {children}\n                </div>\n                {dismissible && onDismiss && (\n                    <button\n                        onClick={onDismiss}\n                        className=\"absolute right-4 top-4 rounded-md p-1 text-muted-foreground/60 hover:text-foreground hover:bg-accent/30 transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\"\n                        aria-label=\"Dismiss alert\"\n                    >\n                        <XCircle className=\"h-4 w-4\"/>\n                    </button>\n                )}\n            </div>\n        )\n    }\n)\nAlert.displayName = \"Alert\"\n\nconst AlertTitle = React.forwardRef<\n    HTMLParagraphElement,\n    React.HTMLAttributes<HTMLHeadingElement>\n>(({className, ...props}, ref) => (\n    <h5\n        ref={ref}\n        className={cn(\n            \"mb-1 font-semibold leading-none tracking-tight bg-gradient-to-r from-foreground to-foreground/80 bg-clip-text\",\n            className\n        )}\n        {...props}\n    />\n))\nAlertTitle.displayName = \"AlertTitle\"\n\nconst AlertDescription = React.forwardRef<\n    HTMLParagraphElement,\n    React.HTMLAttributes<HTMLParagraphElement>\n>(({className, ...props}, ref) => (\n    <div\n        ref={ref}\n        className={cn(\"text-sm leading-relaxed [&_p]:leading-relaxed opacity-90\", className)}\n        {...props}\n    />\n))\nAlertDescription.displayName = \"AlertDescription\"\n\nconst AlertActions = React.forwardRef<\n    HTMLDivElement,\n    React.HTMLAttributes<HTMLDivElement>\n>(({className, ...props}, ref) => (\n    <div\n        ref={ref}\n        className={cn(\n            \"mt-3 flex flex-wrap items-center gap-2 pt-2 border-t border-current/10\",\n            className\n        )}\n        {...props}\n    />\n))\nAlertActions.displayName = \"AlertActions\"\n\nexport {Alert, AlertTitle, AlertDescription, AlertActions}"
  },
  {
    "path": "components/ui/avatar.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Avatar = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full\",\n      className\n    )}\n    {...props}\n  />\n))\nAvatar.displayName = AvatarPrimitive.Root.displayName\n\nconst AvatarImage = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Image>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Image\n    ref={ref}\n    className={cn(\"aspect-square h-full w-full\", className)}\n    {...props}\n  />\n))\nAvatarImage.displayName = AvatarPrimitive.Image.displayName\n\nconst AvatarFallback = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Fallback>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Fallback\n    ref={ref}\n    className={cn(\n      \"flex h-full w-full items-center justify-center rounded-full bg-muted\",\n      className\n    )}\n    {...props}\n  />\n))\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName\n\nexport { Avatar, AvatarImage, AvatarFallback }\n"
  },
  {
    "path": "components/ui/badge.tsx",
    "content": "import * as React from \"react\"\nimport {cva, type VariantProps} from \"class-variance-authority\"\nimport {cn} from \"@/lib/utils\"\n\nconst badgeVariants = cva(\n    \"relative isolate inline-flex items-center justify-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\",\n    {\n        variants: {\n            variant: {\n                default: [\n                    \"border border-primary/20 bg-primary text-primary-foreground\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-full before:bg-primary before:shadow-sm\",\n                    \"after:absolute after:inset-0 after:-z-10 after:rounded-full\",\n                    \"after:shadow-[inset_0_1px_theme(colors.white/15%)]\",\n                    \"hover:after:bg-white/10\",\n                    \"dark:border-primary/30 dark:before:hidden dark:after:-inset-px\",\n                ],\n                secondary: [\n                    \"border border-secondary/20 bg-secondary text-secondary-foreground\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-full before:bg-secondary before:shadow-sm\",\n                    \"after:absolute after:inset-0 after:-z-10 after:rounded-full\",\n                    \"hover:after:bg-foreground/5\",\n                    \"dark:before:hidden dark:after:-inset-px\",\n                ],\n                destructive: [\n                    \"border border-destructive/20 bg-destructive text-destructive-foreground\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-full before:bg-destructive before:shadow-sm\",\n                    \"after:absolute after:inset-0 after:-z-10 after:rounded-full\",\n                    \"after:shadow-[inset_0_1px_theme(colors.white/15%)]\",\n                    \"hover:after:bg-white/10\",\n                    \"dark:border-destructive/30 dark:before:hidden dark:after:-inset-px\",\n                ],\n                outline: [\n                    \"border border-border/60 bg-background text-foreground\",\n                    \"hover:bg-accent/30 hover:text-accent-foreground hover:border-border/80\",\n                    \"dark:border-border/40\",\n                ],\n                ghost: [\n                    \"border border-transparent bg-transparent text-foreground\",\n                    \"hover:bg-accent/30 hover:text-accent-foreground\",\n                ],\n                success: [\n                    \"border border-green-500/20 bg-green-500/10 text-green-700 dark:text-green-400\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-full before:bg-gradient-to-b before:from-green-500/20 before:to-green-500/10\",\n                    \"hover:bg-green-500/20\",\n                    \"dark:bg-green-950/20 dark:border-green-500/30\",\n                ],\n                warning: [\n                    \"border border-yellow-500/20 bg-yellow-500/10 text-yellow-700 dark:text-yellow-400\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-full before:bg-gradient-to-b before:from-yellow-500/20 before:to-yellow-500/10\",\n                    \"hover:bg-yellow-500/20\",\n                    \"dark:bg-yellow-950/20 dark:border-yellow-500/30\",\n                ],\n                info: [\n                    \"border border-blue-500/20 bg-blue-500/10 text-blue-700 dark:text-blue-400\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-full before:bg-gradient-to-b before:from-blue-500/20 before:to-blue-500/10\",\n                    \"hover:bg-blue-500/20\",\n                    \"dark:bg-blue-950/20 dark:border-blue-500/30\",\n                ],\n                glass: [\n                    \"border border-white/20 bg-white/10 backdrop-blur-sm text-foreground\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-full\",\n                    \"before:bg-gradient-to-b before:from-white/15 before:to-white/5\",\n                    \"hover:bg-white/20 hover:border-white/30\",\n                    \"dark:border-white/10 dark:bg-white/5 dark:before:from-white/10 dark:before:to-white/[0.02]\",\n                ],\n            },\n            size: {\n                default: \"h-5 px-2.5 py-0.5 text-xs\",\n                sm: \"h-4 px-2 py-0 text-xs\",\n                lg: \"h-6 px-3 py-1 text-sm\",\n                xl: \"h-8 px-4 py-1.5 text-sm font-semibold\",\n            },\n            glow: {\n                none: \"\",\n                subtle: \"shadow-sm\",\n                medium: \"shadow-md shadow-current/20\",\n                strong: \"shadow-lg shadow-current/30\",\n                intense: \"shadow-xl shadow-current/40 animate-pulse\",\n            },\n            interactive: {\n                true: \"cursor-pointer active:scale-95\",\n                false: \"\",\n            },\n        },\n        defaultVariants: {\n            variant: \"default\",\n            size: \"default\",\n            glow: \"none\",\n            interactive: false,\n        },\n    }\n)\n\nfunction getContrastColor(hexColor: string): string {\n    const r = parseInt(hexColor.slice(1, 3), 16)\n    const g = parseInt(hexColor.slice(3, 5), 16)\n    const b = parseInt(hexColor.slice(5, 7), 16)\n    const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255\n    return luminance > 0.5 ? \"#000000\" : \"#ffffff\"\n}\n\nexport interface BadgeProps\n    extends React.HTMLAttributes<HTMLDivElement>,\n        VariantProps<typeof badgeVariants> {\n    icon?: React.ReactNode\n    dot?: boolean\n    customColor?: string | null\n}\n\nfunction Badge({\n                   className,\n                   variant,\n                   size,\n                   glow,\n                   interactive,\n                   icon,\n                   dot,\n                   customColor,\n                   style,\n                   children,\n                   ...props\n               }: BadgeProps) {\n    const customStyle =\n        customColor && variant !== \"outline\"\n            ? {\n                backgroundColor: customColor,\n                color: getContrastColor(customColor),\n                borderColor: `${customColor}33`,\n                \"--badge-color\": customColor,\n                ...style,\n            }\n            : customColor && variant === \"outline\"\n                ? {\n                    backgroundColor: \"transparent\",\n                    color: customColor,\n                    borderColor: customColor,\n                    \"--badge-color\": customColor,\n                    ...style,\n                }\n                : style\n\n    const colorOverrideClass =\n        customColor && variant !== \"outline\"\n            ? \"before:!bg-[var(--badge-color)] after:!shadow-[inset_0_1px_rgba(255,255,255,0.15)]\"\n            : \"\"\n\n    return (\n        <div\n            className={cn(\n                badgeVariants({variant, size, glow, interactive}),\n                colorOverrideClass,\n                className\n            )}\n            style={customStyle}\n            {...props}\n        >\n            {dot && (\n                <span className=\"mr-1 h-1.5 w-1.5 rounded-full bg-current opacity-70\"/>\n            )}\n            {icon && (\n                <span className=\"shrink-0 [&>svg]:h-3 [&>svg]:w-3\">{icon}</span>\n            )}\n            {children}\n        </div>\n    )\n}\n\nexport {Badge, badgeVariants}\n"
  },
  {
    "path": "components/ui/breadcrumb.tsx",
    "content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { ChevronRight, MoreHorizontal } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Breadcrumb({ ...props }: React.ComponentProps<\"nav\">) {\n    return <nav aria-label=\"breadcrumb\" data-slot=\"breadcrumb\" {...props} />\n}\n\nfunction BreadcrumbList({ className, ...props }: React.ComponentProps<\"ol\">) {\n    return (\n        <ol\n            data-slot=\"breadcrumb-list\"\n            className={cn(\n                \"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5\",\n                className\n            )}\n            {...props}\n        />\n    )\n}\n\nfunction BreadcrumbItem({ className, ...props }: React.ComponentProps<\"li\">) {\n    return (\n        <li\n            data-slot=\"breadcrumb-item\"\n            className={cn(\"inline-flex items-center gap-1.5\", className)}\n            {...props}\n        />\n    )\n}\n\nfunction BreadcrumbLink({\n                            asChild,\n                            className,\n                            ...props\n                        }: React.ComponentProps<\"a\"> & {\n    asChild?: boolean\n}) {\n    const Comp = asChild ? Slot : \"a\"\n\n    return (\n        <Comp\n            data-slot=\"breadcrumb-link\"\n            className={cn(\"hover:text-foreground transition-colors\", className)}\n            {...props}\n        />\n    )\n}\n\nfunction BreadcrumbPage({ className, ...props }: React.ComponentProps<\"span\">) {\n    return (\n        <span\n            data-slot=\"breadcrumb-page\"\n            role=\"link\"\n            aria-disabled=\"true\"\n            aria-current=\"page\"\n            className={cn(\"text-foreground font-normal\", className)}\n            {...props}\n        />\n    )\n}\n\nfunction BreadcrumbSeparator({\n                                 children,\n                                 className,\n                                 ...props\n                             }: React.ComponentProps<\"li\">) {\n    return (\n        <li\n            data-slot=\"breadcrumb-separator\"\n            role=\"presentation\"\n            aria-hidden=\"true\"\n            className={cn(\"[&>svg]:size-3.5\", className)}\n            {...props}\n        >\n            {children ?? <ChevronRight />}\n        </li>\n    )\n}\n\nfunction BreadcrumbEllipsis({\n                                className,\n                                ...props\n                            }: React.ComponentProps<\"span\">) {\n    return (\n        <span\n            data-slot=\"breadcrumb-ellipsis\"\n            role=\"presentation\"\n            aria-hidden=\"true\"\n            className={cn(\"flex size-9 items-center justify-center\", className)}\n            {...props}\n        >\n      <MoreHorizontal className=\"size-4\" />\n      <span className=\"sr-only\">More</span>\n    </span>\n    )\n}\n\nexport {\n    Breadcrumb,\n    BreadcrumbList,\n    BreadcrumbItem,\n    BreadcrumbLink,\n    BreadcrumbPage,\n    BreadcrumbSeparator,\n    BreadcrumbEllipsis,\n}\n"
  },
  {
    "path": "components/ui/breadcrumbs.tsx",
    "content": "'use client'\n\nimport React from 'react'\nimport Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport { ChevronRight } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { useQuery } from '@tanstack/react-query'\n\n// Fetch project name util function\nasync function fetchProjectName(projectId: string) {\n    try {\n        const response = await fetch(`/api/projects/${projectId}`)\n        if (!response.ok) return null\n        const project = await response.json()\n        return project.name\n    } catch (error: unknown) {\n        console.log(error)\n        return null\n    }\n}\n\n// Utility to humanize route segments\nfunction humanizeSegment(segment: string) {\n    return segment\n        .replace(/-/g, ' ')\n        .replace(/\\w\\S*/g, (txt) =>\n            txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()\n        )\n}\n\nexport function Breadcrumbs() {\n    const pathname = usePathname()\n    const pathSegments = pathname.split('/').filter(segment => segment)\n\n    // Check if we have a project ID in the path (assuming 25-char alphanumeric)\n    const projectIdIndex = pathSegments.findIndex(segment =>\n        /^[a-z0-9]{25}$/.test(segment)\n    )\n\n    // Custom hook to fetch project name if applicable\n    const useProjectName = (projectId?: string) => {\n        return useQuery({\n            queryKey: ['project-name', projectId],\n            queryFn: () => projectId ? fetchProjectName(projectId) : null,\n            enabled: !!projectId,\n        })\n    }\n\n    // Fetch project name if applicable\n    const { data: projectName } = useProjectName(\n        projectIdIndex !== -1 ? pathSegments[projectIdIndex] : undefined\n    )\n\n    return (\n        <nav aria-label=\"Breadcrumb\" className=\"flex items-center space-x-1 text-sm\">\n            <ol className=\"flex items-center space-x-1 overflow-x-auto max-w-full\">\n                {pathSegments.map((segment, index) => {\n                    const href = `/${pathSegments.slice(0, index + 1).join('/')}`\n                    const isLast = index === pathSegments.length - 1\n\n                    // Replace project ID with project name if available\n                    const displaySegment =\n                        projectName && index === projectIdIndex\n                            ? projectName\n                            : humanizeSegment(segment)\n\n                    return (\n                        <li\n                            key={href}\n                            className=\"flex items-center min-w-0 whitespace-nowrap\"\n                        >\n                            {index > 0 && (\n                                <ChevronRight\n                                    className=\"h-4 w-4 mx-1 flex-shrink-0 text-gray-400\"\n                                    aria-hidden=\"true\"\n                                />\n                            )}\n                            <Link\n                                href={href}\n                                className={cn(\n                                    \"max-w-[200px] truncate transition-colors duration-200\",\n                                    isLast\n                                        ? \"text-foreground font-semibold cursor-default\"\n                                        : \"text-muted-foreground hover:text-foreground hover:underline\"\n                                )}\n                            >\n                                {displaySegment}\n                            </Link>\n                        </li>\n                    )\n                })}\n            </ol>\n        </nav>\n    )\n}"
  },
  {
    "path": "components/ui/button.tsx",
    "content": "import * as React from \"react\"\nimport {Slot} from \"@radix-ui/react-slot\"\nimport {cva, type VariantProps} from \"class-variance-authority\"\nimport {cn} from \"@/lib/utils\"\n\nconst buttonVariants = cva(\n    \"relative isolate inline-flex items-center justify-center gap-2 whitespace-nowrap font-semibold text-sm transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n    {\n        variants: {\n            variant: {\n                default: [\n                    // Base styling with the optical border\n                    \"border border-primary/20 bg-primary text-primary-foreground\",\n                    // Background layer for depth\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(var(--radius)-1px)] before:bg-primary before:shadow-sm\",\n                    // Overlay layer for hover effects\n                    \"after:absolute after:inset-0 after:-z-10 after:rounded-[calc(var(--radius)-1px)]\",\n                    // Inner highlight\n                    \"after:shadow-[inset_0_1px_theme(colors.white/15%)]\",\n                    // Hover overlay\n                    \"hover:after:bg-white/10 active:after:bg-white/5\",\n                    // Dark mode adjustments\n                    \"dark:border-primary/30 dark:before:hidden dark:after:-inset-px\",\n                ],\n                destructive: [\n                    \"border border-destructive/20 bg-destructive text-destructive-foreground\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(var(--radius)-1px)] before:bg-destructive before:shadow-sm\",\n                    \"after:absolute after:inset-0 after:-z-10 after:rounded-[calc(var(--radius)-1px)]\",\n                    \"after:shadow-[inset_0_1px_theme(colors.white/15%)]\",\n                    \"hover:after:bg-white/10 active:after:bg-white/5\",\n                    \"dark:border-destructive/30 dark:before:hidden dark:after:-inset-px\",\n                ],\n                success: [\n                    \"border border-success/20 bg-green-500 dark:bg-green-600 text-white\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(var(--radius)-1px)] before:bg-green-500 dark:before:bg-green-600 before:shadow-sm\",\n                    \"after:absolute after:inset-0 after:-z-10 after:rounded-[calc(var(--radius)-1px)]\",\n                    \"after:shadow-[inset_0_1px_theme(colors.white/15%)]\",\n                    \"hover:after:bg-white/10 active:after:bg-white/5\",\n                    \"dark:border-green-500/30 dark:before:hidden dark:after:-inset-px\",\n                ],\n                outline: [\n                    \"border border-border bg-background text-foreground\",\n                    \"hover:bg-accent/50 hover:text-accent-foreground\",\n                    \"active:bg-accent/80\",\n                    \"dark:border-border/50 dark:hover:bg-accent/30\",\n                ],\n                secondary: [\n                    \"border border-secondary/20 bg-secondary text-secondary-foreground\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(var(--radius)-1px)] before:bg-secondary before:shadow-sm\",\n                    \"after:absolute after:inset-0 after:-z-10 after:rounded-[calc(var(--radius)-1px)]\",\n                    \"hover:after:bg-foreground/5 active:after:bg-foreground/10\",\n                    \"dark:before:hidden dark:after:-inset-px\",\n                ],\n                ghost: [\n                    \"border border-transparent text-foreground\",\n                    \"hover:bg-accent/50 hover:text-accent-foreground\",\n                    \"active:bg-accent/80\",\n                ],\n                link: [\n                    \"border border-transparent text-primary\",\n                    \"underline-offset-4 hover:underline\",\n                    \"after:absolute after:bottom-0 after:left-0 after:h-[1px] after:w-0 after:bg-current after:transition-all\",\n                    \"hover:after:w-full\",\n                ],\n            },\n            size: {\n                default: \"h-10 px-4 py-2 rounded-lg text-sm\",\n                sm: \"h-8 px-3 py-1.5 rounded-md text-xs\",\n                lg: \"h-12 px-6 py-3 rounded-xl text-base\",\n                icon: \"h-10 w-10 rounded-lg\",\n                pill: \"h-10 px-6 rounded-full text-sm\",\n            },\n            animation: {\n                none: \"\",\n                bounce: \"active:scale-95\",\n                scale: \"hover:scale-105 active:scale-95\",\n                slide: \"active:translate-y-0.5\",\n            }\n        },\n        defaultVariants: {\n            variant: \"default\",\n            size: \"default\",\n            animation: \"none\",\n        },\n    }\n)\n\nexport interface ButtonProps\n    extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n        VariantProps<typeof buttonVariants> {\n    asChild?: boolean\n    isLoading?: boolean\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n    ({className, variant, size, animation = \"none\", asChild = false, isLoading, children, ...props}, ref) => {\n        // Use the correct component based on asChild prop\n        if (asChild) {\n            return (\n                <Slot\n                    ref={ref}\n                    className={cn(buttonVariants({variant, size, animation, className}))}\n                    {...props}\n                >\n                    {isLoading ? (\n                        <>\n                            <span\n                                className=\"mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent\"/>\n                            <span>Loading...</span>\n                        </>\n                    ) : (\n                        children\n                    )}\n                </Slot>\n            );\n        }\n\n        // Add animation classes instead of using framer-motion props directly\n        const animationClasses = {\n            none: \"\",\n            bounce: \"active:translate-y-1 transition-transform\",\n            scale: \"active:scale-95 transition-transform\",\n            slide: \"active:translate-x-1 transition-transform\"\n        };\n\n        return (\n            <button\n                ref={ref}\n                className={cn(\n                    buttonVariants({variant, size, animation, className}),\n                    animation && animation !== \"none\" ? animationClasses[animation] : \"\"\n                )}\n                {...props}\n            >\n                {isLoading ? (\n                    <>\n                        <span\n                            className=\"mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent\"/>\n                        <span>Loading...</span>\n                    </>\n                ) : (\n                    children\n                )}\n            </button>\n        );\n    }\n)\nButton.displayName = \"Button\"\n\nexport {Button, buttonVariants}"
  },
  {
    "path": "components/ui/calendar.tsx",
    "content": "\"use client\"\n\nimport { Button, buttonVariants } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\nimport { differenceInCalendarDays } from \"date-fns\"\nimport { ChevronLeft, ChevronRight } from \"lucide-react\"\nimport * as React from \"react\"\nimport {\n    DayPicker,\n    labelNext,\n    labelPrevious,\n    useDayPicker,\n    type DayPickerProps,\n} from \"react-day-picker\"\n\nexport type CalendarProps = DayPickerProps & {\n    /**\n     * In the year view, the number of years to display at once.\n     * @default 12\n     */\n    yearRange?: number\n\n    /**\n     * Wether to show the year switcher in the caption.\n     * @default true\n     */\n    showYearSwitcher?: boolean\n\n    monthsClassName?: string\n    monthCaptionClassName?: string\n    weekdaysClassName?: string\n    weekdayClassName?: string\n    monthClassName?: string\n    captionClassName?: string\n    captionLabelClassName?: string\n    buttonNextClassName?: string\n    buttonPreviousClassName?: string\n    navClassName?: string\n    monthGridClassName?: string\n    weekClassName?: string\n    dayClassName?: string\n    dayButtonClassName?: string\n    rangeStartClassName?: string\n    rangeEndClassName?: string\n    selectedClassName?: string\n    todayClassName?: string\n    outsideClassName?: string\n    disabledClassName?: string\n    rangeMiddleClassName?: string\n    hiddenClassName?: string\n}\n\ntype NavView = \"days\" | \"years\"\n\n/**\n * A custom calendar component built on top of react-day-picker.\n * @param props The props for the calendar.\n * @default yearRange 12\n * @returns\n */\nfunction Calendar({\n                      className,\n                      showOutsideDays = true,\n                      showYearSwitcher = true,\n                      yearRange = 12,\n                      numberOfMonths,\n                      ...props\n                  }: CalendarProps) {\n    const [navView, setNavView] = React.useState<NavView>(\"days\")\n    const [displayYears, setDisplayYears] = React.useState<{\n        from: number\n        to: number\n    }>(\n        React.useMemo(() => {\n            const currentYear = new Date().getFullYear()\n            return {\n                from: currentYear - Math.floor(yearRange / 2 - 1),\n                to: currentYear + Math.ceil(yearRange / 2),\n            }\n        }, [yearRange])\n    )\n\n    const { onNextClick, onPrevClick, startMonth, endMonth } = props\n\n    const columnsDisplayed = navView === \"years\" ? 1 : numberOfMonths\n\n    const _monthsClassName = cn(\"relative flex\", props.monthsClassName)\n    const _monthCaptionClassName = cn(\n        \"relative mx-10 flex h-7 items-center justify-center\",\n        props.monthCaptionClassName\n    )\n    const _weekdaysClassName = cn(\"flex flex-row\", props.weekdaysClassName)\n    const _weekdayClassName = cn(\n        \"w-8 text-sm font-normal text-muted-foreground\",\n        props.weekdayClassName\n    )\n    const _monthClassName = cn(\"w-full\", props.monthClassName)\n    const _captionClassName = cn(\n        \"relative flex items-center justify-center pt-1\",\n        props.captionClassName\n    )\n    const _captionLabelClassName = cn(\n        \"truncate text-sm font-medium\",\n        props.captionLabelClassName\n    )\n    const buttonNavClassName = buttonVariants({\n        variant: \"outline\",\n        className:\n            \"absolute h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100\",\n    })\n    const _buttonNextClassName = cn(\n        buttonNavClassName,\n        \"right-0\",\n        props.buttonNextClassName\n    )\n    const _buttonPreviousClassName = cn(\n        buttonNavClassName,\n        \"left-0\",\n        props.buttonPreviousClassName\n    )\n    const _navClassName = cn(\"flex items-start\", props.navClassName)\n    const _monthGridClassName = cn(\"mx-auto mt-4\", props.monthGridClassName)\n    const _weekClassName = cn(\"mt-2 flex w-max items-start\", props.weekClassName)\n    const _dayClassName = cn(\n        \"flex size-8 flex-1 items-center justify-center p-0 text-sm\",\n        props.dayClassName\n    )\n    const _dayButtonClassName = cn(\n        buttonVariants({ variant: \"ghost\" }),\n        \"size-8 rounded-md p-0 font-normal transition-none aria-selected:opacity-100\",\n        props.dayButtonClassName\n    )\n    const buttonRangeClassName =\n        \"bg-accent [&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary [&>button]:hover:text-primary-foreground\"\n    const _rangeStartClassName = cn(\n        buttonRangeClassName,\n        \"day-range-start rounded-s-md\",\n        props.rangeStartClassName\n    )\n    const _rangeEndClassName = cn(\n        buttonRangeClassName,\n        \"day-range-end rounded-e-md\",\n        props.rangeEndClassName\n    )\n    const _rangeMiddleClassName = cn(\n        \"bg-accent !text-foreground [&>button]:bg-transparent [&>button]:!text-foreground [&>button]:hover:bg-transparent [&>button]:hover:!text-foreground\",\n        props.rangeMiddleClassName\n    )\n    const _selectedClassName = cn(\n        \"[&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary [&>button]:hover:text-primary-foreground\",\n        props.selectedClassName\n    )\n    const _todayClassName = cn(\n        \"[&>button]:bg-accent [&>button]:text-accent-foreground\",\n        props.todayClassName\n    )\n    const _outsideClassName = cn(\n        \"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30\",\n        props.outsideClassName\n    )\n    const _disabledClassName = cn(\n        \"text-muted-foreground opacity-50\",\n        props.disabledClassName\n    )\n    const _hiddenClassName = cn(\"invisible flex-1\", props.hiddenClassName)\n\n    return (\n        <DayPicker\n            showOutsideDays={showOutsideDays}\n            className={cn(\"p-3\", className)}\n            style={{\n                width: 248.8 * (columnsDisplayed ?? 1) + \"px\",\n            }}\n            classNames={{\n                months: _monthsClassName,\n                month_caption: _monthCaptionClassName,\n                weekdays: _weekdaysClassName,\n                weekday: _weekdayClassName,\n                month: _monthClassName,\n                caption: _captionClassName,\n                caption_label: _captionLabelClassName,\n                button_next: _buttonNextClassName,\n                button_previous: _buttonPreviousClassName,\n                nav: _navClassName,\n                month_grid: _monthGridClassName,\n                week: _weekClassName,\n                day: _dayClassName,\n                day_button: _dayButtonClassName,\n                range_start: _rangeStartClassName,\n                range_middle: _rangeMiddleClassName,\n                range_end: _rangeEndClassName,\n                selected: _selectedClassName,\n                today: _todayClassName,\n                outside: _outsideClassName,\n                disabled: _disabledClassName,\n                hidden: _hiddenClassName,\n            }}\n            components={{\n                Chevron: ({ orientation }) => {\n                    const Icon = orientation === \"left\" ? ChevronLeft : ChevronRight\n                    return <Icon className=\"h-4 w-4\" />\n                },\n                Nav: ({ className }) => (\n                    <Nav\n                        className={className}\n                        displayYears={displayYears}\n                        navView={navView}\n                        setDisplayYears={setDisplayYears}\n                        startMonth={startMonth}\n                        endMonth={endMonth}\n                        onPrevClick={onPrevClick}\n                        onNextClick={onNextClick}\n                    />\n                ),\n                CaptionLabel: (props) => (\n                    <CaptionLabel\n                        showYearSwitcher={showYearSwitcher}\n                        navView={navView}\n                        setNavView={setNavView}\n                        displayYears={displayYears}\n                        {...props}\n                    />\n                ),\n                MonthGrid: ({ className, ...props }) => (\n                    <MonthGrid\n                        className={className}\n                        displayYears={displayYears}\n                        startMonth={startMonth}\n                        endMonth={endMonth}\n                        navView={navView}\n                        setNavView={setNavView}\n                        {...props}\n                    />\n                ),\n            }}\n            numberOfMonths={columnsDisplayed}\n            {...props}\n        />\n    )\n}\nCalendar.displayName = \"Calendar\"\n\nfunction Nav({\n                 className,\n                 navView,\n                 startMonth,\n                 endMonth,\n                 displayYears,\n                 setDisplayYears,\n                 onPrevClick,\n                 onNextClick,\n             }: {\n    className?: string\n    navView: NavView\n    startMonth?: Date\n    endMonth?: Date\n    displayYears: { from: number; to: number }\n    setDisplayYears: React.Dispatch<\n        React.SetStateAction<{ from: number; to: number }>\n    >\n    onPrevClick?: (date: Date) => void\n    onNextClick?: (date: Date) => void\n}) {\n    const { nextMonth, previousMonth, goToMonth } = useDayPicker()\n\n    const isPreviousDisabled = (() => {\n        if (navView === \"years\") {\n            return (\n                (startMonth &&\n                    differenceInCalendarDays(\n                        new Date(displayYears.from - 1, 0, 1),\n                        startMonth\n                    ) < 0) ||\n                (endMonth &&\n                    differenceInCalendarDays(\n                        new Date(displayYears.from - 1, 0, 1),\n                        endMonth\n                    ) > 0)\n            )\n        }\n        return !previousMonth\n    })()\n\n    const isNextDisabled = (() => {\n        if (navView === \"years\") {\n            return (\n                (startMonth &&\n                    differenceInCalendarDays(\n                        new Date(displayYears.to + 1, 0, 1),\n                        startMonth\n                    ) < 0) ||\n                (endMonth &&\n                    differenceInCalendarDays(\n                        new Date(displayYears.to + 1, 0, 1),\n                        endMonth\n                    ) > 0)\n            )\n        }\n        return !nextMonth\n    })()\n\n    const handlePreviousClick = React.useCallback(() => {\n        if (!previousMonth) return\n        if (navView === \"years\") {\n            setDisplayYears((prev) => ({\n                from: prev.from - (prev.to - prev.from + 1),\n                to: prev.to - (prev.to - prev.from + 1),\n            }))\n            onPrevClick?.(\n                new Date(\n                    displayYears.from - (displayYears.to - displayYears.from),\n                    0,\n                    1\n                )\n            )\n            return\n        }\n        goToMonth(previousMonth)\n        onPrevClick?.(previousMonth)\n    }, [previousMonth, goToMonth, onPrevClick, navView, displayYears, setDisplayYears])\n\n    const handleNextClick = React.useCallback(() => {\n        if (!nextMonth) return\n        if (navView === \"years\") {\n            setDisplayYears((prev) => ({\n                from: prev.from + (prev.to - prev.from + 1),\n                to: prev.to + (prev.to - prev.from + 1),\n            }))\n            onNextClick?.(\n                new Date(\n                    displayYears.from + (displayYears.to - displayYears.from),\n                    0,\n                    1\n                )\n            )\n            return\n        }\n        goToMonth(nextMonth)\n        onNextClick?.(nextMonth)\n    }, [goToMonth, nextMonth, onNextClick, navView, displayYears, setDisplayYears])\n\n    return (\n        <nav className={cn(\"flex items-center\", className)}>\n            <Button\n                variant=\"outline\"\n                className=\"absolute left-0 h-7 w-7 bg-transparent p-0 opacity-80 hover:opacity-100\"\n                type=\"button\"\n                tabIndex={isPreviousDisabled ? undefined : -1}\n                disabled={isPreviousDisabled}\n                aria-label={\n                    navView === \"years\"\n                        ? `Go to the previous ${\n                            displayYears.to - displayYears.from + 1\n                        } years`\n                        : labelPrevious(previousMonth)\n                }\n                onClick={handlePreviousClick}\n            >\n                <ChevronLeft className=\"h-4 w-4\" />\n            </Button>\n\n            <Button\n                variant=\"outline\"\n                className=\"absolute right-0 h-7 w-7 bg-transparent p-0 opacity-80 hover:opacity-100\"\n                type=\"button\"\n                tabIndex={isNextDisabled ? undefined : -1}\n                disabled={isNextDisabled}\n                aria-label={\n                    navView === \"years\"\n                        ? `Go to the next ${displayYears.to - displayYears.from + 1} years`\n                        : labelNext(nextMonth)\n                }\n                onClick={handleNextClick}\n            >\n                <ChevronRight className=\"h-4 w-4\" />\n            </Button>\n        </nav>\n    )\n}\n\nfunction CaptionLabel({\n                          children,\n                          showYearSwitcher,\n                          navView,\n                          setNavView,\n                          displayYears,\n                          ...props\n                      }: {\n    showYearSwitcher?: boolean\n    navView: NavView\n    setNavView: React.Dispatch<React.SetStateAction<NavView>>\n    displayYears: { from: number; to: number }\n} & React.HTMLAttributes<HTMLSpanElement>) {\n    if (!showYearSwitcher) return <span {...props}>{children}</span>\n    return (\n        <Button\n            className=\"h-7 w-full truncate text-sm font-medium\"\n            variant=\"ghost\"\n            size=\"sm\"\n            onClick={() => setNavView((prev) => (prev === \"days\" ? \"years\" : \"days\"))}\n        >\n            {navView === \"days\"\n                ? children\n                : displayYears.from + \" - \" + displayYears.to}\n        </Button>\n    )\n}\n\nfunction MonthGrid({\n                       className,\n                       displayYears,\n                       startMonth,\n                       endMonth,\n                       navView,\n                       setNavView,\n                       ...props\n                   }: {\n    className?: string\n    displayYears: { from: number; to: number }\n    startMonth?: Date\n    endMonth?: Date\n    navView: NavView\n    setNavView: React.Dispatch<React.SetStateAction<NavView>>\n} & React.TableHTMLAttributes<HTMLTableElement>) {\n    if (navView === \"years\") {\n        return (\n            <YearGrid\n                displayYears={displayYears}\n                startMonth={startMonth}\n                endMonth={endMonth}\n                setNavView={setNavView}\n                navView={navView}\n                className={className}\n                {...props}\n            />\n        )\n    }\n    return (\n        <table className={className} {...props}>\n            {props.children}\n        </table>\n    )\n}\n\nfunction YearGrid({\n                      className,\n                      displayYears,\n                      startMonth,\n                      endMonth,\n                      setNavView,\n                      navView,\n                      ...props\n                  }: {\n    className?: string\n    displayYears: { from: number; to: number }\n    startMonth?: Date\n    endMonth?: Date\n    setNavView: React.Dispatch<React.SetStateAction<NavView>>\n    navView: NavView\n} & React.HTMLAttributes<HTMLDivElement>) {\n    const { goToMonth, selected } = useDayPicker()\n\n    return (\n        <div className={cn(\"grid grid-cols-4 gap-y-2\", className)} {...props}>\n            {Array.from(\n                { length: displayYears.to - displayYears.from + 1 },\n                (_, i) => {\n                    const isBefore =\n                        differenceInCalendarDays(\n                            new Date(displayYears.from + i, 11, 31),\n                            startMonth!\n                        ) < 0\n\n                    const isAfter =\n                        differenceInCalendarDays(\n                            new Date(displayYears.from + i, 0, 0),\n                            endMonth!\n                        ) > 0\n\n                    const isDisabled = isBefore || isAfter\n                    return (\n                        <Button\n                            key={i}\n                            className={cn(\n                                \"h-7 w-full text-sm font-normal text-foreground\",\n                                displayYears.from + i === new Date().getFullYear() &&\n                                \"bg-accent font-medium text-accent-foreground\"\n                            )}\n                            variant=\"ghost\"\n                            onClick={() => {\n                                setNavView(\"days\")\n                                goToMonth(\n                                    new Date(\n                                        displayYears.from + i,\n                                        (selected as Date | undefined)?.getMonth() ?? 0\n                                    )\n                                )\n                            }}\n                            disabled={navView === \"years\" ? isDisabled : undefined}\n                        >\n                            {displayYears.from + i}\n                        </Button>\n                    )\n                }\n            )}\n        </div>\n    )\n}\n\nexport { Calendar }"
  },
  {
    "path": "components/ui/card.tsx",
    "content": "import * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nconst cardVariants = cva(\n    \"relative isolate rounded-2xl border bg-card text-card-foreground overflow-hidden transition-all duration-200\",\n    {\n        variants: {\n            variant: {\n                default: [\n                    // Base styling with optical border\n                    \"border border-border/40 bg-card shadow-sm\",\n                    // Background layer for depth\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.2xl)-1px)] before:bg-gradient-to-b before:from-background/80 before:to-background/40\",\n                    // Subtle highlight\n                    \"after:absolute after:inset-0 after:-z-10 after:rounded-[calc(theme(borderRadius.2xl)-1px)]\",\n                    \"after:shadow-[inset_0_1px_theme(colors.white/10%)]\",\n                    // Hover effects\n                    \"hover:shadow-md hover:border-border/60 hover:after:shadow-[inset_0_1px_theme(colors.white/15%)]\",\n                    // Dark mode adjustments\n                    \"dark:border-border/20 dark:before:from-background/20 dark:before:to-background/5\",\n                ],\n                elevated: [\n                    \"border border-border/30 bg-card shadow-lg\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.2xl)-1px)] before:bg-gradient-to-b before:from-background/90 before:to-background/60\",\n                    \"after:absolute after:inset-0 after:-z-10 after:rounded-[calc(theme(borderRadius.2xl)-1px)]\",\n                    \"after:shadow-[inset_0_1px_theme(colors.white/20%)]\",\n                    \"hover:shadow-xl hover:border-border/50\",\n                    \"dark:border-border/10 dark:before:from-background/30 dark:before:to-background/10\",\n                ],\n                outlined: [\n                    \"border-2 border-border/60 bg-card/80 shadow-none backdrop-blur-sm\",\n                    \"hover:border-border/80 hover:bg-card/90\",\n                    \"dark:border-border/40 dark:hover:border-border/60\",\n                ],\n                soft: [\n                    \"border border-transparent bg-card/50 backdrop-blur-sm shadow-none\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.2xl)-1px)]\",\n                    \"before:bg-gradient-to-b before:from-background/30 before:to-background/10\",\n                    \"hover:bg-card/70 hover:before:from-background/40 hover:before:to-background/20\",\n                ],\n                glass: [\n                    \"border border-white/20 bg-white/5 backdrop-blur-md shadow-lg\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.2xl)-1px)]\",\n                    \"before:bg-gradient-to-b before:from-white/10 before:to-white/5\",\n                    \"hover:bg-white/10 hover:border-white/30\",\n                    \"dark:border-white/10 dark:bg-white/[0.02] dark:before:from-white/5 dark:before:to-white/[0.02]\",\n                ],\n            },\n            interactive: {\n                true: \"cursor-pointer hover:scale-[1.02] active:scale-[0.98] transition-transform\",\n                false: \"\",\n            },\n        },\n        defaultVariants: {\n            variant: \"default\",\n            interactive: false,\n        },\n    }\n)\n\nexport interface CardProps\n    extends React.HTMLAttributes<HTMLDivElement>,\n        VariantProps<typeof cardVariants> {}\n\nconst Card = React.forwardRef<HTMLDivElement, CardProps>(\n    ({ className, variant, interactive, ...props }, ref) => (\n        <div\n            ref={ref}\n            className={cn(cardVariants({ variant, interactive, className }))}\n            {...props}\n        />\n    )\n)\nCard.displayName = \"Card\"\n\nconst CardHeader = React.forwardRef<\n    HTMLDivElement,\n    React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n    <div\n        ref={ref}\n        className={cn(\"flex flex-col space-y-1.5 p-6 pb-4\", className)}\n        {...props}\n    />\n))\nCardHeader.displayName = \"CardHeader\"\n\nconst CardTitle = React.forwardRef<\n    HTMLDivElement,\n    React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n    <div\n        ref={ref}\n        className={cn(\n            \"text-2xl font-semibold leading-none tracking-tight bg-gradient-to-r from-foreground to-foreground/80 bg-clip-text\",\n            className\n        )}\n        {...props}\n    />\n))\nCardTitle.displayName = \"CardTitle\"\n\nconst CardDescription = React.forwardRef<\n    HTMLDivElement,\n    React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n    <div\n        ref={ref}\n        className={cn(\"text-sm text-muted-foreground/90 leading-relaxed\", className)}\n        {...props}\n    />\n))\nCardDescription.displayName = \"CardDescription\"\n\nconst CardContent = React.forwardRef<\n    HTMLDivElement,\n    React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n    <div ref={ref} className={cn(\"p-6 pt-2\", className)} {...props} />\n))\nCardContent.displayName = \"CardContent\"\n\nconst CardFooter = React.forwardRef<\n    HTMLDivElement,\n    React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n    <div\n        ref={ref}\n        className={cn(\n            \"flex items-center p-6 pt-0 border-t border-border/20 mt-4 bg-gradient-to-r from-background/50 to-background/20\",\n            className\n        )}\n        {...props}\n    />\n))\nCardFooter.displayName = \"CardFooter\"\n\nexport {\n    Card,\n    CardHeader,\n    CardFooter,\n    CardTitle,\n    CardDescription,\n    CardContent,\n    cardVariants\n}"
  },
  {
    "path": "components/ui/checkbox.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport { Check } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Checkbox = React.forwardRef<\n    React.ElementRef<typeof CheckboxPrimitive.Root>,\n    React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => (\n    <CheckboxPrimitive.Root\n        ref={ref}\n        className={cn(\n            \"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground\",\n            className\n        )}\n        {...props}\n    >\n        <CheckboxPrimitive.Indicator\n            className={cn(\"flex items-center justify-center text-current\")}\n        >\n            <Check className=\"h-4 w-4\" />\n        </CheckboxPrimitive.Indicator>\n    </CheckboxPrimitive.Root>\n))\nCheckbox.displayName = CheckboxPrimitive.Root.displayName\n\nexport { Checkbox }\n"
  },
  {
    "path": "components/ui/collapsible.tsx",
    "content": "\"use client\"\n\nimport * as CollapsiblePrimitive from \"@radix-ui/react-collapsible\"\n\nconst Collapsible = CollapsiblePrimitive.Root\n\nconst CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger\n\nconst CollapsibleContent = CollapsiblePrimitive.CollapsibleContent\n\nexport { Collapsible, CollapsibleTrigger, CollapsibleContent }\n"
  },
  {
    "path": "components/ui/command.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { type DialogProps } from \"@radix-ui/react-dialog\"\nimport { Command as CommandPrimitive } from \"cmdk\"\nimport { Search } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Dialog, DialogContent } from \"@/components/ui/dialog\"\n\nconst Command = React.forwardRef<\n    React.ElementRef<typeof CommandPrimitive>,\n    React.ComponentPropsWithoutRef<typeof CommandPrimitive>\n>(({ className, ...props }, ref) => (\n    <CommandPrimitive\n        ref={ref}\n        className={cn(\n            \"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground\",\n            className\n        )}\n        {...props}\n    />\n))\nCommand.displayName = CommandPrimitive.displayName\n\nconst CommandDialog = ({ children, ...props }: DialogProps) => {\n    return (\n        <Dialog {...props}>\n            <DialogContent className=\"overflow-hidden p-0 shadow-lg\">\n                <Command className=\"[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n                    {children}\n                </Command>\n            </DialogContent>\n        </Dialog>\n    )\n}\n\nconst CommandInput = React.forwardRef<\n    React.ElementRef<typeof CommandPrimitive.Input>,\n    React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>\n>(({ className, ...props }, ref) => (\n    <div className=\"flex items-center border-b px-3 group\" cmdk-input-wrapper=\"\">\n        <Search className=\"mr-2 h-4 w-4 shrink-0 opacity-50 group-focus-within:opacity-70 transition-opacity duration-200\" />\n        <CommandPrimitive.Input\n            ref={ref}\n            className={cn(\n                \"flex h-11 w-full rounded-md bg-transparent py-3 text-sm placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n                // Remove browser default outline and focus styling\n                \"outline-none focus:outline-none focus:ring-0 border-0 focus:border-0\",\n                className\n            )}\n            {...props}\n        />\n    </div>\n))\n\nCommandInput.displayName = CommandPrimitive.Input.displayName\n\nconst CommandList = React.forwardRef<\n    React.ElementRef<typeof CommandPrimitive.List>,\n    React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>\n>(({ className, ...props }, ref) => (\n    <CommandPrimitive.List\n        ref={ref}\n        className={cn(\"max-h-[300px] overflow-y-auto overflow-x-hidden\", className)}\n        {...props}\n    />\n))\n\nCommandList.displayName = CommandPrimitive.List.displayName\n\nconst CommandEmpty = React.forwardRef<\n    React.ElementRef<typeof CommandPrimitive.Empty>,\n    React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>\n>((props, ref) => (\n    <CommandPrimitive.Empty\n        ref={ref}\n        className=\"py-6 text-center text-sm text-muted-foreground\"\n        {...props}\n    />\n))\n\nCommandEmpty.displayName = CommandPrimitive.Empty.displayName\n\nconst CommandGroup = React.forwardRef<\n    React.ElementRef<typeof CommandPrimitive.Group>,\n    React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>\n>(({ className, ...props }, ref) => (\n    <CommandPrimitive.Group\n        ref={ref}\n        className={cn(\n            \"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground\",\n            className\n        )}\n        {...props}\n    />\n))\n\nCommandGroup.displayName = CommandPrimitive.Group.displayName\n\nconst CommandSeparator = React.forwardRef<\n    React.ElementRef<typeof CommandPrimitive.Separator>,\n    React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n    <CommandPrimitive.Separator\n        ref={ref}\n        className={cn(\"-mx-1 h-px bg-border\", className)}\n        {...props}\n    />\n))\nCommandSeparator.displayName = CommandPrimitive.Separator.displayName\n\nconst CommandItem = React.forwardRef<\n    React.ElementRef<typeof CommandPrimitive.Item>,\n    React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>\n>(({ className, ...props }, ref) => (\n    <CommandPrimitive.Item\n        ref={ref}\n        className={cn(\n            \"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n            // Enhanced hover and focus states\n            \"transition-colors duration-200 hover:bg-accent/50\",\n            // Remove any potential browser focus outlines\n            \"focus:outline-none focus:ring-0\",\n            className\n        )}\n        {...props}\n    />\n))\n\nCommandItem.displayName = CommandPrimitive.Item.displayName\n\nconst CommandShortcut = ({\n                             className,\n                             ...props\n                         }: React.HTMLAttributes<HTMLSpanElement>) => {\n    return (\n        <span\n            className={cn(\n                \"ml-auto text-xs tracking-widest text-muted-foreground opacity-60\",\n                className\n            )}\n            {...props}\n        />\n    )\n}\nCommandShortcut.displayName = \"CommandShortcut\"\n\nexport {\n    Command,\n    CommandDialog,\n    CommandInput,\n    CommandList,\n    CommandEmpty,\n    CommandGroup,\n    CommandItem,\n    CommandShortcut,\n    CommandSeparator,\n}"
  },
  {
    "path": "components/ui/confetti.tsx",
    "content": "'use client'\n\nimport React, { useEffect, useState } from 'react'\nimport { motion } from 'framer-motion'\n\nexport function Confetti() {\n    const [particles, setParticles] = useState<{ id: number; x: number; color: string; size: number; delay: number }[]>([])\n\n    useEffect(() => {\n        // Create confetti particles\n        const colors = ['#FF5252', '#FF9600', '#FFDE03', '#48FF00', '#00E5FF', '#8B6FFB', '#FF00C6']\n        const newParticles = Array.from({ length: 60 }, (_, i) => ({\n            id: i,\n            x: Math.random() * 100, // random horizontal position (percentage)\n            color: colors[Math.floor(Math.random() * colors.length)],\n            size: Math.random() * 7 + 3, // between 3-10px\n            delay: Math.random() * 0.5 // random delay for varied animation\n        }))\n\n        setParticles(newParticles)\n    }, [])\n\n    return (\n        <div className=\"fixed inset-0 pointer-events-none z-50 overflow-hidden\">\n            {particles.map((particle) => (\n                <motion.div\n                    key={particle.id}\n                    className=\"absolute top-0 rounded-full\"\n                    style={{\n                        left: `${particle.x}%`,\n                        width: `${particle.size}px`,\n                        height: `${particle.size}px`,\n                        backgroundColor: particle.color\n                    }}\n                    initial={{ y: -20, opacity: 1 }}\n                    animate={{\n                        y: ['0%', '100%'],\n                        x: [\n                            `${particle.x}%`,\n                            `${particle.x + (Math.random() * 20 - 10)}%`,\n                            `${particle.x + (Math.random() * 40 - 20)}%`\n                        ],\n                        opacity: [1, 1, 0],\n                        rotate: [0, Math.random() * 360 * (Math.random() > 0.5 ? 1 : -1)]\n                    }}\n                    transition={{\n                        duration: 3 + Math.random() * 2,\n                        ease: \"easeOut\",\n                        delay: particle.delay,\n                    }}\n                />\n            ))}\n        </div>\n    )\n}\n\nexport default Confetti"
  },
  {
    "path": "components/ui/dialog.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport {X} from \"lucide-react\"\n\nimport {cn} from \"@/lib/utils\"\n\nconst Dialog = DialogPrimitive.Root\n\nconst DialogTrigger = DialogPrimitive.Trigger\n\nconst DialogPortal = DialogPrimitive.Portal\n\nconst DialogClose = DialogPrimitive.Close\n\nconst DialogOverlay = React.forwardRef<\n    React.ElementRef<typeof DialogPrimitive.Overlay>,\n    React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({className, ...props}, ref) => (\n    <DialogPrimitive.Overlay\n        ref={ref}\n        className={cn(\n            \"fixed inset-0 z-50 backdrop-blur-md bg-black/70 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n            className\n        )}\n        {...props}\n    />\n))\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName\n\n// Define proper types for the extended props\ninterface DialogContentProps extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {\n    size?: \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\"\n    position?: \"center\" | \"top\"\n    disableClose?: boolean\n}\n\nconst DialogContent = React.forwardRef<\n    React.ElementRef<typeof DialogPrimitive.Content>,\n    DialogContentProps\n>(({className, children, position = \"center\", size = \"md\", disableClose = false, ...props}, ref) => {\n    // Create a new object without disableClose to avoid passing to DOM\n    const domProps = React.useMemo(() => {\n        const filteredProps = {...props}\n        // TypeScript knows disableClose shouldn't be in the DOM props\n        delete (filteredProps as Record<string, unknown>)['disableClose']\n        return filteredProps\n    }, [props])\n\n    const sizeStyles = {\n        sm: \"max-w-sm\",\n        md: \"max-w-md\",\n        lg: \"max-w-lg\",\n        xl: \"max-w-xl\",\n        full: \"max-w-full mx-4\"\n    } as const\n\n    const positionStyles = {\n        center: \"fixed top-[50%] left-[50%] -translate-x-[50%] -translate-y-[50%]\",\n        top: \"fixed top-[10%] left-[50%] -translate-x-[50%]\"\n    } as const\n\n    return (\n        <DialogPortal>\n            <DialogOverlay/>\n            <DialogPrimitive.Content\n                ref={ref}\n                className={cn(\n                    \"z-50 grid w-full gap-4 p-6\",\n                    \"bg-gradient-to-br from-background via-background to-muted/20\",\n                    \"border-2 border-border/60 shadow-2xl shadow-black/25\",\n                    \"backdrop-blur-sm rounded-xl\",\n                    // Optical border effect using pseudo-elements\n                    \"before:absolute before:inset-0 before:rounded-xl\",\n                    \"before:bg-gradient-to-br before:from-white/10 before:via-white/5 before:to-transparent\",\n                    \"before:pointer-events-none\",\n                    // Additional depth with ring\n                    \"ring-1 ring-white/10\",\n                    // Glass morphism effect\n                    \"relative overflow-hidden\",\n                    // Animations\n                    \"data-[state=open]:animate-in data-[state=closed]:animate-out\",\n                    \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n                    \"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\",\n                    \"data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]\",\n                    \"data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]\",\n                    // Size and position\n                    sizeStyles[size],\n                    positionStyles[position],\n                    className\n                )}\n                // Disable escape key and overlay click when disableClose is true\n                onEscapeKeyDown={disableClose ? (e) => e.preventDefault() : undefined}\n                onInteractOutside={disableClose ? (e) => e.preventDefault() : undefined}\n                {...domProps}\n            >\n                {children}\n                {/* Conditionally render close button */}\n                {!disableClose && (\n                    <DialogPrimitive.Close\n                        className=\"absolute right-4 top-4 rounded-full p-1.5 opacity-70 ring-offset-background transition-all duration-200 hover:opacity-100 hover:bg-accent/50 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none\">\n                        <X className=\"h-4 w-4\" strokeWidth={2.5}/>\n                        <span className=\"sr-only\">Close</span>\n                    </DialogPrimitive.Close>\n                )}\n            </DialogPrimitive.Content>\n        </DialogPortal>\n    )\n})\nDialogContent.displayName = DialogPrimitive.Content.displayName\n\nconst DialogHeader = ({\n                          className,\n                          ...props\n                      }: React.HTMLAttributes<HTMLDivElement>) => (\n    <div\n        className={cn(\n            \"flex flex-col space-y-1.5 text-center sm:text-left border-b border-border/50 pb-3 mb-2\",\n            className\n        )}\n        {...props}\n    />\n)\nDialogHeader.displayName = \"DialogHeader\"\n\nconst DialogFooter = ({\n                          className,\n                          ...props\n                      }: React.HTMLAttributes<HTMLDivElement>) => (\n    <div\n        className={cn(\n            \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 border-t border-border/50 pt-3 mt-2\",\n            className\n        )}\n        {...props}\n    />\n)\nDialogFooter.displayName = \"DialogFooter\"\n\nconst DialogTitle = React.forwardRef<\n    React.ElementRef<typeof DialogPrimitive.Title>,\n    React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({className, ...props}, ref) => (\n    <DialogPrimitive.Title\n        ref={ref}\n        className={cn(\n            \"text-lg font-semibold leading-none tracking-tight\",\n            className\n        )}\n        {...props}\n    />\n))\nDialogTitle.displayName = DialogPrimitive.Title.displayName\n\nconst DialogDescription = React.forwardRef<\n    React.ElementRef<typeof DialogPrimitive.Description>,\n    React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({className, ...props}, ref) => (\n    <DialogPrimitive.Description\n        ref={ref}\n        className={cn(\"text-sm text-muted-foreground\", className)}\n        {...props}\n    />\n))\nDialogDescription.displayName = DialogPrimitive.Description.displayName\n\nexport {\n    Dialog,\n    DialogPortal,\n    DialogOverlay,\n    DialogClose,\n    DialogTrigger,\n    DialogContent,\n    DialogHeader,\n    DialogFooter,\n    DialogTitle,\n    DialogDescription,\n}"
  },
  {
    "path": "components/ui/dropdown-menu.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimport { Check, ChevronRight, Circle } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst DropdownMenu = DropdownMenuPrimitive.Root\n\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger\n\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group\n\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal\n\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub\n\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n    inset?: boolean\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      \"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRight className=\"ml-auto\" />\n  </DropdownMenuPrimitive.SubTrigger>\n))\nDropdownMenuSubTrigger.displayName =\n  DropdownMenuPrimitive.SubTrigger.displayName\n\nconst DropdownMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuSubContent.displayName =\n  DropdownMenuPrimitive.SubContent.displayName\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <DropdownMenuPrimitive.Portal>\n    <DropdownMenuPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className\n      )}\n      {...props}\n    />\n  </DropdownMenuPrimitive.Portal>\n))\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName\n\nconst DropdownMenuItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n))\nDropdownMenuCheckboxItem.displayName =\n  DropdownMenuPrimitive.CheckboxItem.displayName\n\nconst DropdownMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Circle className=\"h-2 w-2 fill-current\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.RadioItem>\n))\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName\n\nconst DropdownMenuLabel = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      \"px-2 py-1.5 text-sm font-semibold\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName\n\nconst DropdownMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n))\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName\n\nconst DropdownMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\"ml-auto text-xs tracking-widest opacity-60\", className)}\n      {...props}\n    />\n  )\n}\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\"\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuGroup,\n  DropdownMenuPortal,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuRadioGroup,\n}\n"
  },
  {
    "path": "components/ui/error-alert.tsx",
    "content": "import { AlertCircle } from 'lucide-react'\nimport { motion } from 'framer-motion'\n\ninterface ErrorAlertProps {\n    message: string\n}\n\nexport function ErrorAlert({ message }: ErrorAlertProps) {\n    return (\n        <motion.div\n            initial={{ opacity: 0, y: -10 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0, y: -10 }}\n            className=\"bg-rose-50 border-l-4 border-rose-500 p-4\"\n        >\n            <div className=\"flex\">\n                <AlertCircle className=\"h-5 w-5 text-rose-400\" />\n                <p className=\"ml-3 text-sm text-rose-700\">{message}</p>\n            </div>\n        </motion.div>\n    )\n}"
  },
  {
    "path": "components/ui/form.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport {\n  Controller,\n  ControllerProps,\n  FieldPath,\n  FieldValues,\n  FormProvider,\n  useFormContext,\n} from \"react-hook-form\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Label } from \"@/components/ui/label\"\n\nconst Form = FormProvider\n\ntype FormFieldContextValue<\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>\n> = {\n  name: TName\n}\n\nconst FormFieldContext = React.createContext<FormFieldContextValue>(\n  {} as FormFieldContextValue\n)\n\nconst FormField = <\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>\n>({\n  ...props\n}: ControllerProps<TFieldValues, TName>) => {\n  return (\n    <FormFieldContext.Provider value={{ name: props.name }}>\n      <Controller {...props} />\n    </FormFieldContext.Provider>\n  )\n}\n\nconst useFormField = () => {\n  const fieldContext = React.useContext(FormFieldContext)\n  const itemContext = React.useContext(FormItemContext)\n  const { getFieldState, formState } = useFormContext()\n\n  const fieldState = getFieldState(fieldContext.name, formState)\n\n  if (!fieldContext) {\n    throw new Error(\"useFormField should be used within <FormField>\")\n  }\n\n  const { id } = itemContext\n\n  return {\n    id,\n    name: fieldContext.name,\n    formItemId: `${id}-form-item`,\n    formDescriptionId: `${id}-form-item-description`,\n    formMessageId: `${id}-form-item-message`,\n    ...fieldState,\n  }\n}\n\ntype FormItemContextValue = {\n  id: string\n}\n\nconst FormItemContext = React.createContext<FormItemContextValue>(\n  {} as FormItemContextValue\n)\n\nconst FormItem = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n  const id = React.useId()\n\n  return (\n    <FormItemContext.Provider value={{ id }}>\n      <div ref={ref} className={cn(\"space-y-2\", className)} {...props} />\n    </FormItemContext.Provider>\n  )\n})\nFormItem.displayName = \"FormItem\"\n\nconst FormLabel = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  const { error, formItemId } = useFormField()\n\n  return (\n    <Label\n      ref={ref}\n      className={cn(error && \"text-destructive\", className)}\n      htmlFor={formItemId}\n      {...props}\n    />\n  )\n})\nFormLabel.displayName = \"FormLabel\"\n\nconst FormControl = React.forwardRef<\n  React.ElementRef<typeof Slot>,\n  React.ComponentPropsWithoutRef<typeof Slot>\n>(({ ...props }, ref) => {\n  const { error, formItemId, formDescriptionId, formMessageId } = useFormField()\n\n  return (\n    <Slot\n      ref={ref}\n      id={formItemId}\n      aria-describedby={\n        !error\n          ? `${formDescriptionId}`\n          : `${formDescriptionId} ${formMessageId}`\n      }\n      aria-invalid={!!error}\n      {...props}\n    />\n  )\n})\nFormControl.displayName = \"FormControl\"\n\nconst FormDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => {\n  const { formDescriptionId } = useFormField()\n\n  return (\n    <p\n      ref={ref}\n      id={formDescriptionId}\n      className={cn(\"text-sm text-muted-foreground\", className)}\n      {...props}\n    />\n  )\n})\nFormDescription.displayName = \"FormDescription\"\n\nconst FormMessage = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, children, ...props }, ref) => {\n  const { error, formMessageId } = useFormField()\n  const body = error ? String(error?.message) : children\n\n  if (!body) {\n    return null\n  }\n\n  return (\n    <p\n      ref={ref}\n      id={formMessageId}\n      className={cn(\"text-sm font-medium text-destructive\", className)}\n      {...props}\n    >\n      {body}\n    </p>\n  )\n})\nFormMessage.displayName = \"FormMessage\"\n\nexport {\n  useFormField,\n  Form,\n  FormItem,\n  FormLabel,\n  FormControl,\n  FormDescription,\n  FormMessage,\n  FormField,\n}\n"
  },
  {
    "path": "components/ui/hover-card.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as HoverCardPrimitive from \"@radix-ui/react-hover-card\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst HoverCard = HoverCardPrimitive.Root\n\nconst HoverCardTrigger = HoverCardPrimitive.Trigger\n\nconst HoverCardContent = React.forwardRef<\n    React.ElementRef<typeof HoverCardPrimitive.Content>,\n    React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 4, ...props }, ref) => (\n    <HoverCardPrimitive.Content\n        ref={ref}\n        align={align}\n        sideOffset={sideOffset}\n        className={cn(\n            \"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]\",\n            className\n        )}\n        {...props}\n    />\n))\nHoverCardContent.displayName = HoverCardPrimitive.Content.displayName\n\nexport { HoverCard, HoverCardTrigger, HoverCardContent }\n"
  },
  {
    "path": "components/ui/input.tsx",
    "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"@/lib/utils\"\n\nconst inputVariants = cva(\n    \"relative isolate flex w-full rounded-lg border bg-background px-3 py-2 text-base transition-all duration-200 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground/70 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n    {\n        variants: {\n            variant: {\n                default: [\n                    // Base styling with optical border\n                    \"border border-input/60 bg-background shadow-sm\",\n                    // Background layer for depth\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.lg)-1px)] before:bg-gradient-to-b before:from-background/80 before:to-background/40\",\n                    // Focus ring and highlight\n                    \"focus-visible:border-ring/50 focus-visible:ring-2 focus-visible:ring-ring/20 focus-visible:ring-offset-0\",\n                    \"focus-visible:shadow-md focus-visible:before:from-background/90 focus-visible:before:to-background/60\",\n                    // Subtle inner highlight\n                    \"after:absolute after:inset-0 after:-z-10 after:rounded-[calc(theme(borderRadius.lg)-1px)]\",\n                    \"after:shadow-[inset_0_1px_theme(colors.white/8%)]\",\n                    // Dark mode adjustments\n                    \"dark:border-input/40 dark:before:from-background/20 dark:before:to-background/5\",\n                    \"dark:focus-visible:border-ring/40\",\n                ],\n                flat: [\n                    \"border border-transparent bg-muted/80 shadow-none backdrop-blur-sm\",\n                    \"focus-visible:bg-background focus-visible:border-ring/50 focus-visible:ring-2 focus-visible:ring-ring/20\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.lg)-1px)]\",\n                    \"before:bg-gradient-to-b before:from-muted/60 before:to-muted/40\",\n                    \"focus-visible:before:from-background/80 focus-visible:before:to-background/40\",\n                ],\n                outline: [\n                    \"bg-transparent border border-input/80 shadow-none\",\n                    \"focus-visible:bg-background/50 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/20\",\n                    \"hover:border-input hover:bg-background/30\",\n                ],\n                underlined: [\n                    \"rounded-none border-0 border-b-2 border-input/60 shadow-none bg-transparent px-0\",\n                    \"focus-visible:border-ring focus-visible:ring-0\",\n                    \"after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-0 after:bg-ring after:transition-all\",\n                    \"focus-visible:after:w-full\",\n                ],\n                ghost: [\n                    \"border border-transparent bg-transparent shadow-none\",\n                    \"hover:bg-accent/30 hover:border-accent/40\",\n                    \"focus-visible:bg-background/80 focus-visible:border-ring/50 focus-visible:ring-2 focus-visible:ring-ring/20\",\n                ],\n            },\n            size: {\n                default: \"h-10\",\n                sm: \"h-8 rounded-md px-2 text-xs\",\n                lg: \"h-12 rounded-xl px-4 text-lg\",\n            },\n            state: {\n                default: \"\",\n                error: [\n                    \"border-destructive/60 text-destructive\",\n                    \"focus-visible:border-destructive focus-visible:ring-destructive/20\",\n                    \"before:from-destructive/5 before:to-destructive/[0.02]\",\n                ],\n                success: [\n                    \"border-green-500/60 text-green-700 dark:text-green-400\",\n                    \"focus-visible:border-green-500 focus-visible:ring-green-500/20\",\n                    \"before:from-green-500/5 before:to-green-500/[0.02]\",\n                ],\n                warning: [\n                    \"border-yellow-500/60 text-yellow-700 dark:text-yellow-400\",\n                    \"focus-visible:border-yellow-500 focus-visible:ring-yellow-500/20\",\n                    \"before:from-yellow-500/5 before:to-yellow-500/[0.02]\",\n                ],\n            },\n        },\n        defaultVariants: {\n            variant: \"default\",\n            size: \"default\",\n            state: \"default\",\n        },\n    }\n)\n\nexport interface InputProps\n    extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"size\">,\n        VariantProps<typeof inputVariants> {\n    startIcon?: React.ReactNode\n    endIcon?: React.ReactNode\n}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n    ({ className, variant, size, state, type, startIcon, endIcon, ...props }, ref) => {\n        const hasIcons = startIcon || endIcon\n\n        return (\n            <div className={cn(\"relative\", hasIcons && \"flex items-center\")}>\n                {startIcon && (\n                    <div className=\"absolute left-3 z-10 flex items-center pointer-events-none\">\n                        <div className=\"text-muted-foreground/60 [&>svg]:h-4 [&>svg]:w-4\">\n                            {startIcon}\n                        </div>\n                    </div>\n                )}\n\n                <input\n                    type={type}\n                    className={cn(\n                        inputVariants({ variant, size, state, className }),\n                        startIcon && \"pl-10\",\n                        endIcon && \"pr-10\"\n                    )}\n                    ref={ref}\n                    {...props}\n                />\n\n                {endIcon && (\n                    <div className=\"absolute right-3 z-10 flex items-center pointer-events-none\">\n                        <div className=\"text-muted-foreground/60 [&>svg]:h-4 [&>svg]:w-4\">\n                            {endIcon}\n                        </div>\n                    </div>\n                )}\n            </div>\n        )\n    }\n)\nInput.displayName = \"Input\"\n\nexport { Input, inputVariants }"
  },
  {
    "path": "components/ui/label.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst labelVariants = cva(\n  \"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\"\n)\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &\n    VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root\n    ref={ref}\n    className={cn(labelVariants(), className)}\n    {...props}\n  />\n))\nLabel.displayName = LabelPrimitive.Root.displayName\n\nexport { Label }\n"
  },
  {
    "path": "components/ui/popover.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Popover = PopoverPrimitive.Root\n\nconst PopoverTrigger = PopoverPrimitive.Trigger\n\nconst PopoverContent = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 4, ...props }, ref) => (\n  <PopoverPrimitive.Portal>\n    <PopoverPrimitive.Content\n      ref={ref}\n      align={align}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className\n      )}\n      {...props}\n    />\n  </PopoverPrimitive.Portal>\n))\nPopoverContent.displayName = PopoverPrimitive.Content.displayName\n\nexport { Popover, PopoverTrigger, PopoverContent }\n"
  },
  {
    "path": "components/ui/progress.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ProgressPrimitive from \"@radix-ui/react-progress\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Progress = React.forwardRef<\n  React.ElementRef<typeof ProgressPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>\n>(({ className, value, ...props }, ref) => (\n  <ProgressPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"relative h-4 w-full overflow-hidden rounded-full bg-secondary\",\n      className\n    )}\n    {...props}\n  >\n    <ProgressPrimitive.Indicator\n      className=\"h-full w-full flex-1 bg-primary transition-all\"\n      style={{ transform: `translateX(-${100 - (value || 0)}%)` }}\n    />\n  </ProgressPrimitive.Root>\n))\nProgress.displayName = ProgressPrimitive.Root.displayName\n\nexport { Progress }\n"
  },
  {
    "path": "components/ui/radio-group.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\"\nimport { Circle } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst RadioGroup = React.forwardRef<\n  React.ElementRef<typeof RadioGroupPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  return (\n    <RadioGroupPrimitive.Root\n      className={cn(\"grid gap-2\", className)}\n      {...props}\n      ref={ref}\n    />\n  )\n})\nRadioGroup.displayName = RadioGroupPrimitive.Root.displayName\n\nconst RadioGroupItem = React.forwardRef<\n  React.ElementRef<typeof RadioGroupPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>\n>(({ className, ...props }, ref) => {\n  return (\n    <RadioGroupPrimitive.Item\n      ref={ref}\n      className={cn(\n        \"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    >\n      <RadioGroupPrimitive.Indicator className=\"flex items-center justify-center\">\n        <Circle className=\"h-2.5 w-2.5 fill-current text-current\" />\n      </RadioGroupPrimitive.Indicator>\n    </RadioGroupPrimitive.Item>\n  )\n})\nRadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName\n\nexport { RadioGroup, RadioGroupItem }\n"
  },
  {
    "path": "components/ui/scroll-area.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst ScrollArea = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>\n>(({ className, children, ...props }, ref) => (\n  <ScrollAreaPrimitive.Root\n    ref={ref}\n    className={cn(\"relative overflow-hidden\", className)}\n    {...props}\n  >\n    <ScrollAreaPrimitive.Viewport className=\"h-full w-full rounded-[inherit]\">\n      {children}\n    </ScrollAreaPrimitive.Viewport>\n    <ScrollBar />\n    <ScrollAreaPrimitive.Corner />\n  </ScrollAreaPrimitive.Root>\n))\nScrollArea.displayName = ScrollAreaPrimitive.Root.displayName\n\nconst ScrollBar = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>\n>(({ className, orientation = \"vertical\", ...props }, ref) => (\n  <ScrollAreaPrimitive.ScrollAreaScrollbar\n    ref={ref}\n    orientation={orientation}\n    className={cn(\n      \"flex touch-none select-none transition-colors\",\n      orientation === \"vertical\" &&\n        \"h-full w-2.5 border-l border-l-transparent p-[1px]\",\n      orientation === \"horizontal\" &&\n        \"h-2.5 flex-col border-t border-t-transparent p-[1px]\",\n      className\n    )}\n    {...props}\n  >\n    <ScrollAreaPrimitive.ScrollAreaThumb className=\"relative flex-1 rounded-full bg-border\" />\n  </ScrollAreaPrimitive.ScrollAreaScrollbar>\n))\nScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName\n\nexport { ScrollArea, ScrollBar }\n"
  },
  {
    "path": "components/ui/searchable-select.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ChevronsUpDown } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\"\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command\"\n\ninterface SearchableSelectItem {\n  value: string\n  label: string\n  searchValue?: string\n}\n\ninterface SearchableSelectGroup {\n  heading: string\n  items: SearchableSelectItem[]\n}\n\ninterface SearchableSelectProps {\n  value?: string\n  onValueChange?: (value: string) => void\n  placeholder?: string\n  searchPlaceholder?: string\n  /** Grouped items with headings */\n  groups?: SearchableSelectGroup[]\n  /** Standalone items rendered before groups (e.g. \"System Default\") */\n  items?: SearchableSelectItem[]\n  className?: string\n  disabled?: boolean\n  /** Height class override, defaults to h-10 */\n  triggerClassName?: string\n}\n\nfunction SearchableSelect({\n  value,\n  onValueChange,\n  placeholder = \"Select...\",\n  searchPlaceholder = \"Search...\",\n  groups = [],\n  items = [],\n  className,\n  disabled = false,\n  triggerClassName,\n}: SearchableSelectProps) {\n  const [open, setOpen] = React.useState(false)\n\n  const selectedLabel = React.useMemo(() => {\n    for (const item of items) {\n      if (item.value === value) return item.label\n    }\n    for (const group of groups) {\n      const found = group.items.find((i) => i.value === value)\n      if (found) return found.label\n    }\n    return null\n  }, [items, groups, value])\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"outline\"\n          role=\"combobox\"\n          aria-expanded={open}\n          disabled={disabled}\n          className={cn(\n            \"h-10 w-full justify-between font-normal px-3\",\n            !selectedLabel && \"text-muted-foreground\",\n            triggerClassName,\n            className\n          )}\n        >\n          <span className=\"truncate\">\n            {selectedLabel ?? placeholder}\n          </span>\n          <ChevronsUpDown className=\"ml-2 h-4 w-4 shrink-0 opacity-50\" />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-[--radix-popover-trigger-width] p-0\" align=\"start\">\n        <Command\n          filter={(value, search, keywords) => {\n            const text = keywords?.join(\" \") ?? value\n            return text.toLowerCase().includes(search.toLowerCase()) ? 1 : 0\n          }}\n        >\n          <CommandInput placeholder={searchPlaceholder} />\n          <CommandList>\n            <CommandEmpty>No results found.</CommandEmpty>\n\n            {items.length > 0 && (\n              <CommandGroup>\n                {items.map((item) => (\n                  <CommandItem\n                    key={item.value}\n                    value={item.value}\n                    keywords={[item.searchValue ?? item.label]}\n                    onSelect={(selected) => {\n                      onValueChange?.(selected)\n                      setOpen(false)\n                    }}\n                  >\n                    <Check\n                      className={cn(\n                        \"mr-2 h-4 w-4 shrink-0\",\n                        value === item.value ? \"opacity-100\" : \"opacity-0\"\n                      )}\n                    />\n                    {item.label}\n                  </CommandItem>\n                ))}\n              </CommandGroup>\n            )}\n\n            {groups.map((group) => (\n              <CommandGroup key={group.heading} heading={group.heading}>\n                {group.items.map((item) => (\n                  <CommandItem\n                    key={item.value}\n                    value={item.value}\n                    keywords={[item.searchValue ?? item.label]}\n                    onSelect={(selected) => {\n                      onValueChange?.(selected)\n                      setOpen(false)\n                    }}\n                  >\n                    <Check\n                      className={cn(\n                        \"mr-2 h-4 w-4 shrink-0\",\n                        value === item.value ? \"opacity-100\" : \"opacity-0\"\n                      )}\n                    />\n                    {item.label}\n                  </CommandItem>\n                ))}\n              </CommandGroup>\n            ))}\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  )\n}\n\nexport { SearchableSelect }\nexport type { SearchableSelectGroup, SearchableSelectItem, SearchableSelectProps }\n"
  },
  {
    "path": "components/ui/select.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\nimport {Check, ChevronDown, ChevronUp} from \"lucide-react\"\n\nimport {cn} from \"@/lib/utils\"\n\nconst Select = SelectPrimitive.Root\n\nconst SelectValue = SelectPrimitive.Value\n\nconst SelectTrigger = React.forwardRef<\n    React.ComponentRef<typeof SelectPrimitive.Trigger>,\n    React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({className, children, ...props}, ref) => (\n    <SelectPrimitive.Trigger\n        ref={ref}\n        className={cn(\n            \"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1\",\n            className\n        )}\n        {...props}\n    >\n        {children}\n        <SelectPrimitive.Icon asChild>\n            <ChevronDown className=\"h-4 w-4 opacity-50\"/>\n        </SelectPrimitive.Icon>\n    </SelectPrimitive.Trigger>\n))\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName\n\nconst SelectScrollUpButton = React.forwardRef<\n    React.ComponentRef<typeof SelectPrimitive.ScrollUpButton>,\n    React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({className, ...props}, ref) => (\n    <SelectPrimitive.ScrollUpButton\n        ref={ref}\n        className={cn(\n            \"flex cursor-default items-center justify-center py-1\",\n            className\n        )}\n        {...props}\n    >\n        <ChevronUp className=\"h-4 w-4\"/>\n    </SelectPrimitive.ScrollUpButton>\n))\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName\n\nconst SelectScrollDownButton = React.forwardRef<\n    React.ComponentRef<typeof SelectPrimitive.ScrollDownButton>,\n    React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({className, ...props}, ref) => (\n    <SelectPrimitive.ScrollDownButton\n        ref={ref}\n        className={cn(\n            \"flex cursor-default items-center justify-center py-1\",\n            className\n        )}\n        {...props}\n    >\n        <ChevronDown className=\"h-4 w-4\"/>\n    </SelectPrimitive.ScrollDownButton>\n))\nSelectScrollDownButton.displayName =\n    SelectPrimitive.ScrollDownButton.displayName\n\nconst SelectContent = React.forwardRef<\n    React.ComponentRef<typeof SelectPrimitive.Content>,\n    React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({className, children, position = \"popper\", ...props}, ref) => (\n    <SelectPrimitive.Portal>\n        <SelectPrimitive.Content\n            ref={ref}\n            className={cn(\n                \"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n                position === \"popper\" &&\n                \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n                className\n            )}\n            position={position}\n            {...props}\n        >\n            <SelectScrollUpButton/>\n            <SelectPrimitive.Viewport\n                className={cn(\n                    \"p-1\",\n                    position === \"popper\" &&\n                    \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\"\n                )}\n            >\n                {children}\n            </SelectPrimitive.Viewport>\n            <SelectScrollDownButton/>\n        </SelectPrimitive.Content>\n    </SelectPrimitive.Portal>\n))\nSelectContent.displayName = SelectPrimitive.Content.displayName\n\nconst SelectLabel = React.forwardRef<\n    React.ComponentRef<typeof SelectPrimitive.Label>,\n    React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({className, ...props}, ref) => (\n    <SelectPrimitive.Label\n        ref={ref}\n        className={cn(\"py-1.5 pl-8 pr-2 text-sm font-semibold\", className)}\n        {...props}\n    />\n))\nSelectLabel.displayName = SelectPrimitive.Label.displayName\n\nconst SelectItem = React.forwardRef<\n    React.ComponentRef<typeof SelectPrimitive.Item>,\n    React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({className, children, ...props}, ref) => (\n    <SelectPrimitive.Item\n        ref={ref}\n        className={cn(\n            \"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n            className\n        )}\n        {...props}\n    >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <SelectPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\"/>\n      </SelectPrimitive.ItemIndicator>\n    </span>\n        <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n    </SelectPrimitive.Item>\n))\nSelectItem.displayName = SelectPrimitive.Item.displayName\n\nconst SelectSeparator = React.forwardRef<\n    React.ComponentRef<typeof SelectPrimitive.Separator>,\n    React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({className, ...props}, ref) => (\n    <SelectPrimitive.Separator\n        ref={ref}\n        className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n        {...props}\n    />\n))\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName\n\nexport {\n    Select,\n    SelectValue,\n    SelectTrigger,\n    SelectContent,\n    SelectLabel,\n    SelectItem,\n    SelectSeparator,\n    SelectScrollUpButton,\n    SelectScrollDownButton,\n}\n"
  },
  {
    "path": "components/ui/separator.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Separator = React.forwardRef<\n  React.ElementRef<typeof SeparatorPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(\n  (\n    { className, orientation = \"horizontal\", decorative = true, ...props },\n    ref\n  ) => (\n    <SeparatorPrimitive.Root\n      ref={ref}\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"shrink-0 bg-border\",\n        orientation === \"horizontal\" ? \"h-[1px] w-full\" : \"h-full w-[1px]\",\n        className\n      )}\n      {...props}\n    />\n  )\n)\nSeparator.displayName = SeparatorPrimitive.Root.displayName\n\nexport { Separator }\n"
  },
  {
    "path": "components/ui/sheet.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Sheet = SheetPrimitive.Root\n\nconst SheetTrigger = SheetPrimitive.Trigger\n\nconst SheetClose = SheetPrimitive.Close\n\nconst SheetPortal = SheetPrimitive.Portal\n\nconst SheetOverlay = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Overlay\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className\n    )}\n    {...props}\n    ref={ref}\n  />\n))\nSheetOverlay.displayName = SheetPrimitive.Overlay.displayName\n\nconst sheetVariants = cva(\n  \"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500\",\n  {\n    variants: {\n      side: {\n        top: \"inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top\",\n        bottom:\n          \"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom\",\n        left: \"inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm\",\n        right:\n          \"inset-y-0 right-0 h-full w-3/4  border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm\",\n      },\n    },\n    defaultVariants: {\n      side: \"right\",\n    },\n  }\n)\n\ninterface SheetContentProps\n  extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,\n    VariantProps<typeof sheetVariants> {}\n\nconst SheetContent = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Content>,\n  SheetContentProps\n>(({ side = \"right\", className, children, ...props }, ref) => (\n  <SheetPortal>\n    <SheetOverlay />\n    <SheetPrimitive.Content\n      ref={ref}\n      className={cn(sheetVariants({ side }), className)}\n      {...props}\n    >\n      {children}\n      <SheetPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary\">\n        <X className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </SheetPrimitive.Close>\n    </SheetPrimitive.Content>\n  </SheetPortal>\n))\nSheetContent.displayName = SheetPrimitive.Content.displayName\n\nconst SheetHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-2 text-center sm:text-left\",\n      className\n    )}\n    {...props}\n  />\n)\nSheetHeader.displayName = \"SheetHeader\"\n\nconst SheetFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n      className\n    )}\n    {...props}\n  />\n)\nSheetFooter.displayName = \"SheetFooter\"\n\nconst SheetTitle = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Title\n    ref={ref}\n    className={cn(\"text-lg font-semibold text-foreground\", className)}\n    {...props}\n  />\n))\nSheetTitle.displayName = SheetPrimitive.Title.displayName\n\nconst SheetDescription = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n))\nSheetDescription.displayName = SheetPrimitive.Description.displayName\n\nexport {\n  Sheet,\n  SheetPortal,\n  SheetOverlay,\n  SheetTrigger,\n  SheetClose,\n  SheetContent,\n  SheetHeader,\n  SheetFooter,\n  SheetTitle,\n  SheetDescription,\n}\n"
  },
  {
    "path": "components/ui/sidebar.tsx",
    "content": "'use client'\n\nimport React, { useState } from 'react'\nimport Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport { LucideIcon, ChevronLeft, ChevronRight, Menu, LogOut, Settings } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport {\n    Avatar,\n    AvatarFallback,\n    AvatarImage,\n} from '@/components/ui/avatar'\nimport { Button } from '@/components/ui/button'\nimport {\n    Dialog,\n    DialogClose,\n    DialogContent,\n    DialogTrigger,\n} from '@/components/ui/dialog'\nimport {\n    Sheet,\n    SheetContent,\n    SheetTrigger,\n} from '@/components/ui/sheet'\nimport {\n    Tooltip,\n    TooltipContent,\n    TooltipProvider,\n    TooltipTrigger,\n} from '@/components/ui/tooltip'\n\n// Type definitions\ninterface SidebarUser {\n    id: string\n    name: string | null\n    email: string | null\n    role: string\n    avatar?: string\n}\n\ninterface NavItem {\n    href: string\n    label: string\n    icon: LucideIcon\n    requiredRole?: string[]\n}\n\ninterface NavSection {\n    title: string\n    items: NavItem[]\n}\n\ninterface SidebarProps {\n    user: SidebarUser\n    sections: NavSection[]\n    onLogout: () => Promise<void>\n    brandName?: string\n    brandHref?: string\n    isExpanded: boolean\n    setIsExpanded: (expanded: boolean) => void\n    isVisible: boolean\n    // setIsVisible: (visible: boolean) => void\n}\n\n// Desktop Sidebar Component\nfunction DesktopSidebar({\n                            user,\n                            sections,\n                            onLogout,\n                            isExpanded,\n                            setIsExpanded,\n                            isVisible,\n                            // setIsVisible,\n                            brandName = \"Changerawr\",\n                            brandHref = \"/dashboard\"\n                        }: {\n    user: SidebarUser\n    sections: NavSection[]\n    onLogout: () => Promise<void>\n    isExpanded: boolean\n    setIsExpanded: (expanded: boolean) => void\n    isVisible: boolean\n    // setIsVisible: (visible: boolean) => void\n    brandName?: string\n    brandHref?: string\n}) {\n    const pathname = usePathname()\n\n    // Filter navigation items based on user role\n    const filteredNavSections = sections.map(section => ({\n        ...section,\n        items: section.items.filter(item =>\n            !item.requiredRole || item.requiredRole.includes(user.role)\n        )\n    })).filter(section => section.items.length > 0)\n\n    // Function to check if a route is active based on namespace\n    const isRouteActive = (itemHref: string): boolean => {\n        // Exact match for dashboard home\n        if (itemHref === '/dashboard' && pathname === '/dashboard') {\n            return true\n        }\n\n        // For other routes, check if current path starts with the item href\n        // but make sure we don't match dashboard root for other routes\n        if (itemHref !== '/dashboard' && pathname.startsWith(itemHref)) {\n            return true\n        }\n\n        return false\n    }\n\n    const getUserInitial = () => {\n        return user.name?.[0] || user.email?.[0] || '?'\n    }\n\n    return (\n        <aside\n            className={cn(\n                \"hidden md:flex fixed inset-y-0 left-0 z-40 bg-background border-r transition-all duration-300 ease-in-out flex-col\",\n                isVisible ? (isExpanded ? \"w-64\" : \"w-16\") : \"w-0 overflow-hidden\"\n            )}\n            data-expanded={isExpanded}\n            data-visible={isVisible}\n        >\n            {isVisible && (\n                <>\n                    {/* Header with brand and controls */}\n                    <div className=\"h-16 flex items-center justify-between border-b px-4 flex-shrink-0\">\n                        {isExpanded && (\n                            <Link\n                                href={brandHref}\n                                className=\"text-xl font-bold truncate\"\n                            >\n                                {brandName}\n                            </Link>\n                        )}\n                        <div className={cn(\"flex items-center gap-1\", isExpanded ? \"\" : \"mx-auto\")}>\n                            <Button\n                                variant=\"ghost\"\n                                size=\"icon\"\n                                className=\"flex-shrink-0\"\n                                onClick={() => setIsExpanded(!isExpanded)}\n                                aria-label={isExpanded ? \"Collapse sidebar\" : \"Expand sidebar\"}\n                            >\n                                {isExpanded ? (\n                                    <ChevronLeft className=\"h-5 w-5\"/>\n                                ) : (\n                                    <ChevronRight className=\"h-5 w-5\"/>\n                                )}\n                            </Button>\n                            {/*{isExpanded && (*/}\n                            {/*    <Button*/}\n                            {/*        variant=\"ghost\"*/}\n                            {/*        size=\"icon\"*/}\n                            {/*        className=\"flex-shrink-0\"*/}\n                            {/*        onClick={() => setIsVisible(false)}*/}\n                            {/*        aria-label=\"Hide sidebar\"*/}\n                            {/*    >*/}\n                            {/*        <PanelRightOpen className=\"h-4 w-4\"/>*/}\n                            {/*    </Button>*/}\n                            {/*)}*/}\n                        </div>\n                    </div>\n\n                    {/* Navigation */}\n                    <nav className=\"flex-1 py-4 overflow-y-auto\">\n                        {filteredNavSections.map((section, sectionIndex) => (\n                            <div key={sectionIndex} className=\"mb-4\">\n                                {isExpanded && (\n                                    <p className=\"text-xs font-medium text-muted-foreground px-4 mb-2\">\n                                        {section.title}\n                                    </p>\n                                )}\n                                {section.items.map((item) => (\n                                    <TooltipProvider key={item.href}>\n                                        <Tooltip delayDuration={300}>\n                                            <TooltipTrigger asChild>\n                                                <Link\n                                                    href={item.href}\n                                                    className={cn(\n                                                        \"flex items-center p-2 transition-colors rounded-md mx-2\",\n                                                        isExpanded ? \"px-3 gap-3\" : \"justify-center\",\n                                                        isRouteActive(item.href)\n                                                            ? \"bg-secondary text-secondary-foreground\"\n                                                            : \"hover:bg-secondary/50\",\n                                                        \"group\"\n                                                    )}\n                                                >\n                                                    <item.icon className={cn(\n                                                        \"h-5 w-5 flex-shrink-0\",\n                                                        isRouteActive(item.href)\n                                                            ? \"text-secondary-foreground\"\n                                                            : \"text-muted-foreground group-hover:text-foreground\"\n                                                    )} />\n                                                    {isExpanded && (\n                                                        <span className=\"truncate\">{item.label}</span>\n                                                    )}\n                                                </Link>\n                                            </TooltipTrigger>\n                                            {!isExpanded && (\n                                                <TooltipContent side=\"right\">\n                                                    {item.label}\n                                                </TooltipContent>\n                                            )}\n                                        </Tooltip>\n                                    </TooltipProvider>\n                                ))}\n                            </div>\n                        ))}\n                    </nav>\n\n                    {/* User Profile Section */}\n                    <div className=\"border-t p-4 flex-shrink-0\">\n                        {isExpanded ? (\n                            <Dialog>\n                                <DialogTrigger asChild>\n                                    <Button\n                                        variant=\"ghost\"\n                                        className=\"w-full flex items-center gap-3 p-2 hover:bg-secondary/50\"\n                                    >\n                                        <Avatar className=\"h-8 w-8 flex-shrink-0\">\n                                            <AvatarImage\n                                                src={user.avatar}\n                                                alt={user.name || 'User avatar'}\n                                            />\n                                            <AvatarFallback>{getUserInitial()}</AvatarFallback>\n                                        </Avatar>\n                                        <div className=\"text-left flex-1 min-w-0\">\n                                            <p className=\"text-sm font-medium truncate\">\n                                                {user.name || 'Unnamed User'}\n                                            </p>\n                                            <p className=\"text-xs text-muted-foreground truncate\">\n                                                {user.role}\n                                            </p>\n                                        </div>\n                                    </Button>\n                                </DialogTrigger>\n                                <DialogContent className=\"sm:max-w-md\">\n                                    <div className=\"flex flex-col items-center space-y-4\">\n                                        <Avatar className=\"h-16 w-16\">\n                                            <AvatarImage\n                                                src={user.avatar}\n                                                alt={user.name || 'User avatar'}\n                                            />\n                                            <AvatarFallback>{getUserInitial()}</AvatarFallback>\n                                        </Avatar>\n                                        <div className=\"text-center\">\n                                            <p className=\"font-semibold\">{user.name || 'Unnamed User'}</p>\n                                            <p className=\"text-sm text-muted-foreground\">{user.email || 'No email'}</p>\n                                            <p className=\"text-xs uppercase text-muted-foreground mt-1\">\n                                                {user.role || 'Unknown Role'}\n                                            </p>\n                                        </div>\n                                        <div className=\"flex w-full space-x-2\">\n                                            <Button\n                                                variant=\"outline\"\n                                                className=\"w-full\"\n                                                asChild\n                                            >\n                                                <DialogClose asChild>\n                                                    <Link href=\"/dashboard/settings\">\n                                                        <Settings className=\"h-4 w-4 mr-2\"/>\n                                                        Settings\n                                                    </Link>\n                                                </DialogClose>\n                                            </Button>\n                                            <Button\n                                                variant=\"destructive\"\n                                                className=\"w-full\"\n                                                onClick={onLogout}\n                                            >\n                                                <LogOut className=\"h-4 w-4 mr-2\"/>\n                                                Logout\n                                            </Button>\n                                        </div>\n                                    </div>\n                                </DialogContent>\n                            </Dialog>\n                        ) : (\n                            <TooltipProvider>\n                                <Tooltip delayDuration={300}>\n                                    <TooltipTrigger asChild>\n                                        <Button\n                                            variant=\"ghost\"\n                                            size=\"icon\"\n                                            className=\"w-full h-10\"\n                                        >\n                                            <Avatar className=\"h-8 w-8\">\n                                                <AvatarImage\n                                                    src={user.avatar}\n                                                    alt={user.name || 'User avatar'}\n                                                />\n                                                <AvatarFallback>\n                                                    {getUserInitial()}\n                                                </AvatarFallback>\n                                            </Avatar>\n                                        </Button>\n                                    </TooltipTrigger>\n                                    <TooltipContent side=\"right\">\n                                        Expand to view profile\n                                    </TooltipContent>\n                                </Tooltip>\n                            </TooltipProvider>\n                        )}\n                    </div>\n                </>\n            )}\n        </aside>\n    )\n}\n\n// Mobile Navigation Component\nfunction MobileNav({\n                       user,\n                       sections,\n                       onLogout,\n                       brandName = \"Changerawr\",\n                       brandHref = \"/dashboard\"\n                   }: {\n    user: SidebarUser;\n    sections: NavSection[];\n    onLogout: () => Promise<void>;\n    brandName?: string;\n    brandHref?: string;\n}) {\n    const pathname = usePathname()\n    const [isOpen, setIsOpen] = useState(false)\n\n    // Filter navigation items based on user role\n    const filteredNavSections = sections.map(section => ({\n        ...section,\n        items: section.items.filter(item =>\n            !item.requiredRole || item.requiredRole.includes(user.role)\n        )\n    })).filter(section => section.items.length > 0)\n\n    // Function to check if a route is active based on namespace\n    const isRouteActive = (itemHref: string): boolean => {\n        // Exact match for dashboard home\n        if (itemHref === '/dashboard' && pathname === '/dashboard') {\n            return true\n        }\n\n        // For other routes, check if current path starts with the item href\n        // but make sure we don't match dashboard root for other routes\n        if (itemHref !== '/dashboard' && pathname.startsWith(itemHref)) {\n            return true\n        }\n\n        return false\n    }\n\n    const getUserInitial = () => {\n        return user.name?.[0] || user.email?.[0] || '?'\n    }\n\n    const logout = async () => {\n        setIsOpen(false)\n        await onLogout()\n    }\n\n    return (\n        <header className=\"md:hidden fixed top-0 left-0 right-0 h-16 bg-background/80 backdrop-blur-md z-50 border-b\">\n            <div className=\"flex items-center justify-between px-4 h-full\">\n                <Link href={brandHref} className=\"text-xl font-semibold\">\n                    {brandName}\n                </Link>\n\n                <div className=\"flex items-center gap-2\">\n                    <Sheet open={isOpen} onOpenChange={setIsOpen}>\n                        <SheetTrigger asChild>\n                            <Button variant=\"ghost\" size=\"icon\">\n                                <Menu className=\"h-5 w-5\"/>\n                                <span className=\"sr-only\">Open menu</span>\n                            </Button>\n                        </SheetTrigger>\n                        <SheetContent side=\"right\" className=\"w-80 p-0\">\n                            <div className=\"flex flex-col h-full\">\n                                {/* Header */}\n                                <div className=\"flex items-center gap-3 p-6 border-b\">\n                                    <Avatar className=\"h-10 w-10\">\n                                        <AvatarImage\n                                            src={user.avatar}\n                                            alt={user.name || 'User avatar'}\n                                        />\n                                        <AvatarFallback>{getUserInitial()}</AvatarFallback>\n                                    </Avatar>\n                                    <div className=\"flex-1 min-w-0\">\n                                        <p className=\"font-semibold truncate\">\n                                            {user.name || 'Unnamed User'}\n                                        </p>\n                                        <p className=\"text-sm text-muted-foreground truncate\">\n                                            {user.email || 'No email'}\n                                        </p>\n                                        <p className=\"text-xs uppercase text-muted-foreground\">\n                                            {user.role}\n                                        </p>\n                                    </div>\n                                </div>\n\n                                {/* Navigation */}\n                                <nav className=\"flex-1 overflow-y-auto p-6\">\n                                    {filteredNavSections.map((section, sectionIndex) => (\n                                        <div key={sectionIndex} className=\"mb-6\">\n                                            <p className=\"text-xs font-medium text-muted-foreground mb-2\">\n                                                {section.title}\n                                            </p>\n                                            <div className=\"space-y-1\">\n                                                {section.items.map((item) => (\n                                                    <Link\n                                                        key={item.href}\n                                                        href={item.href}\n                                                        onClick={() => setIsOpen(false)}\n                                                        className={cn(\n                                                            \"flex items-center gap-3 p-2 rounded-md transition-colors\",\n                                                            isRouteActive(item.href)\n                                                                ? \"bg-secondary text-secondary-foreground\"\n                                                                : \"hover:bg-secondary/50\"\n                                                        )}\n                                                    >\n                                                        <item.icon className=\"h-5 w-5 text-muted-foreground\" />\n                                                        <span>{item.label}</span>\n                                                    </Link>\n                                                ))}\n                                            </div>\n                                        </div>\n                                    ))}\n                                </nav>\n\n                                {/* Footer Actions */}\n                                <div className=\"border-t p-6 space-y-2\">\n                                    <Button\n                                        variant=\"outline\"\n                                        className=\"w-full justify-start\"\n                                        asChild\n                                    >\n                                        <Link href=\"/dashboard/settings\" onClick={() => setIsOpen(false)}>\n                                            <Settings className=\"h-4 w-4 mr-2\"/>\n                                            Settings\n                                        </Link>\n                                    </Button>\n                                    <Button\n                                        variant=\"destructive\"\n                                        className=\"w-full justify-start\"\n                                        onClick={logout}\n                                    >\n                                        <LogOut className=\"h-4 w-4 mr-2\"/>\n                                        Logout\n                                    </Button>\n                                </div>\n                            </div>\n                        </SheetContent>\n                    </Sheet>\n                </div>\n            </div>\n        </header>\n    )\n}\n\n// Main Sidebar Component\nfunction Sidebar({\n                     user,\n                     sections,\n                     onLogout,\n                     brandName,\n                     brandHref,\n                     isExpanded,\n                     setIsExpanded,\n                     isVisible,\n                     // setIsVisible\n                 }: SidebarProps) {\n    return (\n        <>\n            <DesktopSidebar\n                user={user}\n                sections={sections}\n                onLogout={onLogout}\n                isExpanded={isExpanded}\n                setIsExpanded={setIsExpanded}\n                isVisible={isVisible}\n                // setIsVisible={setIsVisible}\n                brandName={brandName}\n                brandHref={brandHref}\n            />\n            <MobileNav\n                user={user}\n                sections={sections}\n                onLogout={onLogout}\n                brandName={brandName}\n                brandHref={brandHref}\n            />\n        </>\n    )\n}\n\nexport { Sidebar, type SidebarUser, type NavSection, type NavItem }"
  },
  {
    "path": "components/ui/skeleton.tsx",
    "content": "import { cn } from \"@/lib/utils\"\n\nfunction Skeleton({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      className={cn(\"animate-pulse rounded-md bg-muted\", className)}\n      {...props}\n    />\n  )\n}\n\nexport { Skeleton }\n"
  },
  {
    "path": "components/ui/slider.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SliderPrimitive from \"@radix-ui/react-slider\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Slider = React.forwardRef<\n    React.ElementRef<typeof SliderPrimitive.Root>,\n    React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>\n>(({ className, ...props }, ref) => (\n    <SliderPrimitive.Root\n        ref={ref}\n        className={cn(\n            \"relative flex w-full touch-none select-none items-center\",\n            className\n        )}\n        {...props}\n    >\n        <SliderPrimitive.Track className=\"relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20\">\n            <SliderPrimitive.Range className=\"absolute h-full bg-primary\" />\n        </SliderPrimitive.Track>\n        <SliderPrimitive.Thumb className=\"block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\" />\n    </SliderPrimitive.Root>\n))\nSlider.displayName = SliderPrimitive.Root.displayName\n\nexport { Slider }\n"
  },
  {
    "path": "components/ui/switch.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"@/lib/utils\"\n\nconst switchVariants = cva(\n    \"peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input\",\n    {\n        variants: {\n            size: {\n                default: \"h-6 w-11\",\n                sm: \"h-5 w-9\",\n                lg: \"h-7 w-14\",\n            },\n            variant: {\n                default: \"\",\n                pill: \"rounded-full\",\n                square: \"rounded-md\",\n            },\n            withIcon: {\n                true: \"[&>span]:flex [&>span]:items-center [&>span]:justify-center\",\n                false: \"\",\n            },\n        },\n        defaultVariants: {\n            size: \"default\",\n            variant: \"default\",\n            withIcon: false,\n        },\n    }\n)\n\nconst thumbVariants = cva(\n    \"pointer-events-none block rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0\",\n    {\n        variants: {\n            size: {\n                default: \"h-5 w-5\",\n                sm: \"h-4 w-4\",\n                lg: \"h-6 w-6\",\n            },\n            variant: {\n                default: \"rounded-full\",\n                pill: \"rounded-full\",\n                square: \"rounded\",\n            },\n        },\n        defaultVariants: {\n            size: \"default\",\n            variant: \"default\",\n        },\n    }\n)\n\nexport interface SwitchProps\n    extends React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>,\n        VariantProps<typeof switchVariants> {\n    thumbClassName?: string\n    icon?: React.ReactNode\n}\n\nconst Switch = React.forwardRef<\n    React.ElementRef<typeof SwitchPrimitives.Root>,\n    SwitchProps\n>(({\n       className,\n       size,\n       variant,\n       withIcon,\n       thumbClassName,\n       icon,\n       ...props\n   }, ref) => (\n    <SwitchPrimitives.Root\n        className={cn(switchVariants({ size, variant, withIcon }), className)}\n        {...props}\n        ref={ref}\n    >\n        <SwitchPrimitives.Thumb\n            className={cn(\n                thumbVariants({ size, variant }),\n                thumbClassName\n            )}\n        >\n            {withIcon && icon}\n        </SwitchPrimitives.Thumb>\n    </SwitchPrimitives.Root>\n))\nSwitch.displayName = SwitchPrimitives.Root.displayName\n\nexport { Switch }"
  },
  {
    "path": "components/ui/table.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Table = React.forwardRef<\n  HTMLTableElement,\n  React.HTMLAttributes<HTMLTableElement>\n>(({ className, ...props }, ref) => (\n  <div className=\"relative w-full overflow-auto\">\n    <table\n      ref={ref}\n      className={cn(\"w-full caption-bottom text-sm\", className)}\n      {...props}\n    />\n  </div>\n))\nTable.displayName = \"Table\"\n\nconst TableHeader = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <thead ref={ref} className={cn(\"[&_tr]:border-b\", className)} {...props} />\n))\nTableHeader.displayName = \"TableHeader\"\n\nconst TableBody = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tbody\n    ref={ref}\n    className={cn(\"[&_tr:last-child]:border-0\", className)}\n    {...props}\n  />\n))\nTableBody.displayName = \"TableBody\"\n\nconst TableFooter = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tfoot\n    ref={ref}\n    className={cn(\n      \"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0\",\n      className\n    )}\n    {...props}\n  />\n))\nTableFooter.displayName = \"TableFooter\"\n\nconst TableRow = React.forwardRef<\n  HTMLTableRowElement,\n  React.HTMLAttributes<HTMLTableRowElement>\n>(({ className, ...props }, ref) => (\n  <tr\n    ref={ref}\n    className={cn(\n      \"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted\",\n      className\n    )}\n    {...props}\n  />\n))\nTableRow.displayName = \"TableRow\"\n\nconst TableHead = React.forwardRef<\n  HTMLTableCellElement,\n  React.ThHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <th\n    ref={ref}\n    className={cn(\n      \"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0\",\n      className\n    )}\n    {...props}\n  />\n))\nTableHead.displayName = \"TableHead\"\n\nconst TableCell = React.forwardRef<\n  HTMLTableCellElement,\n  React.TdHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <td\n    ref={ref}\n    className={cn(\"p-4 align-middle [&:has([role=checkbox])]:pr-0\", className)}\n    {...props}\n  />\n))\nTableCell.displayName = \"TableCell\"\n\nconst TableCaption = React.forwardRef<\n  HTMLTableCaptionElement,\n  React.HTMLAttributes<HTMLTableCaptionElement>\n>(({ className, ...props }, ref) => (\n  <caption\n    ref={ref}\n    className={cn(\"mt-4 text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n))\nTableCaption.displayName = \"TableCaption\"\n\nexport {\n  Table,\n  TableHeader,\n  TableBody,\n  TableFooter,\n  TableHead,\n  TableRow,\n  TableCell,\n  TableCaption,\n}\n"
  },
  {
    "path": "components/ui/tabs.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Tabs = TabsPrimitive.Root\n\nconst TabsList = React.forwardRef<\n    React.ElementRef<typeof TabsPrimitive.List>,\n    React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & {\n    variant?: \"default\" | \"outline\" | \"pills\" | \"underlined\" | \"minimal\"\n    size?: \"sm\" | \"md\" | \"lg\"\n}\n>(({ className, variant = \"default\", size = \"md\", ...props }, ref) => {\n    const sizeStyles = {\n        sm: \"h-8 text-xs\",\n        md: \"h-10 text-sm\",\n        lg: \"h-12 text-base\"\n    }\n\n    const variantStyles = {\n        default: \"bg-muted p-1 rounded-lg\",\n        outline: \"border rounded-lg p-1\",\n        pills: \"space-x-1\",\n        underlined: \"border-b space-x-2\",\n        minimal: \"space-x-6\"\n    }\n\n    return (\n        <TabsPrimitive.List\n            ref={ref}\n            className={cn(\n                \"inline-flex items-center justify-center text-muted-foreground\",\n                sizeStyles[size],\n                variantStyles[variant],\n                className\n            )}\n            {...props}\n        />\n    )\n})\nTabsList.displayName = TabsPrimitive.List.displayName\n\nconst TabsTrigger = React.forwardRef<\n    React.ElementRef<typeof TabsPrimitive.Trigger>,\n    React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> & {\n    variant?: \"default\" | \"outline\" | \"pills\" | \"underlined\" | \"minimal\"\n    withIndicator?: boolean\n}\n>(({\n       className,\n       variant = \"default\",\n       withIndicator = false,\n       ...props\n   }, ref) => {\n    const variantStyles = {\n        default: \"rounded-md ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm\",\n        outline: \"rounded-md border border-transparent data-[state=active]:border-border data-[state=active]:bg-background data-[state=active]:text-foreground\",\n        pills: \"rounded-full bg-transparent px-4 py-1.5 data-[state=active]:bg-primary data-[state=active]:text-primary-foreground\",\n        underlined: \"rounded-none border-b-2 border-transparent px-1 pt-1 data-[state=active]:border-primary data-[state=active]:text-foreground\",\n        minimal: \"rounded-none text-muted-foreground hover:text-foreground data-[state=active]:text-foreground data-[state=active]:font-medium\"\n    }\n\n    return (\n        <TabsPrimitive.Trigger\n            ref={ref}\n            className={cn(\n                \"relative inline-flex items-center justify-center whitespace-nowrap px-3 py-1.5 font-medium ring-offset-background transition-all focus-visible:outline-none\",\n                variantStyles[variant],\n                className\n            )}\n            {...props}\n        >\n            {props.children}\n            {withIndicator && variant !== \"pills\" && (\n                <div className={cn(\n                    \"absolute left-0 right-0 w-full origin-left transition-all duration-300 ease-spring\",\n                    {\n                        \"bottom-1 h-0.5 rounded-full bg-primary\": variant === \"default\" || variant === \"outline\" || variant === \"minimal\",\n                        \"bottom-0 h-0.5 rounded-none bg-primary\": variant === \"underlined\",\n                        \"opacity-0\": true,\n                        \"data-[state=active]:opacity-100\": true\n                    }\n                )} />\n            )}\n        </TabsPrimitive.Trigger>\n    )\n})\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName\n\nconst TabsContent = React.forwardRef<\n    React.ElementRef<typeof TabsPrimitive.Content>,\n    React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> & {\n    animate?: boolean\n}\n>(({ className, animate = true, ...props }, ref) => {\n    if (animate) {\n        // Instead of using Framer Motion directly with asChild,\n        // use CSS transitions with classes\n        return (\n            <TabsPrimitive.Content\n                ref={ref}\n                className={cn(\n                    \"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n                    \"transition-all duration-200 ease-out\",\n                    \"data-[state=inactive]:opacity-0 data-[state=inactive]:translate-y-2\",\n                    \"data-[state=active]:opacity-100 data-[state=active]:translate-y-0\",\n                    className\n                )}\n                {...props}\n            />\n        )\n    }\n\n    return (\n        <TabsPrimitive.Content\n            ref={ref}\n            className={cn(\n                \"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n                className\n            )}\n            {...props}\n        />\n    )\n})\nTabsContent.displayName = TabsPrimitive.Content.displayName\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent }"
  },
  {
    "path": "components/ui/textarea.tsx",
    "content": "import * as React from \"react\"\n\nimport {cn} from \"@/lib/utils\"\n\nexport interface TextareaProps extends React.ComponentProps<\"textarea\"> {\n    variant?: \"default\" | \"ghost\"\n}\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n    ({className, variant = \"default\", ...props}, ref) => {\n        const baseStyles = \"flex min-h-[80px] w-full bg-background text-base placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\"\n\n        const variantStyles = {\n            default: \"rounded-md border border-input px-3 py-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n            ghost: \"border-0 px-0 py-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0\"\n        }\n\n        return (\n            <textarea\n                className={cn(\n                    baseStyles,\n                    variantStyles[variant],\n                    className\n                )}\n                ref={ref}\n                {...props}\n            />\n        )\n    }\n)\nTextarea.displayName = \"Textarea\"\n\nexport {Textarea}"
  },
  {
    "path": "components/ui/toast.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ToastPrimitives from \"@radix-ui/react-toast\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { X, AlertCircle, CheckCircle, Info, AlertTriangle } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\nconst ToastProvider = ToastPrimitives.Provider\n\nconst ToastViewport = React.forwardRef<\n    React.ElementRef<typeof ToastPrimitives.Viewport>,\n    React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport> & {\n    position?: \"top-right\" | \"top-left\" | \"bottom-right\" | \"bottom-left\" | \"top-center\" | \"bottom-center\"\n}\n>(({ className, position = \"bottom-right\", ...props }, ref) => (\n    <ToastPrimitives.Viewport\n        ref={ref}\n        className={cn(\n            \"fixed z-[100] flex max-h-screen p-4 gap-2\",\n            {\n                \"top-0 right-0 flex-col-reverse sm:top-0 sm:right-0 sm:flex-col\": position === \"top-right\",\n                \"top-0 left-0 flex-col-reverse sm:top-0 sm:left-0 sm:flex-col\": position === \"top-left\",\n                \"bottom-0 right-0 flex-col sm:bottom-0 sm:right-0 sm:flex-col\": position === \"bottom-right\",\n                \"bottom-0 left-0 flex-col sm:bottom-0 sm:left-0 sm:flex-col\": position === \"bottom-left\",\n                \"top-0 right-0 left-0 flex-col-reverse items-center sm:top-0 sm:right-0 sm:left-0 sm:flex-col\": position === \"top-center\",\n                \"bottom-0 right-0 left-0 flex-col items-center sm:bottom-0 sm:right-0 sm:left-0 sm:flex-col\": position === \"bottom-center\",\n            },\n            \"md:max-w-[420px]\",\n            className\n        )}\n        {...props}\n    />\n))\nToastViewport.displayName = ToastPrimitives.Viewport.displayName\n\nconst toastVariants = cva(\n    \"group pointer-events-auto relative isolate flex w-full items-center justify-between space-x-2 overflow-hidden rounded-xl border p-4 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full backdrop-blur-sm\",\n    {\n        variants: {\n            variant: {\n                default: [\n                    // Base styling with optical border\n                    \"border border-border/40 bg-background/90 text-foreground backdrop-blur-md\",\n                    // Background layer for depth\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.xl)-1px)] before:bg-gradient-to-b before:from-background/90 before:to-background/70\",\n                    // Subtle highlight\n                    \"after:absolute after:inset-0 after:-z-10 after:rounded-[calc(theme(borderRadius.xl)-1px)]\",\n                    \"after:shadow-[inset_0_1px_theme(colors.white/10%)]\",\n                    \"shadow-lg shadow-black/10\",\n                ],\n                destructive: [\n                    \"border border-destructive/30 bg-destructive/10 text-destructive backdrop-blur-md\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.xl)-1px)]\",\n                    \"before:bg-gradient-to-b before:from-destructive/20 before:to-destructive/5\",\n                    \"after:absolute after:inset-0 after:-z-10 after:rounded-[calc(theme(borderRadius.xl)-1px)]\",\n                    \"after:shadow-[inset_0_1px_theme(colors.red.200/20%)]\",\n                    \"shadow-lg shadow-red-500/20\",\n                ],\n                success: [\n                    \"border border-green-500/30 bg-green-500/10 text-green-700 dark:text-green-400 backdrop-blur-md\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.xl)-1px)]\",\n                    \"before:bg-gradient-to-b before:from-green-500/20 before:to-green-500/5\",\n                    \"after:absolute after:inset-0 after:-z-10 after:rounded-[calc(theme(borderRadius.xl)-1px)]\",\n                    \"after:shadow-[inset_0_1px_theme(colors.green.200/20%)]\",\n                    \"shadow-lg shadow-green-500/20\",\n                ],\n                warning: [\n                    \"border border-yellow-500/30 bg-yellow-500/10 text-yellow-700 dark:text-yellow-400 backdrop-blur-md\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.xl)-1px)]\",\n                    \"before:bg-gradient-to-b before:from-yellow-500/20 before:to-yellow-500/5\",\n                    \"after:absolute after:inset-0 after:-z-10 after:rounded-[calc(theme(borderRadius.xl)-1px)]\",\n                    \"after:shadow-[inset_0_1px_theme(colors.yellow.200/20%)]\",\n                    \"shadow-lg shadow-yellow-500/20\",\n                ],\n                info: [\n                    \"border border-blue-500/30 bg-blue-500/10 text-blue-700 dark:text-blue-400 backdrop-blur-md\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.xl)-1px)]\",\n                    \"before:bg-gradient-to-b before:from-blue-500/20 before:to-blue-500/5\",\n                    \"after:absolute after:inset-0 after:-z-10 after:rounded-[calc(theme(borderRadius.xl)-1px)]\",\n                    \"after:shadow-[inset_0_1px_theme(colors.blue.200/20%)]\",\n                    \"shadow-lg shadow-blue-500/20\",\n                ],\n                glass: [\n                    \"border border-white/20 bg-white/10 backdrop-blur-xl text-foreground\",\n                    \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.xl)-1px)]\",\n                    \"before:bg-gradient-to-b before:from-white/20 before:to-white/5\",\n                    \"shadow-xl shadow-black/20\",\n                    \"dark:border-white/10 dark:bg-white/5 dark:before:from-white/15 dark:before:to-white/[0.02]\",\n                ],\n            },\n            size: {\n                default: \"p-4\",\n                sm: \"p-3 text-sm\",\n                lg: \"p-6 text-base\",\n            },\n            withIcon: {\n                true: \"pl-12\",\n                false: \"\",\n            },\n        },\n        defaultVariants: {\n            variant: \"default\",\n            size: \"default\",\n            withIcon: false,\n        },\n    }\n)\n\nconst iconMap = {\n    default: null,\n    destructive: AlertCircle,\n    success: CheckCircle,\n    warning: AlertTriangle,\n    info: Info,\n    glass: Info,\n}\n\nconst Toast = React.forwardRef<\n    React.ElementRef<typeof ToastPrimitives.Root>,\n    React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &\n    VariantProps<typeof toastVariants> & {\n    icon?: React.ReactNode\n}\n>(({ className, variant = \"default\", size, withIcon, icon, children, ...props }, ref) => {\n    const IconComponent = iconMap[variant as keyof typeof iconMap]\n    const toastIcon = icon || (IconComponent ? <IconComponent className=\"h-5 w-5\" /> : null)\n    const hasIcon = !!toastIcon || withIcon\n\n    return (\n        <ToastPrimitives.Root\n            ref={ref}\n            className={cn(toastVariants({ variant, size, withIcon: hasIcon }), className)}\n            {...props}\n        >\n            {hasIcon && (\n                <div className=\"absolute left-4 top-1/2 -translate-y-1/2 z-10\">\n                    {toastIcon}\n                </div>\n            )}\n            <div className=\"grid gap-1 w-full\">{children}</div>\n            <ToastPrimitives.Close className=\"absolute right-2 top-2 rounded-md p-1.5 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600\">\n                <X className=\"h-4 w-4\" />\n                <span className=\"sr-only\">Close</span>\n            </ToastPrimitives.Close>\n        </ToastPrimitives.Root>\n    )\n})\nToast.displayName = ToastPrimitives.Root.displayName\n\nconst ToastAction = React.forwardRef<\n    React.ElementRef<typeof ToastPrimitives.Action>,\n    React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action> & {\n    variant?: \"default\" | \"primary\" | \"secondary\" | \"outline\" | \"destructive\"\n}\n>(({ className, variant = \"default\", ...props }, ref) => (\n    <ToastPrimitives.Action\n        ref={ref}\n        className={cn(\n            \"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-all focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n            {\n                \"border-muted/40 hover:bg-secondary hover:text-secondary-foreground\": variant === \"default\",\n                \"border-transparent bg-primary/20 text-primary hover:bg-primary/30\": variant === \"primary\",\n                \"border-transparent bg-secondary/20 text-secondary-foreground hover:bg-secondary/30\": variant === \"secondary\",\n                \"border-border hover:bg-accent hover:text-accent-foreground\": variant === \"outline\",\n                \"border-transparent bg-destructive/20 text-destructive hover:bg-destructive/30\": variant === \"destructive\",\n            },\n            \"group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive\",\n            className\n        )}\n        {...props}\n    />\n))\nToastAction.displayName = ToastPrimitives.Action.displayName\n\nconst ToastClose = React.forwardRef<\n    React.ElementRef<typeof ToastPrimitives.Close>,\n    React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n    <ToastPrimitives.Close\n        ref={ref}\n        className={cn(\n            \"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600\",\n            className\n        )}\n        toast-close=\"\"\n        {...props}\n    >\n    </ToastPrimitives.Close>\n))\nToastClose.displayName = ToastPrimitives.Close.displayName\n\nconst ToastTitle = React.forwardRef<\n    React.ElementRef<typeof ToastPrimitives.Title>,\n    React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => (\n    <ToastPrimitives.Title\n        ref={ref}\n        className={cn(\n            \"text-sm font-semibold bg-gradient-to-r from-foreground to-foreground/80 bg-clip-text\",\n            className\n        )}\n        {...props}\n    />\n))\nToastTitle.displayName = ToastPrimitives.Title.displayName\n\nconst ToastDescription = React.forwardRef<\n    React.ElementRef<typeof ToastPrimitives.Description>,\n    React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n    <ToastPrimitives.Description\n        ref={ref}\n        className={cn(\"text-sm opacity-90 leading-relaxed\", className)}\n        {...props}\n    />\n))\nToastDescription.displayName = ToastPrimitives.Description.displayName\n\n// Progress indicator for timed toasts\nconst ToastProgress = React.forwardRef<\n    HTMLDivElement,\n    React.HTMLAttributes<HTMLDivElement> & {\n    value: number // 0-100\n    variant?: \"default\" | \"destructive\" | \"success\" | \"warning\" | \"info\"\n}\n>(({ className, value, variant = \"default\", ...props }, ref) => (\n    <div\n        ref={ref}\n        className={cn(\n            \"absolute bottom-0 left-0 right-0 h-1 bg-current/10 overflow-hidden rounded-b-xl\",\n            className\n        )}\n        {...props}\n    >\n        <div\n            className={cn(\n                \"h-full transition-all duration-100\",\n                {\n                    \"bg-current/40\": variant === \"default\",\n                    \"bg-destructive/60\": variant === \"destructive\",\n                    \"bg-green-500/60\": variant === \"success\",\n                    \"bg-yellow-500/60\": variant === \"warning\",\n                    \"bg-blue-500/60\": variant === \"info\",\n                }\n            )}\n            style={{ width: `${value}%` }}\n        />\n    </div>\n))\nToastProgress.displayName = \"ToastProgress\"\n\ntype ToastProps = React.ComponentPropsWithoutRef<typeof Toast>\ntype ToastActionElement = React.ReactElement<typeof ToastAction>\n\nexport {\n    type ToastProps,\n    type ToastActionElement,\n    ToastProvider,\n    ToastViewport,\n    Toast,\n    ToastTitle,\n    ToastDescription,\n    ToastClose,\n    ToastAction,\n    ToastProgress,\n}"
  },
  {
    "path": "components/ui/toaster.tsx",
    "content": "\"use client\"\n\nimport { useToast } from \"@/hooks/use-toast\"\nimport {\n  Toast,\n  ToastClose,\n  ToastDescription,\n  ToastProvider,\n  ToastTitle,\n  ToastViewport,\n} from \"@/components/ui/toast\"\n\nexport function Toaster() {\n  const { toasts } = useToast()\n\n  return (\n    <ToastProvider>\n      {toasts.map(function ({ id, title, description, action, ...props }) {\n        return (\n          <Toast key={id} {...props}>\n            <div className=\"grid gap-1\">\n              {title && <ToastTitle>{title}</ToastTitle>}\n              {description && (\n                <ToastDescription>{description}</ToastDescription>\n              )}\n            </div>\n            {action}\n            <ToastClose />\n          </Toast>\n        )\n      })}\n      <ToastViewport />\n    </ToastProvider>\n  )\n}\n"
  },
  {
    "path": "components/ui/tooltip.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst TooltipProvider = TooltipPrimitive.Provider\n\nconst Tooltip = TooltipPrimitive.Root\n\nconst TooltipTrigger = TooltipPrimitive.Trigger\n\nconst TooltipContent = React.forwardRef<\n    React.ElementRef<typeof TooltipPrimitive.Content>,\n    React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> & {\n    variant?: \"default\" | \"info\" | \"warning\" | \"error\" | \"success\"\n    withArrow?: boolean\n    size?: \"sm\" | \"md\" | \"lg\"\n}\n>(({\n       className,\n       sideOffset = 4,\n       withArrow = true,\n       variant = \"default\",\n       size = \"md\",\n       ...props\n   }, ref) => {\n\n    const variantStyles = {\n        default: \"bg-popover text-popover-foreground border-border\",\n        info: \"bg-blue-50 text-blue-900 border-blue-200 dark:bg-blue-950 dark:text-blue-200 dark:border-blue-800\",\n        warning: \"bg-amber-50 text-amber-900 border-amber-200 dark:bg-amber-950 dark:text-amber-200 dark:border-amber-800\",\n        error: \"bg-red-50 text-red-900 border-red-200 dark:bg-red-950 dark:text-red-200 dark:border-red-800\",\n        success: \"bg-green-50 text-green-900 border-green-200 dark:bg-green-950 dark:text-green-200 dark:border-green-800\",\n    }\n\n    const sizeStyles = {\n        sm: \"py-1 px-2 text-xs\",\n        md: \"py-1.5 px-3 text-sm\",\n        lg: \"py-2 px-4 text-base\"\n    }\n\n    return (\n        <TooltipPrimitive.Content\n            ref={ref}\n            sideOffset={sideOffset}\n            className={cn(\n                \"z-50 overflow-hidden rounded-md border shadow-md\",\n                \"data-[state=open]:animate-in data-[state=closed]:animate-out\",\n                \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n                \"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\",\n                \"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2\",\n                \"data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n                sizeStyles[size],\n                variantStyles[variant],\n                className\n            )}\n            {...props}\n        >\n            {props.children}\n            {withArrow && (\n                <TooltipPrimitive.Arrow\n                    className={cn(\n                        \"fill-current\",\n                        {\n                            \"fill-border\": variant === \"default\",\n                            \"fill-blue-200 dark:fill-blue-800\": variant === \"info\",\n                            \"fill-amber-200 dark:fill-amber-800\": variant === \"warning\",\n                            \"fill-red-200 dark:fill-red-800\": variant === \"error\",\n                            \"fill-green-200 dark:fill-green-800\": variant === \"success\",\n                        }\n                    )}\n                    width={10}\n                    height={5}\n                />\n            )}\n        </TooltipPrimitive.Content>\n    )\n})\nTooltipContent.displayName = TooltipPrimitive.Content.displayName\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }"
  },
  {
    "path": "components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.ts\",\n    \"css\": \"app/globals.css\",\n    \"baseColor\": \"slate\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"iconLibrary\": \"lucide\"\n}"
  },
  {
    "path": "context/auth.tsx",
    "content": "'use client'\n\nimport React, { createContext, useContext, useEffect, useState, useRef, useCallback } from 'react'\nimport { useRouter } from 'next/navigation'\nimport { User } from '@prisma/client';\n\ninterface AuthContextType {\n    user: User | null\n    login: (email: string, password: string) => Promise<void>\n    logout: () => Promise<void>\n    isLoading: boolean\n    testRefresh?: boolean\n}\n\nconst AuthContext = createContext<AuthContextType | undefined>(undefined)\n\nconst REFRESH_INTERVAL = 15 * 60 * 1000 // 15 minutes\nconst TEST_REFRESH_INTERVAL = 15 * 1000 // 15 seconds\nconst REFRESH_THRESHOLD = 0.8 // Refresh at 80% of token lifetime\n\nexport function AuthProvider({\n                                 children,\n                                 testRefresh = false\n                             }: {\n    children: React.ReactNode\n    testRefresh?: boolean\n}) {\n    const [user, setUser] = useState<User | null>(null)\n    const [isLoading, setIsLoading] = useState(true)\n    const router = useRouter()\n    // @ts-expect-error 1 arg but zero nw\n    const refreshTimerRef = useRef<NodeJS.Timeout>()\n\n    const refreshInterval = testRefresh ? TEST_REFRESH_INTERVAL : REFRESH_INTERVAL\n    const timeUntilRefresh = refreshInterval * REFRESH_THRESHOLD\n\n    const refreshToken = async () => {\n        try {\n            const refreshResponse = await fetch('/api/auth/refresh', {\n                method: 'POST',\n                credentials: 'include', // Important for cookies\n                headers: {\n                    'Content-Type': 'application/json'\n                }\n            })\n\n            if (refreshResponse.ok) {\n                const { user: refreshedUser } = await refreshResponse.json()\n                setUser(refreshedUser)\n                return true\n            }\n\n            // If refresh fails, clear user state\n            setUser(null)\n            return false\n        } catch (error) {\n            console.error('Token refresh failed:', error)\n            setUser(null)\n            return false\n        }\n    }\n\n    const scheduleTokenRefresh = useCallback(() => {\n        if (refreshTimerRef.current) {\n            clearTimeout(refreshTimerRef.current)\n        }\n\n        refreshTimerRef.current = setTimeout(async () => {\n            if (user) {\n                const success = await refreshToken()\n                if (success) {\n                    scheduleTokenRefresh()\n                } else {\n                    await logout()\n                }\n            }\n        }, timeUntilRefresh)\n    }, [user, timeUntilRefresh])\n\n    const checkAuthState = useCallback(async () => {\n        setIsLoading(true)\n        try {\n            const response = await fetch('/api/auth/me', {\n                credentials: 'include',\n                cache: 'no-store'\n            })\n\n            if (response.ok) {\n                const userData = await response.json()\n                setUser(userData)\n            } else {\n                // If me endpoint fails, try to refresh token\n                const success = await refreshToken()\n                if (!success) {\n                    setUser(null)\n                }\n            }\n        } catch (error) {\n            console.error('Authentication check failed:', error)\n            setUser(null)\n        } finally {\n            setIsLoading(false)\n        }\n    }, [])\n\n    const logout = useCallback(async () => {\n        try {\n            setIsLoading(true)\n            if (refreshTimerRef.current) {\n                clearTimeout(refreshTimerRef.current)\n            }\n\n            await fetch('/api/auth/logout', {\n                method: 'POST',\n                credentials: 'include'\n            })\n        } catch (error) {\n            console.error('Logout error:', error)\n        } finally {\n            setUser(null)\n            setIsLoading(false)\n            router.push('/login')\n        }\n    }, [router])\n\n    // Initial auth check\n    useEffect(() => {\n        checkAuthState()\n    }, [checkAuthState])\n\n    // Schedule refresh when user changes\n    useEffect(() => {\n        if (user) {\n            scheduleTokenRefresh()\n        }\n\n        return () => {\n            if (refreshTimerRef.current) {\n                clearTimeout(refreshTimerRef.current)\n            }\n        }\n    }, [user, scheduleTokenRefresh])\n\n    const login = async (email: string, password: string) => {\n        try {\n            setIsLoading(true)\n            const response = await fetch('/api/auth/login', {\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json'\n                },\n                body: JSON.stringify({ email, password }),\n                credentials: 'include'\n            })\n\n            if (!response.ok) {\n                const errorData = await response.json()\n                throw new Error(errorData.error || 'Authentication failed')\n            }\n\n            const { user: userData } = await response.json()\n            setUser(userData)\n            router.push('/dashboard')\n        } catch (error) {\n            console.error('Login error:', error)\n            setUser(null)\n            throw error\n        } finally {\n            setIsLoading(false)\n        }\n    }\n\n    return (\n        <AuthContext.Provider\n            value={{\n                user,\n                login,\n                logout,\n                isLoading,\n                testRefresh\n            }}\n        >\n            {children}\n        </AuthContext.Provider>\n    )\n}\n\nexport function useAuth() {\n    const context = useContext(AuthContext)\n    if (context === undefined) {\n        throw new Error('useAuth must be used within an AuthProvider')\n    }\n    return context\n}"
  },
  {
    "path": "docker-compose-online.yml",
    "content": "version: '3.8'\n\nservices:\n  postgres:\n    image: postgres:16-alpine\n    container_name: changerawr_postgres\n    environment:\n      POSTGRES_USER: postgres\n      POSTGRES_PASSWORD: postgres\n      POSTGRES_DB: changerawr\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\n    ports:\n      - \"5432:5432\"\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U postgres -d changerawr\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    restart: unless-stopped\n\n  app:\n    image: ghcr.io/supernova3339/changerawr:latest\n    container_name: changerawr_app\n    depends_on:\n      postgres:\n        condition: service_healthy\n    environment:\n      DATABASE_URL: \"postgresql://postgres:postgres@postgres:5432/changerawr?schema=public\"\n      JWT_ACCESS_SECRET: \"your_jwt_secret_key_here_change_me\"\n      NEXT_PUBLIC_APP_URL: \"http://localhost:3000\"\n      GITHUB_ENCRYPTION_KEY: \"abcdef1234567890abcdef1234567890\"\n      ANALYTICS_SALT: \"your_secure_analytics_salt_here\"\n      NODE_ENV: \"production\"\n    ports:\n      - \"3000:3000\"\n    volumes:\n      - app_uploads:/app/uploads\n      - app_public:/app/public/generated\n    healthcheck:\n      test: [\"CMD-SHELL\", \"curl -f http://localhost:3000/api/health || exit 1\"]\n      interval: 30s\n      timeout: 10s\n      retries: 3\n      start_period: 60s\n    restart: unless-stopped\n\nvolumes:\n  postgres_data:\n    driver: local\n  app_uploads:\n    driver: local\n  app_public:\n    driver: local\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3.8'\n\nservices:\n  postgres:\n    image: postgres:16-alpine\n    container_name: changerawr_postgres\n    environment:\n      POSTGRES_USER: postgres\n      POSTGRES_PASSWORD: postgres\n      POSTGRES_DB: changerawr\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\n    ports:\n      - \"5432:5432\"\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U postgres -d changerawr\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    restart: unless-stopped\n\n  app:\n    build:\n      context: .\n      dockerfile: Dockerfile.compose\n    container_name: changerawr_app\n    depends_on:\n      postgres:\n        condition: service_healthy\n    environment:\n      DATABASE_URL: \"postgresql://postgres:postgres@postgres:5432/changerawr?schema=public\"\n      JWT_ACCESS_SECRET: \"your_jwt_secret_key_here_change_me\"\n      NEXT_PUBLIC_APP_URL: \"http://localhost:3000\"\n      GITHUB_ENCRYPTION_KEY: \"abcdef1234567890abcdef1234567890\"\n      ANALYTICS_SALT: \"your_secure_analytics_salt_here\"\n      NODE_ENV: \"production\"\n      # SSL Certificate Management\n      DOCKER_DEPLOYMENT: \"true\"\n      ENCRYPTION_KEY: \"abcdef1234567890abcdef1234567890\" # Must be 32 bytes base64 (same as GITHUB_ENCRYPTION_KEY)\n      ACME_EMAIL: \"admin@example.com\" # Email for Let's Encrypt notifications\n      ACME_STAGING: \"true\" # Set to \"false\" in production\n      INTERNAL_API_SECRET: \"your_internal_api_secret_here_change_me\"\n      NGINX_AGENT_URL: \"http://chr-nginx-agent:8080\" # Optional: nginx-agent URL\n      NGINX_AGENT_SECRET: \"your_nginx_agent_secret_here_change_me\" # Optional: webhook signature secret\n      CRON_SECRET: \"your_cron_secret_here_change_me\"\n    ports:\n      - \"3000:3000\"  # Next.js app (internal)\n      - \"80:80\"      # HTTP (nginx)\n      - \"443:443\"    # HTTPS (nginx)\n      - \"7842:7842\"  # nginx-agent API\n    volumes:\n      - app_uploads:/app/uploads\n      - app_public:/app/public/generated\n      - nginx_certs:/etc/ssl/changerawr\n      - nginx_configs:/etc/nginx/sites-enabled\n    healthcheck:\n      test: [\"CMD-SHELL\", \"curl -f http://localhost:3000/api/health || exit 1\"]\n      interval: 30s\n      timeout: 10s\n      retries: 3\n      start_period: 60s\n    restart: unless-stopped\n\n  # Caddy reverse proxy with on-demand TLS\n  caddy:\n    image: caddy:2-alpine\n    container_name: changerawr_caddy\n    profiles: [\"ssl\"]\n    depends_on:\n      - app\n    ports:\n      - \"80:80\"\n      - \"443:443\"\n    volumes:\n      - ./Caddyfile:/etc/caddy/Caddyfile:ro\n      - caddy_data:/data\n      - caddy_config:/config\n    restart: unless-stopped\n\n  # SSL certificate renewal cron job\n  ssl-cron:\n    image: curlimages/curl:latest\n    container_name: changerawr_ssl_cron\n    profiles: [\"ssl\"]\n    depends_on:\n      - app\n    entrypoint: /bin/sh\n    command:\n      - -c\n      - |\n        while true; do\n          sleep 43200  # 12 hours\n          curl -H \"Authorization: Bearer $${CRON_SECRET}\" http://app:3000/api/cron/ssl-renewal || true\n        done\n    environment:\n      CRON_SECRET: \"your_cron_secret_here_change_me\"\n    restart: unless-stopped\n\nvolumes:\n  postgres_data:\n    driver: local\n  app_uploads:\n    driver: local\n  app_public:\n    driver: local\n  caddy_data:\n    driver: local\n  caddy_config:\n    driver: local\n  nginx_certs:\n    driver: local\n  nginx_configs:\n    driver: local"
  },
  {
    "path": "docker-entrypoint-compose.sh",
    "content": "#!/bin/bash\nset -e\n\necho \"🦖 Starting Changerawr deployment...\"\n\n# Start maintenance server in the background\necho \"🦖 Starting maintenance server...\"\nnode scripts/maintenance/server.js &\nMAINTENANCE_PID=$!\n\n# Function to cleanup maintenance server\ncleanup_maintenance() {\n    echo \"🦖 Stopping maintenance server...\"\n    kill $MAINTENANCE_PID 2>/dev/null || true\n    wait $MAINTENANCE_PID 2>/dev/null || true\n}\n\n# Trap to ensure maintenance server is cleaned up\ntrap cleanup_maintenance EXIT\n\n# Give maintenance server a moment to start\nsleep 2\n\necho \"🦖 Maintenance server running (PID: $MAINTENANCE_PID)\"\necho \"🦖 Starting application setup...\"\n\n# Generate Prisma client\necho \"🦖 Generating Prisma client...\"\nnpx prisma generate\n\n# Run database migrations\necho \"🦖 Running database migrations...\"\nnpx prisma migrate deploy\n\n# Run the widget build script\necho \"🦖 Building widget...\"\nnpm run build:widget\n\n# Generate Swagger documentation\necho \"🦖 Generating Swagger documentation...\"\nnpm run generate-swagger\n\n# Stop maintenance server\necho \"🦖 Setup complete! Stopping maintenance server...\"\ncleanup_maintenance\n\n# Small delay to ensure port is released\nsleep 1\n\n# Execute the main application\necho \"🦖 Starting main application...\"\nnpm start"
  },
  {
    "path": "docker-entrypoint.sh",
    "content": "#!/bin/bash\nset -e\n\necho \"🦖 Starting Changerawr deployment...\"\n\n# Start maintenance server in the background\necho \"🦖 Starting maintenance server...\"\nnode scripts/maintenance/server.js &\nMAINTENANCE_PID=$!\n\n# Function to cleanup maintenance server\ncleanup_maintenance() {\n    if [ -n \"$MAINTENANCE_PID\" ]; then\n        echo \"🦖 Stopping maintenance server...\"\n        kill $MAINTENANCE_PID 2>/dev/null || true\n        # Don't wait - just kill and move on\n    fi\n}\n\n# Trap to ensure maintenance server is cleaned up\ntrap cleanup_maintenance EXIT\n\n# Give maintenance server a moment to start\nsleep 2\n\necho \"🦖 Maintenance server running (PID: $MAINTENANCE_PID)\"\necho \"🦖 Starting application setup...\"\n\n# Generate Prisma client\necho \"🦖 Generating Prisma client...\"\nnpx prisma generate\n\n# Run database migrations\necho \"🦖 Running database migrations...\"\nnpx prisma migrate deploy\n\n# Run the widget build script\necho \"🦖 Building widget...\"\nnpm run build:widget\n\n# Generate Swagger documentation\necho \"🦖 Generating Swagger documentation...\"\nnpm run generate-swagger\n\n# Stop maintenance server\necho \"🦖 Setup complete! Stopping maintenance server...\"\ncleanup_maintenance\n\n# Small delay to ensure port is released\nsleep 1\n\n# Clean up any leftover domain configs from previous runs that might reference missing certs\necho \"🦖 Cleaning up any stale domain configs...\"\nrm -f /etc/nginx/sites-enabled/*.conf 2>/dev/null || true\necho \"🦖 Cleaned up $(ls -1 /etc/nginx/sites-enabled/*.conf 2>/dev/null | wc -l) domain configs\"\n\n# Test and start nginx in daemon mode (background)\necho \"🦖 Testing nginx configuration...\"\nif ! nginx -t 2>&1; then\n    echo \"❌ nginx configuration test failed even after cleanup!\"\n    echo \"🦖 Last chance: nuking cert directory and retrying...\"\n\n    # Nuclear option: remove all certs and configs\n    rm -rf /etc/ssl/changerawr/* 2>/dev/null || true\n    rm -f /etc/nginx/sites-enabled/*.conf 2>/dev/null || true\n\n    if ! nginx -t 2>&1; then\n        echo \"❌ nginx configuration is fundamentally broken, exiting...\"\n        exit 1\n    fi\n    echo \"✅ nginx configuration fixed after nuclear cleanup!\"\nfi\n\necho \"🦖 Starting nginx...\"\nnginx 2>&1\nif [ $? -eq 0 ]; then\n    echo \"🦖 nginx started successfully\"\nelse\n    echo \"⚠️  nginx failed to start, continuing without nginx...\"\nfi\n\n# Start nginx-agent if SSL is enabled\nif [ \"$NEXT_PUBLIC_SSL_ENABLED\" = \"true\" ]; then\n    echo \"🦖 Starting nginx-agent...\"\n    if [ -d /nginx-agent ]; then\n        cd /nginx-agent\n\n        # Set agent environment variables\n        export AGENT_SECRET=\"${NGINX_AGENT_SECRET}\"\n        export CHANGERAWR_URL=\"http://127.0.0.1:3000\"\n        export INTERNAL_API_SECRET=\"${INTERNAL_API_SECRET}\"\n        export AGENT_PORT=\"${NGINX_AGENT_PORT:-7842}\"\n        export CERT_DIR=\"/etc/ssl/changerawr\"\n        export NGINX_DIR=\"/etc/nginx/sites-enabled\"\n        export NGINX_RELOAD_CMD=\"/usr/local/bin/nginx-reload.sh\"\n\n        # Make sure agent doesn't try to bind to port 80\n        npm start 2>&1 &\n        NGINX_AGENT_PID=$!\n        echo \"🦖 nginx-agent running (PID: $NGINX_AGENT_PID)\"\n        cd /app\n    else\n        echo \"⚠️  nginx-agent directory not found, skipping...\"\n    fi\nelse\n    echo \"🦖 SSL not enabled, skipping nginx-agent...\"\nfi\n\n# Start Next.js application in background\necho \"🦖 Starting Next.js application on port 3000...\"\n# Ensure Next.js uses port 3000\nexport PORT=3000\nexport HOSTNAME=\"0.0.0.0\"\n\"$@\" &\nAPP_PID=$!\necho \"🦖 Next.js running (PID: $APP_PID)\"\n\n# Function to handle shutdown gracefully\nshutdown() {\n    echo \"🦖 Shutting down...\"\n\n    # Stop Next.js\n    if [ -n \"$APP_PID\" ]; then\n        echo \"🦖 Stopping Next.js (PID: $APP_PID)...\"\n        kill -TERM \"$APP_PID\" 2>/dev/null || true\n        wait \"$APP_PID\" 2>/dev/null || true\n    fi\n\n    # Stop nginx-agent\n    if [ -n \"$NGINX_AGENT_PID\" ]; then\n        echo \"🦖 Stopping nginx-agent (PID: $NGINX_AGENT_PID)...\"\n        kill -TERM \"$NGINX_AGENT_PID\" 2>/dev/null || true\n        wait \"$NGINX_AGENT_PID\" 2>/dev/null || true\n    fi\n\n    # Stop nginx\n    echo \"🦖 Stopping nginx...\"\n    nginx -s quit 2>/dev/null || true\n\n    echo \"🦖 Shutdown complete\"\n    exit 0\n}\n\n# Trap signals for graceful shutdown\ntrap shutdown SIGTERM SIGINT\n\n# Wait for Next.js process (keeps container alive)\necho \"🦖 All services started. Waiting for Next.js process...\"\nwait \"$APP_PID\"\n\n# If Next.js exits, trigger shutdown\necho \"🦖 Next.js process exited\"\nshutdown"
  },
  {
    "path": "emails/approval-notification.tsx",
    "content": "import * as React from 'react';\nimport {\n    Html,\n    Head,\n    Body,\n    Container,\n    Section,\n    Heading,\n    Text,\n    Hr,\n    Button,\n} from '@react-email/components';\n\ninterface ApprovalNotificationEmailProps {\n    recipientName?: string;\n    projectName: string;\n    requestType: string;\n    entryTitle?: string;\n    adminName?: string;\n    dashboardUrl: string;\n}\n\nexport const ApprovalNotificationEmail: React.FC<ApprovalNotificationEmailProps> = ({\n                                                                                        recipientName,\n                                                                                        projectName,\n                                                                                        requestType,\n                                                                                        entryTitle,\n                                                                                        adminName = 'An administrator',\n                                                                                        dashboardUrl,\n                                                                                    }) => {\n    const displayRequestType = requestType.replace(/_/g, ' ').toLowerCase();\n\n    return (\n        <Html>\n            <Head>\n                <title>Your Request Has Been Approved</title>\n            </Head>\n            <Body style={{\n                fontFamily: '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif',\n                backgroundColor: '#f6f9fc',\n                margin: '0 auto',\n                padding: '20px 0'\n            }}>\n                <Container style={{\n                    backgroundColor: '#ffffff',\n                    borderRadius: '8px',\n                    boxShadow: '0 2px 8px rgba(0, 0, 0, 0.05)',\n                    maxWidth: '600px',\n                    margin: '0 auto',\n                    padding: '20px'\n                }}>\n                    <Section>\n                        <Heading as=\"h1\" style={{\n                            color: '#333',\n                            fontSize: '24px',\n                            fontWeight: 'bold',\n                            margin: '10px 0 20px',\n                            textAlign: 'center'\n                        }}>\n                            Request Approved\n                        </Heading>\n\n                        <Text style={{\n                            color: '#333',\n                            fontSize: '16px',\n                            margin: '0 0 15px'\n                        }}>\n                            Hello {recipientName || 'there'},\n                        </Text>\n\n                        <Text style={{\n                            color: '#333',\n                            fontSize: '16px',\n                            margin: '0 0 20px'\n                        }}>\n                            Good news! Your request to {displayRequestType} {entryTitle\n                            ? <>the entry <strong>&ldquo;{entryTitle}&rdquo;</strong> in</>\n                            : <>for</>} <strong>{projectName}</strong> has been approved by {adminName}.\n                        </Text>\n\n                        <Section style={{ textAlign: 'center', margin: '30px 0' }}>\n                            <Button\n                                href={dashboardUrl}\n                                style={{\n                                    backgroundColor: '#0891b2',\n                                    borderRadius: '4px',\n                                    color: '#fff',\n                                    fontSize: '16px',\n                                    textDecoration: 'none',\n                                    textAlign: 'center',\n                                    display: 'inline-block',\n                                    padding: '12px 20px'\n                                }}\n                            >\n                                View Dashboard\n                            </Button>\n                        </Section>\n\n                        <Hr style={{ margin: '30px 0 20px' }} />\n\n                        <Text style={{\n                            color: '#999',\n                            fontSize: '12px',\n                            textAlign: 'center'\n                        }}>\n                            This is an automated notification from Changerawr. You received this email because you have notifications enabled in your account settings.\n                        </Text>\n                    </Section>\n                </Container>\n            </Body>\n        </Html>\n    );\n};\n\nexport default ApprovalNotificationEmail;"
  },
  {
    "path": "emails/changelog.tsx",
    "content": "import * as React from 'react';\nimport {\n    Html,\n    Head,\n    Body,\n    Container,\n    Section,\n    Row,\n    Column,\n    Heading,\n    Text,\n    Hr,\n    Link\n} from '@react-email/components';\nimport { renderMarkdown } from '@/lib/services/core/markdown/useCustomExtensions';\n\n/**\n * Sanitizes HTML to fix malformed tags (unclosed li, p, etc.)\n * This is necessary because the markdown renderer may produce non-strict HTML\n */\nfunction sanitizeHtml(html: string): string {\n    // Fix unclosed li tags by ensuring proper closing\n    let sanitized = html.replace(/<li>([^<]*)<li>/g, '<li>$1</li><li>');\n    sanitized = sanitized.replace(/<li>([^<]*)<\\/li><li>/g, '<li>$1</li><li>');\n\n    // Ensure all li tags are properly closed before closing ul/ol\n    sanitized = sanitized.replace(/<li>([^<]*)<\\/(ul|ol)>/g, '<li>$1</li></$2>');\n\n    // Fix unclosed p tags\n    sanitized = sanitized.replace(/<p>([^<]*)<p>/g, '<p>$1</p><p>');\n    sanitized = sanitized.replace(/<p>([^<]*)<\\/p><p>/g, '<p>$1</p><p>');\n\n    return sanitized;\n}\n\ninterface Entry {\n    id: string;\n    title: string;\n    content: string;\n    version?: string | null;\n    publishedAt?: Date | null;\n    tags?: { id: string; name: string }[];\n}\n\ninterface ChangelogEmailProps {\n    projectName: string;\n    entries: Entry[];\n    isDigest?: boolean;\n    unsubscribeUrl?: string;\n    recipientName?: string; // Added recipient name for personalization\n    recipientEmail?: string; // Added recipient email for fallback\n    changelogUrl: string,\n    customDomain?: string // optional custom domain\n}\n\nexport const ChangelogEmail: React.FC<ChangelogEmailProps> = ({\n                                                                  projectName,\n                                                                  entries,\n                                                                  isDigest = false,\n                                                                  unsubscribeUrl,\n                                                                  recipientName,\n                                                                  recipientEmail,\n                                                                  changelogUrl,\n                                                                  customDomain\n                                                              }) => {\n    const title = isDigest\n        ? `${projectName} - Latest Changelog Updates`\n        : `${projectName} - ${entries[0]?.title || 'Changelog Update'}`;\n\n    // Create personalized greeting\n    const getPersonalizedGreeting = () => {\n        if (recipientName) {\n            return `Hello ${recipientName},`;\n        } else if (recipientEmail) {\n            // Extract name from email as fallback (e.g., john.doe@example.com -> John)\n            const possibleName = recipientEmail.split('@')[0].split('.')[0];\n            const capitalizedName = possibleName.charAt(0).toUpperCase() + possibleName.slice(1);\n            return `Hello ${capitalizedName},`;\n        }\n        return 'Hello,';\n    };\n\n    // Use custom domain for branding if available\n    const brandDomain = customDomain || changelogUrl\n\n    return (\n        <Html>\n            <Head>\n                <title>{title}</title>\n            </Head>\n            <Body style={{\n                fontFamily: '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif',\n                backgroundColor: '#f6f9fc',\n                margin: '0 auto',\n                padding: '20px 0'\n            }}>\n                <Container style={{\n                    backgroundColor: '#ffffff',\n                    borderRadius: '8px',\n                    boxShadow: '0 2px 8px rgba(0, 0, 0, 0.05)',\n                    maxWidth: '600px',\n                    margin: '0 auto',\n                    padding: '20px'\n                }}>\n                    <Section>\n                        <Heading as=\"h1\" style={{\n                            color: '#333',\n                            fontSize: '24px',\n                            fontWeight: 'bold',\n                            margin: '10px 0 20px',\n                            textAlign: 'center'\n                        }}>\n                            {projectName} Changelog\n                        </Heading>\n\n                        {/* Personalized greeting */}\n                        <Text style={{\n                            color: '#333',\n                            fontSize: '16px',\n                            margin: '0 0 15px'\n                        }}>\n                            {getPersonalizedGreeting()}\n                        </Text>\n\n                        <Text style={{\n                            color: '#666',\n                            fontSize: '16px',\n                            margin: '0 0 20px'\n                        }}>\n                            {isDigest\n                                ? `Here are the latest updates to ${projectName} that we wanted to share with you.`\n                                : `We've just published a new update to ${projectName} that we wanted to share with you.`\n                            }\n                        </Text>\n                        <Hr style={{ margin: '20px 0' }} />\n                    </Section>\n\n                    {entries.map((entry, index) => (\n                        <Section key={entry.id} style={{\n                            padding: '10px 0',\n                            borderBottom: index < entries.length - 1 ? '1px solid #eaeaea' : 'none',\n                            marginBottom: '20px'\n                        }}>\n                            <Row>\n                                <Column>\n                                    <Heading as=\"h2\" style={{\n                                        fontSize: '18px',\n                                        fontWeight: 'bold',\n                                        margin: '10px 0'\n                                    }}>\n                                        {entry.title}\n                                        {entry.version && (\n                                            <Text style={{\n                                                color: '#666',\n                                                fontSize: '14px',\n                                                fontWeight: 'normal',\n                                                display: 'inline',\n                                                marginLeft: '10px'\n                                            }}>\n                                                {entry.version}\n                                            </Text>\n                                        )}\n                                    </Heading>\n\n                                    {entry.tags && entry.tags.length > 0 && (\n                                        <Row style={{ marginBottom: '10px' }}>\n                                            {entry.tags.map(tag => (\n                                                <Text key={tag.id} style={{\n                                                    backgroundColor: '#f1f5f9',\n                                                    borderRadius: '4px',\n                                                    color: '#475569',\n                                                    display: 'inline-block',\n                                                    fontSize: '12px',\n                                                    fontWeight: 'normal',\n                                                    margin: '0 4px 4px 0',\n                                                    padding: '2px 6px'\n                                                }}>\n                                                    {tag.name}\n                                                </Text>\n                                            ))}\n                                        </Row>\n                                    )}\n\n                                    <div\n                                        style={{\n                                            color: '#333',\n                                            fontSize: '14px',\n                                            lineHeight: '24px',\n                                            margin: '10px 0',\n                                        }}\n                                        dangerouslySetInnerHTML={{\n                                            __html: sanitizeHtml(renderMarkdown(entry.content))\n                                        }}\n                                    />\n\n                                    {entry.publishedAt && (\n                                        <Text style={{\n                                            color: '#999',\n                                            fontSize: '12px',\n                                            margin: '10px 0'\n                                        }}>\n                                            Published: {new Date(entry.publishedAt).toLocaleDateString()}\n                                        </Text>\n                                    )}\n                                </Column>\n                            </Row>\n                        </Section>\n                    ))}\n\n                    <Section style={{ marginTop: '30px' }}>\n                        <Hr style={{ margin: '0 0 20px' }} />\n                        <Text style={{\n                            color: '#999',\n                            fontSize: '12px',\n                            textAlign: 'center'\n                        }}>\n                            Thank you for your continued interest in {projectName}.\n                        </Text>\n                        <Text style={{\n                            color: '#999',\n                            fontSize: '12px',\n                            textAlign: 'center'\n                        }}>\n                            You received this email because you&apos;re subscribed to changelog updates for {projectName} | {brandDomain}.\n                            {unsubscribeUrl && (\n                                <>\n                                    <br />\n                                    <Link\n                                        href={unsubscribeUrl}\n                                        style={{\n                                            color: '#666',\n                                            textDecoration: 'underline',\n                                        }}\n                                    >\n                                        Unsubscribe from these notifications\n                                    </Link>\n                                </>\n                            )}\n                        </Text>\n                    </Section>\n                </Container>\n            </Body>\n        </Html>\n    );\n};\n\nexport default ChangelogEmail;"
  },
  {
    "path": "emails/password-reset.tsx",
    "content": "import * as React from 'react';\nimport {\n    Html,\n    Head,\n    Body,\n    Container,\n    Section,\n    Row,\n    Column,\n    Heading,\n    Text,\n    Hr,\n    Button\n} from '@react-email/components';\nimport {MailWarning} from \"lucide-react\";\n\ninterface PasswordResetEmailProps {\n    resetLink: string;\n    recipientName?: string;\n    recipientEmail: string;\n    expiresInMinutes: number;\n}\n\nexport const PasswordResetEmail: React.FC<PasswordResetEmailProps> = ({\n                                                                          resetLink,\n                                                                          recipientName,\n                                                                          recipientEmail,\n                                                                          expiresInMinutes = 60\n                                                                      }) => {\n    // Create personalized greeting for user\n    const getPersonalizedGreeting = () => {\n        if (recipientName) {\n            return `Hello ${recipientName},`;\n        } else {\n            // Extract name from email as fallback (e.g., john.doe@example.com -> John)\n            const possibleName = recipientEmail.split('@')[0].split('.')[0];\n            const capitalizedName = possibleName.charAt(0).toUpperCase() + possibleName.slice(1);\n            return `Hello ${capitalizedName},`;\n        }\n    };\n\n    // Branding Styles\n    const colors = {\n        primary: '#0891b2',\n        success: '#16a34a',\n        text: '#334155',\n        mutedText: '#64748b',\n        background: '#f8fafc',\n        card: '#ffffff',\n        border: '#e2e8f0',\n        buttonText: '#ffffff'\n    };\n\n    return (\n        <Html>\n            <Head>\n                <title>Reset Your Password</title>\n            </Head>\n            <Body style={{\n                fontFamily: '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif',\n                backgroundColor: colors.background,\n                margin: '0 auto',\n                padding: '40px 20px'\n            }}>\n                <Container style={{\n                    maxWidth: '600px',\n                    margin: '0 auto',\n                }}>\n                    {/* Header with logo/brand */}\n                    <Section style={{ textAlign: 'center', marginBottom: '20px' }}>\n                        <Heading as=\"h1\" style={{\n                            color: colors.primary,\n                            fontSize: '28px',\n                            fontWeight: 'bold',\n                            margin: '0'\n                        }}>\n                            Changerawr\n                        </Heading>\n                    </Section>\n\n                    {/* Main card */}\n                    <Section style={{\n                        backgroundColor: colors.card,\n                        borderRadius: '8px',\n                        boxShadow: '0 4px 6px rgba(0, 0, 0, 0.05)',\n                        overflow: 'hidden',\n                        border: `1px solid ${colors.border}`,\n                        borderTop: `4px solid ${colors.primary}`\n                    }}>\n                        {/* Card top with icon */}\n                        <Section style={{\n                            textAlign: 'center',\n                            padding: '30px 40px 0'\n                        }}>\n                            <Row>\n                                <Column>\n                                    {/* Circle with envelope icon */}\n                                    <div style={{\n                                        width: '80px',\n                                        height: '80px',\n                                        margin: '0 auto 20px',\n                                        backgroundColor: '#e0f2fe', // Light blue bg\n                                        borderRadius: '50%',\n                                        display: 'flex',\n                                        alignItems: 'center',\n                                        justifyContent: 'center'\n                                    }}>\n                                        {/* Email icon */}\n                                        <MailWarning height={40} width={40} />\n                                    </div>\n\n                                    <Heading as=\"h2\" style={{\n                                        color: colors.text,\n                                        fontSize: '24px',\n                                        fontWeight: 'bold',\n                                        margin: '0 0 15px',\n                                        textAlign: 'center'\n                                    }}>\n                                        Reset Your Password\n                                    </Heading>\n                                </Column>\n                            </Row>\n                        </Section>\n\n                        {/* Card content */}\n                        <Section style={{ padding: '0 40px 30px' }}>\n                            {/* Personalized greeting */}\n                            <Text style={{\n                                color: colors.text,\n                                fontSize: '16px',\n                                lineHeight: '24px',\n                                margin: '0 0 15px'\n                            }}>\n                                {getPersonalizedGreeting()}\n                            </Text>\n\n                            <Text style={{\n                                color: colors.text,\n                                fontSize: '16px',\n                                lineHeight: '24px',\n                                margin: '0 0 20px'\n                            }}>\n                                We received a request to reset your password. Use the button below to set a new password. This link is only valid for the next {expiresInMinutes} minutes.\n                            </Text>\n\n                            {/* Button in a separate section */}\n                            <Section style={{ textAlign: 'center', margin: '30px 0' }}>\n                                <Button\n                                    href={resetLink}\n                                    style={{\n                                        backgroundColor: colors.primary,\n                                        borderRadius: '6px',\n                                        color: colors.buttonText,\n                                        fontSize: '16px',\n                                        fontWeight: 'bold',\n                                        textDecoration: 'none',\n                                        textAlign: 'center',\n                                        display: 'inline-block',\n                                        padding: '12px 30px',\n                                        boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)'\n                                    }}\n                                >\n                                    Reset Password\n                                </Button>\n                            </Section>\n\n                            <Text style={{\n                                color: colors.mutedText,\n                                fontSize: '14px',\n                                margin: '30px 0 10px'\n                            }}>\n                                If the button doesn&apos;t work, copy and paste this link into your browser:\n                            </Text>\n\n                            <Text style={{\n                                color: colors.primary,\n                                fontSize: '14px',\n                                margin: '0 0 30px',\n                                wordBreak: 'break-all'\n                            }}>\n                                {resetLink}\n                            </Text>\n\n                            <Hr style={{\n                                borderColor: colors.border,\n                                borderWidth: '1px',\n                                margin: '25px 0'\n                            }} />\n\n                            <Text style={{\n                                color: colors.mutedText,\n                                fontSize: '14px',\n                                margin: '0'\n                            }}>\n                                If you didn&apos;t request a password reset, you can safely ignore this email. Someone may have entered your email address by accident.\n                            </Text>\n                        </Section>\n                    </Section>\n\n                    {/* Footer */}\n                    <Section style={{\n                        textAlign: 'center',\n                        margin: '20px 0 0',\n                        color: colors.mutedText,\n                        fontSize: '12px'\n                    }}>\n                        <Text>\n                            © {new Date().getFullYear()} Changerawr. All rights reserved.\n                        </Text>\n                        <Text style={{ margin: '10px 0' }}>\n                            This email was sent to {recipientEmail}\n                        </Text>\n                    </Section>\n                </Container>\n            </Body>\n        </Html>\n    );\n};\n\nexport default PasswordResetEmail;"
  },
  {
    "path": "emails/rejection-notification.tsx",
    "content": "import * as React from 'react';\nimport {\n    Html,\n    Head,\n    Body,\n    Container,\n    Section,\n    Heading,\n    Text,\n    Hr,\n    Button,\n} from '@react-email/components';\n\ninterface RejectionNotificationEmailProps {\n    recipientName?: string;\n    projectName: string;\n    requestType: string;\n    entryTitle?: string;\n    adminName?: string;\n    dashboardUrl: string;\n}\n\nexport const RejectionNotificationEmail: React.FC<RejectionNotificationEmailProps> = ({\n                                                                                          recipientName,\n                                                                                          projectName,\n                                                                                          requestType,\n                                                                                          entryTitle,\n                                                                                          adminName = 'An administrator',\n                                                                                          dashboardUrl,\n                                                                                      }) => {\n    const displayRequestType = requestType.replace(/_/g, ' ').toLowerCase();\n\n    return (\n        <Html>\n            <Head>\n                <title>Your Request Has Been Rejected</title>\n            </Head>\n            <Body style={{\n                fontFamily: '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif',\n                backgroundColor: '#f6f9fc',\n                margin: '0 auto',\n                padding: '20px 0'\n            }}>\n                <Container style={{\n                    backgroundColor: '#ffffff',\n                    borderRadius: '8px',\n                    boxShadow: '0 2px 8px rgba(0, 0, 0, 0.05)',\n                    maxWidth: '600px',\n                    margin: '0 auto',\n                    padding: '20px'\n                }}>\n                    <Section>\n                        <Heading as=\"h1\" style={{\n                            color: '#333',\n                            fontSize: '24px',\n                            fontWeight: 'bold',\n                            margin: '10px 0 20px',\n                            textAlign: 'center'\n                        }}>\n                            Request Not Approved\n                        </Heading>\n\n                        <Text style={{\n                            color: '#333',\n                            fontSize: '16px',\n                            margin: '0 0 15px'\n                        }}>\n                            Hello {recipientName || 'there'},\n                        </Text>\n\n                        <Text style={{\n                            color: '#333',\n                            fontSize: '16px',\n                            margin: '0 0 20px'\n                        }}>\n                            Your request to {displayRequestType} {entryTitle\n                            ? <>the entry <strong>&ldquo;{entryTitle}&ldquo;</strong> in</>\n                            : <>for</>} <strong>{projectName}</strong> has not been approved by {adminName}.\n                        </Text>\n\n                        <Text style={{\n                            color: '#333',\n                            fontSize: '16px',\n                            margin: '0 0 20px'\n                        }}>\n                            If you have any questions about this decision, please reach out to {adminName} for further details.\n                        </Text>\n\n                        <Section style={{ textAlign: 'center', margin: '30px 0' }}>\n                            <Button\n                                href={dashboardUrl}\n                                style={{\n                                    backgroundColor: '#0891b2',\n                                    borderRadius: '4px',\n                                    color: '#fff',\n                                    fontSize: '16px',\n                                    textDecoration: 'none',\n                                    textAlign: 'center',\n                                    display: 'inline-block',\n                                    padding: '12px 20px'\n                                }}\n                            >\n                                View Dashboard\n                            </Button>\n                        </Section>\n\n                        <Hr style={{ margin: '30px 0 20px' }} />\n\n                        <Text style={{\n                            color: '#999',\n                            fontSize: '12px',\n                            textAlign: 'center'\n                        }}>\n                            This is an automated notification from Changerawr. You received this email because you have notifications enabled in your account settings.\n                        </Text>\n                    </Section>\n                </Container>\n            </Body>\n        </Html>\n    );\n};\n\nexport default RejectionNotificationEmail;"
  },
  {
    "path": "emails/schedule-published.tsx",
    "content": "import * as React from 'react';\nimport {\n    Html,\n    Head,\n    Body,\n    Container,\n    Section,\n    Row,\n    Column,\n    Heading,\n    Text,\n    Hr,\n    Button,\n    Preview,\n    Tailwind\n} from '@react-email/components';\n\ninterface SchedulePublishedEmailProps {\n    recipientName?: string;\n    projectName: string;\n    entryTitle: string;\n    entryVersion?: string;\n    publishedAt: Date;\n    viewEntryUrl?: string;\n    timezone?: string;\n}\n\nexport const SchedulePublishedEmail: React.FC<SchedulePublishedEmailProps> = ({\n                                                                                  recipientName,\n                                                                                  projectName,\n                                                                                  entryTitle,\n                                                                                  entryVersion,\n                                                                                  publishedAt,\n                                                                                  viewEntryUrl,\n                                                                                  timezone = 'UTC',\n                                                                              }) => {\n    const formattedDate = publishedAt.toLocaleDateString('en-US', {\n        weekday: 'long',\n        year: 'numeric',\n        month: 'long',\n        day: 'numeric',\n        hour: '2-digit',\n        minute: '2-digit',\n        timeZone: timezone,\n        timeZoneName: 'short'\n    });\n\n    const getPersonalizedGreeting = (): string => {\n        if (recipientName) {\n            return `Hello ${recipientName},`;\n        }\n        return 'Hello,';\n    };\n\n    return (\n        <Html>\n            <Head/>\n            <Preview>\n                🎉 &ldquo;{entryTitle}&rdquo; is now live! Your scheduled changelog entry has been published\n                successfully.\n            </Preview>\n            <Tailwind>\n                <Body className=\"bg-slate-50 font-sans py-5\">\n                    <Container className=\"bg-white mx-auto p-0 max-w-2xl rounded-xl shadow-lg overflow-hidden\">\n                        {/* Header with Brand */}\n                        <Section className=\"bg-gradient-to-br from-emerald-600 to-emerald-700 px-6 py-8 text-center\">\n                            <Row>\n                                <Column>\n                                    <Text\n                                        className=\"text-white text-sm font-semibold tracking-wide m-0 mb-2 opacity-90\">\n                                        Changerawr\n                                    </Text>\n                                    <Heading className=\"text-white text-3xl font-bold m-0 text-center leading-tight\">\n                                        <span className=\"text-3xl mr-2\">🎉</span> Your Entry is Live!\n                                    </Heading>\n                                </Column>\n                            </Row>\n                        </Section>\n\n                        {/* Main Content */}\n                        <Section className=\"px-6 py-8\">\n                            <Text className=\"text-gray-800 text-lg font-semibold leading-6 m-0 mb-5\">\n                                {getPersonalizedGreeting()}\n                            </Text>\n\n                            <Text className=\"text-gray-600 text-base leading-7 my-4\">\n                                Great news! Your scheduled changelog\n                                entry <strong>&ldquo;{entryTitle}&rdquo;</strong> has\n                                been automatically published and is now live for your audience.\n                            </Text>\n\n                            {/* Enhanced Entry Details Card */}\n                            <Section className=\"bg-white border-2 border-gray-200 rounded-xl p-6 my-6 shadow-sm\">\n                                <Row>\n                                    <Column>\n                                        <div className=\"border-b border-gray-200 pb-3 mb-4\">\n                                            <Text className=\"text-gray-700 text-base font-medium m-0\">\n                                                📝 Entry Details\n                                            </Text>\n                                        </div>\n                                    </Column>\n                                </Row>\n\n                                <Row className=\"mb-3\">\n                                    <Column className=\"w-[30%] align-top pr-3\">\n                                        <Text className=\"text-gray-500 text-sm font-medium leading-5 m-0\">\n                                            Title\n                                        </Text>\n                                    </Column>\n                                    <Column className=\"w-[70%]\">\n                                        <Text className=\"text-gray-800 text-[15px] font-semibold leading-5 m-0\">\n                                            {entryTitle}\n                                        </Text>\n                                    </Column>\n                                </Row>\n\n                                <Row className=\"mb-3\">\n                                    <Column className=\"w-[30%] align-top pr-3\">\n                                        <Text className=\"text-gray-500 text-sm font-medium leading-5 m-0\">\n                                            Project\n                                        </Text>\n                                    </Column>\n                                    <Column className=\"w-[70%]\">\n                                        <Text className=\"text-gray-800 text-[15px] font-semibold leading-5 m-0\">\n                                            {projectName}\n                                        </Text>\n                                    </Column>\n                                </Row>\n\n                                <Row className=\"mb-3\">\n                                    <Column className=\"w-[30%] align-top pr-3\">\n                                        <Text className=\"text-gray-500 text-sm font-medium leading-5 m-0\">\n                                            Published\n                                        </Text>\n                                    </Column>\n                                    <Column className=\"w-[70%]\">\n                                        <Text className=\"text-gray-800 text-[15px] font-semibold leading-5 m-0\">\n                                            {formattedDate}\n                                        </Text>\n                                    </Column>\n                                </Row>\n\n                                {entryVersion && (\n                                    <Row className=\"mb-3\">\n                                        <Column className=\"w-[30%] align-top pr-3\">\n                                            <Text className=\"text-gray-500 text-sm font-medium leading-5 m-0\">\n                                                Version\n                                            </Text>\n                                        </Column>\n                                        <Column className=\"w-[70%]\">\n                                            <Text className=\"text-gray-800 text-[15px] font-semibold leading-5 m-0\">\n                                                {entryVersion}\n                                            </Text>\n                                        </Column>\n                                    </Row>\n                                )}\n\n                                {/* Success Badge */}\n                                <Section\n                                    className=\"bg-green-50 border border-emerald-500 rounded-md px-3 py-2 mt-4 text-center\">\n                                    <Text className=\"text-emerald-700 text-sm font-semibold m-0\">\n                                        ✅ Successfully Published\n                                    </Text>\n                                </Section>\n                            </Section>\n\n                            <Text className=\"text-gray-600 text-base leading-7 my-4\">\n                                Your changelog entry is now visible to your audience.\n                            </Text>\n\n                            {/* Enhanced Action Buttons */}\n                            <Section className=\"my-8\">\n                                {viewEntryUrl && (\n                                    <Row className=\"mb-3\">\n                                        <Column>\n                                            <Button\n                                                className=\"bg-emerald-600 rounded-lg text-white text-base font-semibold no-underline text-center block w-full py-3.5 px-6 border-0 shadow-sm hover:bg-emerald-700\"\n                                                href={viewEntryUrl}\n                                            >\n                                                🔗 View Live Entry\n                                            </Button>\n                                        </Column>\n                                    </Row>\n                                )}\n                            </Section>\n                        </Section>\n\n                        <Hr className=\"border-gray-200 m-0\"/>\n\n                        {/* Enhanced Footer */}\n                        <Section className=\"bg-gray-50 p-6 text-center\">\n                            <Text className=\"text-gray-500 text-sm leading-5 my-2\">\n                                <strong>Why did I receive this?</strong><br/>\n                                This notification was sent because you scheduled a changelog entry for automatic\n                                publishing.\n                            </Text>\n\n                            <Text className=\"text-gray-400 text-xs mt-4 mb-0\">\n                                Made with ❤️ by <strong>Changerawr</strong>\n                            </Text>\n                        </Section>\n                    </Container>\n                </Body>\n            </Tailwind>\n        </Html>\n    );\n};\n\nexport default SchedulePublishedEmail;"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import { dirname } from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { FlatCompat } from \"@eslint/eslintrc\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst compat = new FlatCompat({\n  baseDirectory: __dirname,\n});\n\nconst eslintConfig = [\n  ...compat.extends(\"next/core-web-vitals\", \"next/typescript\"),\n];\n\nexport default eslintConfig;\n"
  },
  {
    "path": "hooks/use-local-storage.ts",
    "content": "'use client'\n\nimport { useState, useEffect } from 'react'\n\ntype SetValue<T> = (value: T | ((prevValue: T) => T)) => void\n\n/**\n * A hook that allows for persisting state in localStorage.\n * @param key The key to store the value under in localStorage\n * @param initialValue The initial value to use if no value is found in localStorage\n * @returns A stateful value and a function to update it, like useState\n */\nexport function useLocalStorage<T>(\n  key: string,\n  initialValue: T\n): [T, SetValue<T>] {\n  // Get from local storage then parse stored json or return initialValue\n  const readValue = (): T => {\n    // SSR check\n    if (typeof window === 'undefined') {\n      return initialValue\n    }\n\n    try {\n      const item = window.localStorage.getItem(key)\n      return item ? (JSON.parse(item) as T) : initialValue\n    } catch (error) {\n      console.warn(`Error reading localStorage key \"${key}\":`, error)\n      return initialValue\n    }\n  }\n\n  // State to store our value\n  const [storedValue, setStoredValue] = useState<T>(readValue)\n\n  // Return a wrapped version of useState's setter function that persists the new value to localStorage\n  const setValue: SetValue<T> = (value) => {\n    try {\n      // Allow value to be a function so we have the same API as useState\n      const valueToStore = value instanceof Function ? value(storedValue) : value\n\n      // Save state\n      setStoredValue(valueToStore)\n\n      // Save to local storage\n      if (typeof window !== 'undefined') {\n        window.localStorage.setItem(key, JSON.stringify(valueToStore))\n\n        // Dispatch a custom event so other instances can update\n        window.dispatchEvent(new Event('local-storage-change'))\n      }\n    } catch (error) {\n      console.warn(`Error setting localStorage key \"${key}\":`, error)\n    }\n  }\n\n  // Listen for changes to this localStorage value from other instances\n  useEffect(() => {\n    const handleStorageChange = () => {\n      setStoredValue(readValue())\n    }\n\n    // this only works for other documents, not the current one\n    window.addEventListener('storage', handleStorageChange)\n    // this is a custom event, triggered in setValue\n    window.addEventListener('local-storage-change', handleStorageChange)\n\n    return () => {\n      window.removeEventListener('storage', handleStorageChange)\n      window.removeEventListener('local-storage-change', handleStorageChange)\n    }\n  }, [])\n\n  return [storedValue, setValue]\n}\n\nexport default useLocalStorage"
  },
  {
    "path": "hooks/use-media-query.ts",
    "content": "'use client'\n\nimport { useState, useEffect } from 'react'\n\n/**\n * Custom hook for handling media queries\n * @param query - CSS media query string\n * @returns boolean indicating if the media query matches\n *\n * @example\n * ```tsx\n * const isMobile = useMediaQuery('(max-width: 768px)')\n * const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)')\n * const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)')\n * ```\n */\nexport function useMediaQuery(query: string): boolean {\n    // Initialize with null and update on mount to avoid hydration mismatch\n    const [matches, setMatches] = useState<boolean | null>(null)\n\n    useEffect(() => {\n        // Create media query list\n        const mediaQuery = window.matchMedia(query)\n\n        // Set initial value\n        setMatches(mediaQuery.matches)\n\n        // Create event listener\n        const handleChange = (event: MediaQueryListEvent) => {\n            setMatches(event.matches)\n        }\n\n        // Add listener\n        mediaQuery.addEventListener('change', handleChange)\n\n        // Cleanup\n        return () => {\n            mediaQuery.removeEventListener('change', handleChange)\n        }\n    }, [query]) // Only re-run effect if query changes\n\n    // Return false during SSR to avoid hydration mismatch\n    return matches ?? false\n}\n\n// Predefined breakpoint queries\nexport const breakpoints = {\n    sm: '(min-width: 640px)',\n    md: '(min-width: 768px)',\n    lg: '(min-width: 1024px)',\n    xl: '(min-width: 1280px)',\n    '2xl': '(min-width: 1536px)',\n    // Mobile first approach\n    mobile: '(max-width: 767px)',\n    tablet: '(min-width: 768px) and (max-width: 1023px)',\n    desktop: '(min-width: 1024px)',\n    // Feature queries\n    dark: '(prefers-color-scheme: dark)',\n    light: '(prefers-color-scheme: light)',\n    reducedMotion: '(prefers-reduced-motion: reduce)',\n    highContrast: '(prefers-contrast: high)',\n    portrait: '(orientation: portrait)',\n    landscape: '(orientation: landscape)',\n} as const\n\n/**\n * Hook creator for predefined breakpoints\n * @param breakpoint - Key of predefined breakpoint\n * @returns boolean indicating if the breakpoint matches\n *\n * @example\n * ```tsx\n * const isMobile = useBreakpoint('mobile')\n * const isTablet = useBreakpoint('tablet')\n * const prefersReducedMotion = useBreakpoint('reducedMotion')\n * ```\n */\nexport function useBreakpoint(breakpoint: keyof typeof breakpoints): boolean {\n    return useMediaQuery(breakpoints[breakpoint])\n}"
  },
  {
    "path": "hooks/use-timezone.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\n\ninterface TimezoneResponse {\n    timezone: string\n    source: 'user' | 'system'\n    allowUserTimezone: boolean\n}\n\n/**\n * Returns the effective timezone for the current user.\n * Resolves user override → system global → 'UTC' fallback.\n *\n * Uses a 5-minute stale time so the value is cached across components.\n */\nexport function useTimezone(): string {\n    const { data } = useQuery<TimezoneResponse>({\n        queryKey: ['system-timezone'],\n        queryFn: async () => {\n            const res = await fetch('/api/config/timezone')\n            if (!res.ok) return { timezone: 'UTC', source: 'system' as const, allowUserTimezone: true }\n            return res.json()\n        },\n        staleTime: 300000, // 5 minutes\n    })\n\n    return data?.timezone ?? 'UTC'\n}\n"
  },
  {
    "path": "hooks/use-toast.ts",
    "content": "\"use client\"\n\n// Inspired by react-hot-toast library\nimport * as React from \"react\"\n\nimport type {\n  ToastActionElement,\n  ToastProps,\n} from \"@/components/ui/toast\"\n\nconst TOAST_LIMIT = 1\nconst TOAST_REMOVE_DELAY = 1000000\n\ntype ToasterToast = ToastProps & {\n  id: string\n  title?: React.ReactNode\n  description?: React.ReactNode\n  action?: ToastActionElement\n}\n\nconst actionTypes = {\n  ADD_TOAST: \"ADD_TOAST\",\n  UPDATE_TOAST: \"UPDATE_TOAST\",\n  DISMISS_TOAST: \"DISMISS_TOAST\",\n  REMOVE_TOAST: \"REMOVE_TOAST\",\n} as const\n\nlet count = 0\n\nfunction genId() {\n  count = (count + 1) % Number.MAX_SAFE_INTEGER\n  return count.toString()\n}\n\ntype ActionType = typeof actionTypes\n\ntype Action =\n  | {\n      type: ActionType[\"ADD_TOAST\"]\n      toast: ToasterToast\n    }\n  | {\n      type: ActionType[\"UPDATE_TOAST\"]\n      toast: Partial<ToasterToast>\n    }\n  | {\n      type: ActionType[\"DISMISS_TOAST\"]\n      toastId?: ToasterToast[\"id\"]\n    }\n  | {\n      type: ActionType[\"REMOVE_TOAST\"]\n      toastId?: ToasterToast[\"id\"]\n    }\n\ninterface State {\n  toasts: ToasterToast[]\n}\n\nconst toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()\n\nconst addToRemoveQueue = (toastId: string) => {\n  if (toastTimeouts.has(toastId)) {\n    return\n  }\n\n  const timeout = setTimeout(() => {\n    toastTimeouts.delete(toastId)\n    dispatch({\n      type: \"REMOVE_TOAST\",\n      toastId: toastId,\n    })\n  }, TOAST_REMOVE_DELAY)\n\n  toastTimeouts.set(toastId, timeout)\n}\n\nexport const reducer = (state: State, action: Action): State => {\n  switch (action.type) {\n    case \"ADD_TOAST\":\n      return {\n        ...state,\n        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),\n      }\n\n    case \"UPDATE_TOAST\":\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === action.toast.id ? { ...t, ...action.toast } : t\n        ),\n      }\n\n    case \"DISMISS_TOAST\": {\n      const { toastId } = action\n\n      // ! Side effects ! - This could be extracted into a dismissToast() action,\n      // but I'll keep it here for simplicity\n      if (toastId) {\n        addToRemoveQueue(toastId)\n      } else {\n        state.toasts.forEach((toast) => {\n          addToRemoveQueue(toast.id)\n        })\n      }\n\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === toastId || toastId === undefined\n            ? {\n                ...t,\n                open: false,\n              }\n            : t\n        ),\n      }\n    }\n    case \"REMOVE_TOAST\":\n      if (action.toastId === undefined) {\n        return {\n          ...state,\n          toasts: [],\n        }\n      }\n      return {\n        ...state,\n        toasts: state.toasts.filter((t) => t.id !== action.toastId),\n      }\n  }\n}\n\nconst listeners: Array<(state: State) => void> = []\n\nlet memoryState: State = { toasts: [] }\n\nfunction dispatch(action: Action) {\n  memoryState = reducer(memoryState, action)\n  listeners.forEach((listener) => {\n    listener(memoryState)\n  })\n}\n\ntype Toast = Omit<ToasterToast, \"id\">\n\nfunction toast({ ...props }: Toast) {\n  const id = genId()\n\n  const update = (props: ToasterToast) =>\n    dispatch({\n      type: \"UPDATE_TOAST\",\n      toast: { ...props, id },\n    })\n  const dismiss = () => dispatch({ type: \"DISMISS_TOAST\", toastId: id })\n\n  dispatch({\n    type: \"ADD_TOAST\",\n    toast: {\n      ...props,\n      id,\n      open: true,\n      onOpenChange: (open) => {\n        if (!open) dismiss()\n      },\n    },\n  })\n\n  return {\n    id: id,\n    dismiss,\n    update,\n  }\n}\n\nfunction useToast() {\n  const [state, setState] = React.useState<State>(memoryState)\n\n  React.useEffect(() => {\n    listeners.push(setState)\n    return () => {\n      const index = listeners.indexOf(setState)\n      if (index > -1) {\n        listeners.splice(index, 1)\n      }\n    }\n  }, [state])\n\n  return {\n    ...state,\n    toast,\n    dismiss: (toastId?: string) => dispatch({ type: \"DISMISS_TOAST\", toastId }),\n  }\n}\n\nexport { useToast, toast }\n"
  },
  {
    "path": "hooks/useAIAssistant.ts",
    "content": "import { useState, useCallback, useRef, useEffect } from 'react';\nimport {\n    AICompletionType,\n    AIEditorRequest,\n    AIEditorResult,\n    CompletionRequest,\n    CompletionResponse,\n    AIMessage\n} from '@/lib/utils/ai/types';\nimport { getMessagesForRequest } from '@/lib/utils/ai/prompts';\n\nexport interface AIAssistantState {\n    isVisible: boolean;\n    isLoading: boolean;\n    completionType: AICompletionType;\n    customPrompt: string;\n    temperature: number;\n    error: Error | null;\n    lastResult: AIEditorResult | null;\n    apiKeyValid: boolean | null;\n}\n\nexport interface UseAIAssistantOptions {\n    apiKey?: string;\n    model?: string;\n    baseUrl?: string;\n    onGenerated?: (result: AIEditorResult) => void;\n}\n\n/**\n * Custom hook for AI assistant functionality in the markdown editor\n */\nexport default function useAIAssistant({\n                                           apiKey,\n                                           model = 'copilot-zero',\n                                           baseUrl = 'https://api.secton.org/v1',\n                                           onGenerated\n                                       }: UseAIAssistantOptions = {}) {\n    // Panel state\n    const [state, setState] = useState<AIAssistantState>({\n        isVisible: false,\n        isLoading: false,\n        completionType: AICompletionType.COMPLETE,\n        customPrompt: '',\n        temperature: 0.7,\n        error: null,\n        lastResult: null,\n        apiKeyValid: apiKey ? true : null,\n    });\n\n    // API key ref\n    const apiKeyRef = useRef<string | undefined>(apiKey);\n\n    // Update key ref if provided in options\n    useEffect(() => {\n        if (apiKey) {\n            apiKeyRef.current = apiKey;\n            setState(prev => ({\n                ...prev,\n                apiKeyValid: true\n            }));\n        }\n    }, [apiKey]);\n\n    /**\n     * Open the AI assistant panel\n     */\n    const openAssistant = useCallback((type: AICompletionType = AICompletionType.COMPLETE) => {\n        setState(prev => ({\n            ...prev,\n            isVisible: true,\n            completionType: type,\n            // Reset error and result when opening\n            error: null,\n            lastResult: null\n        }));\n    }, []);\n\n    /**\n     * Close the AI assistant panel\n     */\n    const closeAssistant = useCallback(() => {\n        setState(prev => ({\n            ...prev,\n            isVisible: false\n        }));\n    }, []);\n\n    /**\n     * Set the completion type\n     */\n    const setCompletionType = useCallback((type: AICompletionType) => {\n        setState(prev => ({\n            ...prev,\n            completionType: type,\n            // Reset error and result when changing type\n            error: null,\n            lastResult: null\n        }));\n    }, []);\n\n    /**\n     * Set the custom prompt\n     */\n    const setCustomPrompt = useCallback((prompt: string) => {\n        setState(prev => ({\n            ...prev,\n            customPrompt: prompt\n        }));\n    }, []);\n\n    /**\n     * Set the temperature\n     */\n    const setTemperature = useCallback((temperature: number) => {\n        setState(prev => ({\n            ...prev,\n            temperature\n        }));\n    }, []);\n\n    /**\n     * Set the API key\n     */\n    const setApiKey = useCallback(async (key: string) => {\n        if (!key.trim()) {\n            setState(prev => ({\n                ...prev,\n                apiKeyValid: false,\n                error: new Error('API key cannot be empty')\n            }));\n            apiKeyRef.current = undefined;\n            return false;\n        }\n\n        try {\n            setState(prev => ({\n                ...prev,\n                isLoading: true,\n                error: null\n            }));\n\n            apiKeyRef.current = key;\n\n            // Test API key by making a simple request\n            const isValid = await validateApiKey(key);\n\n            setState(prev => ({\n                ...prev,\n                isLoading: false,\n                apiKeyValid: isValid,\n                error: isValid ? null : new Error('Invalid API key')\n            }));\n\n            return isValid;\n        } catch (error) {\n            setState(prev => ({\n                ...prev,\n                isLoading: false,\n                apiKeyValid: false,\n                error: error instanceof Error ? error : new Error('Failed to validate API key')\n            }));\n            return false;\n        }\n    }, []);\n\n    /**\n     * Validate the API key\n     */\n    const validateApiKey = async (key: string): Promise<boolean> => {\n        try {\n            // Make a small test request to validate the key\n            const response = await fetch(`${baseUrl}/chat/completions`, {\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json',\n                    'Authorization': `Bearer ${key}`\n                },\n                body: JSON.stringify({\n                    model,\n                    messages: [\n                        { role: 'system', content: 'You are a helpful assistant.' },\n                        { role: 'user', content: 'Test' }\n                    ],\n                    max_tokens: 1\n                })\n            });\n\n            return response.ok;\n        } catch (error) {\n            console.error('Error validating API key:', error);\n            return false;\n        }\n    };\n\n    /**\n     * Create a completion request\n     */\n    const createCompletion = useCallback(async (\n        messages: AIMessage[],\n        options: {\n            temperature?: number;\n            max_tokens?: number;\n        } = {}\n    ): Promise<CompletionResponse> => {\n        const key = apiKeyRef.current;\n\n        if (!key) {\n            throw new Error('API key not set');\n        }\n\n        const completionRequest: CompletionRequest = {\n            model,\n            messages,\n            temperature: options.temperature ?? state.temperature,\n            max_tokens: options.max_tokens ?? 1024\n        };\n\n        // Log outgoing request for debugging\n        // console.log('AI Request:', JSON.stringify(completionRequest, null, 2));\n\n        const response = await fetch(`${baseUrl}/chat/completions`, {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/json',\n                'Authorization': `Bearer ${key}`\n            },\n            body: JSON.stringify(completionRequest)\n        });\n\n        if (!response.ok) {\n            const errorData = await response.json().catch(() => null);\n            console.error('AI API Error:', errorData);\n            throw new Error(errorData?.error?.message || 'Failed to create completion');\n        }\n\n        // Parse the JSON response\n        const jsonResponse = await response.json();\n\n        // Log the response for debugging\n        // console.log('AI Response:', JSON.stringify(jsonResponse, null, 2));\n\n        return jsonResponse as CompletionResponse;\n    }, [baseUrl, model, state.temperature]);\n\n    /**\n     * Generate content with the AI assistant\n     */\n    const generateCompletion = useCallback(async (request: AIEditorRequest): Promise<AIEditorResult | null> => {\n        // Validate API key\n        if (!apiKeyRef.current) {\n            setState(prev => ({\n                ...prev,\n                error: new Error('API key not set'),\n                isLoading: false\n            }));\n            return null;\n        }\n\n        // Validate content\n        if (!request.content?.trim() && request.type !== AICompletionType.CUSTOM) {\n            setState(prev => ({\n                ...prev,\n                error: new Error('No content provided for processing'),\n                isLoading: false\n            }));\n            return null;\n        }\n\n        // Validate custom prompt if type is CUSTOM\n        if (request.type === AICompletionType.CUSTOM && !request.customPrompt?.trim() && !state.customPrompt?.trim()) {\n            setState(prev => ({\n                ...prev,\n                error: new Error('Please provide custom instructions'),\n                isLoading: false\n            }));\n            return null;\n        }\n\n        try {\n            setState(prev => ({\n                ...prev,\n                isLoading: true,\n                error: null\n            }));\n\n            // Prepare final request with proper custom prompt\n            const finalRequest: AIEditorRequest = {\n                ...request,\n                customPrompt: request.type === AICompletionType.CUSTOM\n                    ? (request.customPrompt || state.customPrompt)\n                    : request.customPrompt,\n                options: {\n                    ...request.options,\n                    temperature: request.options?.temperature || state.temperature\n                }\n            };\n\n            // Get formatted messages for the request\n            const messages = getMessagesForRequest(finalRequest);\n\n            // Create completion\n            const response = await createCompletion(messages, finalRequest.options);\n\n            // Extract the assistant's response\n            const assistantResponse = response.messages.find(m => m.role === 'assistant');\n            const generatedText = assistantResponse?.content || '';\n\n            // Prepare result\n            const result: AIEditorResult = {\n                text: generatedText,\n                originalRequest: finalRequest,\n                usage: response.usage,\n                metadata: {\n                    model: response.model,\n                    timestamp: Date.now()\n                }\n            };\n\n            // Update state\n            setState(prev => ({\n                ...prev,\n                isLoading: false,\n                lastResult: result\n            }));\n\n            // Trigger callback\n            onGenerated?.(result);\n\n            return result;\n        } catch (error) {\n            console.error('Error generating completion:', error);\n\n            setState(prev => ({\n                ...prev,\n                isLoading: false,\n                error: error instanceof Error\n                    ? error\n                    : new Error('Failed to generate completion')\n            }));\n\n            return null;\n        }\n    }, [createCompletion, onGenerated, state.customPrompt, state.temperature]);\n\n    /**\n     * Clear the last result\n     */\n    const clearLastResult = useCallback(() => {\n        setState(prev => ({\n            ...prev,\n            lastResult: null,\n            error: null\n        }));\n    }, []);\n\n    return {\n        state,\n        openAssistant,\n        closeAssistant,\n        setCompletionType,\n        setCustomPrompt,\n        setTemperature,\n        setApiKey,\n        generateCompletion,\n        clearLastResult\n    };\n}"
  },
  {
    "path": "hooks/useBookmarks.ts",
    "content": "// /lib/hooks/useBookmarks.ts\n\nimport { useState, useEffect, useCallback, useMemo } from 'react';\nimport { BookmarkService, type BookmarkedItem } from '@/lib/services/bookmarks/bookmark.service';\n\ninterface UseBookmarksOptions {\n    projectId?: string;\n    entryId?: string;\n}\n\ninterface UseBookmarksReturn {\n    // State\n    bookmarks: BookmarkedItem[];\n    isBookmarked: boolean;\n    isLoading: boolean;\n\n    // Actions\n    toggleBookmark: (entryId: string, title: string, projectId: string) => Promise<boolean>;\n    addBookmark: (entryId: string, title: string, projectId: string) => Promise<boolean>;\n    removeBookmark: (entryId: string, projectId: string) => Promise<boolean>;\n    updateBookmarkTitle: (entryId: string, newTitle: string, projectId: string) => Promise<boolean>;\n\n    // Utilities\n    getBookmarkCount: (projectId: string) => number;\n    searchBookmarks: (query: string, projectId?: string) => BookmarkedItem[];\n    refreshBookmarks: () => void;\n}\n\nexport function useBookmarks(options: UseBookmarksOptions = {}): UseBookmarksReturn {\n    const { projectId, entryId } = options;\n\n    const [bookmarks, setBookmarks] = useState<BookmarkedItem[]>([]);\n    const [isLoading, setIsLoading] = useState(true);\n\n    // Load bookmarks from localStorage\n    const loadBookmarks = useCallback(() => {\n        try {\n            setIsLoading(true);\n\n            if (projectId) {\n                const projectBookmarks = BookmarkService.getProjectBookmarks(projectId);\n                setBookmarks(projectBookmarks);\n            } else {\n                // Load all bookmarks if no specific project\n                const allBookmarks = BookmarkService.getAllBookmarks();\n                const flattenedBookmarks: BookmarkedItem[] = [];\n                Object.values(allBookmarks).forEach(projectBookmarks => {\n                    flattenedBookmarks.push(...projectBookmarks);\n                });\n                setBookmarks(flattenedBookmarks);\n            }\n        } catch (error) {\n            console.error('Failed to load bookmarks:', error);\n            setBookmarks([]);\n        } finally {\n            setIsLoading(false);\n        }\n    }, [projectId]);\n\n    // Load bookmarks on mount and when projectId changes\n    useEffect(() => {\n        loadBookmarks();\n    }, [loadBookmarks]);\n\n    // Listen for storage changes from other tabs/windows\n    useEffect(() => {\n        const handleStorageChange = (event: StorageEvent) => {\n            if (event.key?.startsWith('bookmarked-') || event.key === 'changerawr-global-bookmarks') {\n                loadBookmarks();\n            }\n        };\n\n        window.addEventListener('storage', handleStorageChange);\n        return () => window.removeEventListener('storage', handleStorageChange);\n    }, [loadBookmarks]);\n\n    // Check if current entry is bookmarked\n    const isBookmarked = useMemo(() => {\n        if (!entryId || !projectId) return false;\n        return BookmarkService.isBookmarked(entryId, projectId);\n    }, [entryId, projectId, bookmarks]);\n\n    // Bookmark actions\n    const toggleBookmark = useCallback(async (entryId: string, title: string, projectId: string): Promise<boolean> => {\n        try {\n            const success = BookmarkService.toggleBookmark(entryId, title, projectId);\n            if (success) {\n                loadBookmarks(); // Refresh local state\n            }\n            return success;\n        } catch (error) {\n            console.error('Failed to toggle bookmark:', error);\n            return false;\n        }\n    }, [loadBookmarks]);\n\n    const addBookmark = useCallback(async (entryId: string, title: string, projectId: string): Promise<boolean> => {\n        try {\n            const success = BookmarkService.addBookmark(entryId, title, projectId);\n            if (success) {\n                loadBookmarks(); // Refresh local state\n            }\n            return success;\n        } catch (error) {\n            console.error('Failed to add bookmark:', error);\n            return false;\n        }\n    }, [loadBookmarks]);\n\n    const removeBookmark = useCallback(async (entryId: string, projectId: string): Promise<boolean> => {\n        try {\n            const success = BookmarkService.removeBookmark(entryId, projectId);\n            if (success) {\n                loadBookmarks(); // Refresh local state\n            }\n            return success;\n        } catch (error) {\n            console.error('Failed to remove bookmark:', error);\n            return false;\n        }\n    }, [loadBookmarks]);\n\n    const updateBookmarkTitle = useCallback(async (entryId: string, newTitle: string, projectId: string): Promise<boolean> => {\n        try {\n            const success = BookmarkService.updateBookmarkTitle(entryId, newTitle, projectId);\n            if (success) {\n                loadBookmarks(); // Refresh local state\n            }\n            return success;\n        } catch (error) {\n            console.error('Failed to update bookmark title:', error);\n            return false;\n        }\n    }, [loadBookmarks]);\n\n    // Utility functions\n    const getBookmarkCount = useCallback((projectId: string): number => {\n        return BookmarkService.getProjectBookmarkCount(projectId);\n    }, []);\n\n    const searchBookmarks = useCallback((query: string, searchProjectId?: string): BookmarkedItem[] => {\n        return BookmarkService.searchBookmarks(query, searchProjectId);\n    }, []);\n\n    const refreshBookmarks = useCallback(() => {\n        loadBookmarks();\n    }, [loadBookmarks]);\n\n    return {\n        // State\n        bookmarks,\n        isBookmarked,\n        isLoading,\n\n        // Actions\n        toggleBookmark,\n        addBookmark,\n        removeBookmark,\n        updateBookmarkTitle,\n\n        // Utilities\n        getBookmarkCount,\n        searchBookmarks,\n        refreshBookmarks,\n    };\n}\n\n// Hook for global bookmark management\nexport function useGlobalBookmarks(): UseBookmarksReturn {\n    return useBookmarks(); // No projectId means all bookmarks\n}\n\n// Hook specifically for checking if an entry is bookmarked\nexport function useIsBookmarked(entryId: string, projectId: string): boolean {\n    const [isBookmarked, setIsBookmarked] = useState(false);\n\n    useEffect(() => {\n        if (!entryId || !projectId) {\n            setIsBookmarked(false);\n            return;\n        }\n\n        const checkBookmarkStatus = () => {\n            const bookmarked = BookmarkService.isBookmarked(entryId, projectId);\n            setIsBookmarked(bookmarked);\n        };\n\n        checkBookmarkStatus();\n\n        // Listen for storage changes\n        const handleStorageChange = (event: StorageEvent) => {\n            if (event.key?.startsWith('bookmarked-') || event.key === 'changerawr-global-bookmarks') {\n                checkBookmarkStatus();\n            }\n        };\n\n        window.addEventListener('storage', handleStorageChange);\n        return () => window.removeEventListener('storage', handleStorageChange);\n    }, [entryId, projectId]);\n\n    return isBookmarked;\n}"
  },
  {
    "path": "hooks/useChunkedData.ts",
    "content": "import { useState, useEffect, useCallback, useRef } from 'react'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\n\n// Generic type for the log data - ensure it includes an id\ninterface ChunkedDataParams<T extends { id: string | number }> {\n    fetchFn: (cursor: string, filters: unknown) => Promise<{\n        logs: T[];\n        total: number;\n        nextCursor: string | null;\n        hasMore?: boolean;\n        currentCount?: number;\n    }>;\n    queryKey: string;\n    filters: unknown;\n    chunkSize?: number;\n    initialCursor?: string;\n    maxRetries?: number;\n    retryDelay?: number;\n}\n\nexport const useChunkedData = <T extends { id: string | number }>({\n                                                                      fetchFn,\n                                                                      queryKey,\n                                                                      filters,\n                                                                      initialCursor = '',\n                                                                      maxRetries = 3,\n                                                                      retryDelay = 1000,\n                                                                  }: ChunkedDataParams<T>) => {\n    const [data, setData] = useState<T[]>([]);\n    const [isLoadingMore, setIsLoadingMore] = useState(false);\n    const [cursor, setCursor] = useState(initialCursor);\n    const [hasMore, setHasMore] = useState(true);\n    const [total, setTotal] = useState(0);\n    const [currentRetries, setCurrentRetries] = useState(0);\n    const [lastError, setLastError] = useState<string | null>(null);\n    const queryClient = useQueryClient();\n    const initialLoadComplete = useRef(false);\n    const isResetting = useRef(false);\n\n    // Keep a cache of loaded cursors to avoid duplicate data\n    const loadedCursors = useRef(new Set<string>());\n    const loadedIds = useRef(new Set<string | number>());\n\n    // Debug logging\n    const debugLog = useCallback((message: string, ...args: unknown[]) => {\n        console.log(`🦖 ChunkedData [${queryKey}]:`, message, ...args);\n    }, [queryKey]);\n\n    // Main query to fetch data\n    const { isLoading, isError, refetch, error } = useQuery({\n        queryKey: [queryKey, 'chunked', cursor, filters],\n        queryFn: async () => {\n            if (cursor && loadedCursors.current.has(cursor)) {\n                debugLog('Skipping already loaded cursor:', cursor);\n                return null; // Skip if this cursor was already loaded\n            }\n\n            debugLog('Fetching with cursor:', cursor || 'initial');\n\n            try {\n                const result = await fetchFn(cursor, filters);\n\n                debugLog('Fetch result:', {\n                    logsCount: result.logs.length,\n                    total: result.total,\n                    nextCursor: result.nextCursor,\n                    hasMore: result.hasMore\n                });\n\n                // Reset error state on successful fetch\n                setLastError(null);\n                setCurrentRetries(0);\n\n                // Mark this cursor as loaded\n                if (cursor) {\n                    loadedCursors.current.add(cursor);\n                }\n\n                setTotal(result.total);\n\n                // Append new data to existing data\n                setData(prevData => {\n                    // Ensure no duplicates by checking IDs\n                    const existingIds = new Set(prevData.map(item => item.id));\n                    const uniqueNewItems = result.logs.filter(item => {\n                        if (existingIds.has(item.id) || loadedIds.current.has(item.id)) {\n                            return false;\n                        }\n                        loadedIds.current.add(item.id);\n                        return true;\n                    });\n\n                    debugLog('Adding unique items:', uniqueNewItems.length, 'out of', result.logs.length);\n\n                    return [...prevData, ...uniqueNewItems];\n                });\n\n                // Update cursor and hasMore flag\n                const hasMoreData = result.hasMore !== undefined ? result.hasMore : !!result.nextCursor;\n\n                if (result.nextCursor && hasMoreData) {\n                    setCursor(result.nextCursor);\n                    setHasMore(true);\n                } else {\n                    setHasMore(false);\n                    debugLog('No more data available');\n                }\n\n                return result;\n            } catch (error) {\n                debugLog('Fetch error:', error);\n                setLastError(error instanceof Error ? error.message : 'Unknown error');\n\n                // Retry logic\n                if (currentRetries < maxRetries) {\n                    debugLog('Retrying fetch, attempt:', currentRetries + 1);\n                    setCurrentRetries(prev => prev + 1);\n\n                    // Wait before retrying\n                    await new Promise(resolve => setTimeout(resolve, retryDelay * (currentRetries + 1)));\n                    throw error; // Re-throw to trigger react-query retry\n                } else {\n                    debugLog('Max retries reached, giving up');\n                    throw error;\n                }\n            }\n        },\n        enabled: hasMore && !loadedCursors.current.has(cursor) && !isResetting.current,\n        refetchOnWindowFocus: false,\n        retry: false, // We handle retries manually\n        staleTime: 30000, // 30 seconds\n    });\n\n    // Load initial data\n    useEffect(() => {\n        if (!initialLoadComplete.current && !isResetting.current) {\n            debugLog('Loading initial data');\n            refetch();\n            initialLoadComplete.current = true;\n        }\n    }, [refetch]);\n\n    // Reset when filters change\n    useEffect(() => {\n        // eslint-disable-next-line @typescript-eslint/no-unused-vars\n        const filtersString = JSON.stringify(filters);\n        debugLog('Filters changed, resetting data');\n\n        isResetting.current = true;\n        setData([]);\n        setCursor('');\n        setHasMore(true);\n        setLastError(null);\n        setCurrentRetries(0);\n        loadedCursors.current.clear();\n        loadedIds.current.clear();\n        initialLoadComplete.current = false;\n\n        // Small delay to ensure state is updated\n        setTimeout(() => {\n            isResetting.current = false;\n            refetch();\n        }, 10);\n    }, [JSON.stringify(filters), refetch]);\n\n    // Function to load more data\n    const loadMore = useCallback(async () => {\n        if (!hasMore || isLoadingMore || isLoading || isResetting.current) {\n            debugLog('Skipping loadMore - conditions not met:', {\n                hasMore,\n                isLoadingMore,\n                isLoading,\n                isResetting: isResetting.current\n            });\n            return;\n        }\n\n        debugLog('Loading more data with cursor:', cursor);\n        setIsLoadingMore(true);\n\n        try {\n            await refetch();\n        } catch (error) {\n            debugLog('LoadMore error:', error);\n        } finally {\n            setIsLoadingMore(false);\n        }\n    }, [hasMore, isLoadingMore, isLoading, cursor, refetch]);\n\n    // Function to reset and reload data\n    const reset = useCallback(() => {\n        debugLog('Manual reset triggered');\n\n        isResetting.current = true;\n        setData([]);\n        setCursor('');\n        setHasMore(true);\n        setLastError(null);\n        setCurrentRetries(0);\n        loadedCursors.current.clear();\n        loadedIds.current.clear();\n        initialLoadComplete.current = false;\n\n        // Invalidate all related queries\n        queryClient.invalidateQueries({\n            queryKey: [queryKey],\n        });\n\n        setTimeout(() => {\n            isResetting.current = false;\n            refetch();\n        }, 10);\n    }, [queryKey, queryClient, refetch]);\n\n    // Enhanced progress calculation\n    const progress = total > 0 ? Math.min(100, (data.length / total) * 100) : 0;\n\n    // Debug state changes\n    useEffect(() => {\n        debugLog('State update:', {\n            dataLength: data.length,\n            total,\n            hasMore,\n            cursor,\n            progress: progress.toFixed(1) + '%',\n            isLoading,\n            isLoadingMore,\n            isError,\n            lastError\n        });\n    }, [data.length, total, hasMore, cursor, progress, isLoading, isLoadingMore, isError, lastError]);\n\n    return {\n        data,\n        isLoading: (isLoading && !initialLoadComplete.current) || isResetting.current,\n        isLoadingMore,\n        isError: isError || !!lastError,\n        hasMore,\n        loadMore,\n        reset,\n        total,\n        progress,\n        currentRetries,\n        lastError,\n        error: error || lastError,\n    };\n};"
  },
  {
    "path": "hooks/useCommandPalette.ts",
    "content": "// hooks/useCommandPalette.ts\n'use client';\n\nimport { useState, useEffect } from 'react';\n\nexport function useCommandPalette() {\n    const [isOpen, setIsOpen] = useState(false);\n\n    // Global keyboard shortcut\n    useEffect(() => {\n        const handleKeyDown = (e: KeyboardEvent) => {\n            // Cmd+K or Ctrl+K to open\n            if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {\n                e.preventDefault();\n                setIsOpen(true);\n            }\n        };\n\n        // Only listen when not focused on input elements\n        const handleGlobalKeyDown = (e: KeyboardEvent) => {\n            const target = e.target as HTMLElement;\n            const isInputFocused =\n                target.tagName === 'INPUT' ||\n                target.tagName === 'TEXTAREA' ||\n                target.contentEditable === 'true';\n\n            if (!isInputFocused) {\n                handleKeyDown(e);\n            }\n        };\n\n        window.addEventListener('keydown', handleGlobalKeyDown);\n        return () => window.removeEventListener('keydown', handleGlobalKeyDown);\n    }, []);\n\n    const open = () => setIsOpen(true);\n    const close = () => setIsOpen(false);\n    const toggle = () => setIsOpen(prev => !prev);\n\n    return {\n        isOpen,\n        open,\n        close,\n        toggle\n    };\n}"
  },
  {
    "path": "hooks/useEditorHistory.ts",
    "content": "import { useState, useCallback, useEffect, useRef } from 'react';\n\n/**\n * Options for the editor history hook\n */\nexport interface EditorHistoryOptions {\n    initialValue?: string;\n    maxHistoryLength?: number;\n    debounceTime?: number;\n}\n\n/**\n * Hook for managing editor history (undo/redo)\n */\nexport function useEditorHistory({\n                                     initialValue = '',\n                                     maxHistoryLength = 100,\n                                     debounceTime = 500,\n                                 }: EditorHistoryOptions = {}) {\n    // State for content and history\n    const [content, setContent] = useState(initialValue);\n    const [history, setHistory] = useState<string[]>([initialValue]);\n    const [historyIndex, setHistoryIndex] = useState(0);\n\n    // Debounce timer for adding history entries\n    const debounceTimerRef = useRef<number | null>(null);\n\n    // Track if the content change is from undo/redo\n    const isUndoRedoRef = useRef<boolean>(false);\n\n    /**\n     * Add a new history entry\n     */\n    const addHistoryEntry = useCallback((newContent: string) => {\n        // Don't add if nothing changed or if we're undoing/redoing\n        if (newContent === history[historyIndex] || isUndoRedoRef.current) {\n            isUndoRedoRef.current = false;\n            return;\n        }\n\n        // Clear any existing timer\n        if (debounceTimerRef.current) {\n            window.clearTimeout(debounceTimerRef.current);\n            debounceTimerRef.current = null;\n        }\n\n        // Create a new timer\n        debounceTimerRef.current = window.setTimeout(() => {\n            setHistory(prev => {\n                // If we're not at the newest entry, trim the history\n                const newHistory = prev.slice(0, historyIndex + 1);\n\n                // Add the new entry\n                newHistory.push(newContent);\n\n                // Trim history if it exceeds the max length\n                if (newHistory.length > maxHistoryLength) {\n                    return newHistory.slice(newHistory.length - maxHistoryLength);\n                }\n\n                return newHistory;\n            });\n\n            setHistoryIndex(prev => {\n                // If we've cut off history due to maxLength, adjust the index\n                const newIndex = prev + 1;\n                return Math.min(newIndex, maxHistoryLength - 1);\n            });\n        }, debounceTime);\n    }, [history, historyIndex, maxHistoryLength, debounceTime]);\n\n    /**\n     * Update editor content and track history\n     */\n    const updateContent = useCallback((newContent: string) => {\n        setContent(newContent);\n\n        // Skip history addition if coming from undo/redo\n        if (!isUndoRedoRef.current) {\n            addHistoryEntry(newContent);\n        }\n    }, [addHistoryEntry]);\n\n    /**\n     * Undo the last change\n     */\n    const undo = useCallback(() => {\n        if (historyIndex > 0) {\n            isUndoRedoRef.current = true;\n            const newIndex = historyIndex - 1;\n            setHistoryIndex(newIndex);\n            setContent(history[newIndex]);\n        }\n    }, [history, historyIndex]);\n\n    /**\n     * Redo the last undone change\n     */\n    const redo = useCallback(() => {\n        if (historyIndex < history.length - 1) {\n            isUndoRedoRef.current = true;\n            const newIndex = historyIndex + 1;\n            setHistoryIndex(newIndex);\n            setContent(history[newIndex]);\n        }\n    }, [history, historyIndex]);\n\n    /**\n     * Clear history and set new content\n     */\n    const resetContent = useCallback((newContent: string) => {\n        setContent(newContent);\n        setHistory([newContent]);\n        setHistoryIndex(0);\n    }, []);\n\n    // Clean up the debounce timer\n    useEffect(() => {\n        return () => {\n            if (debounceTimerRef.current) {\n                window.clearTimeout(debounceTimerRef.current);\n            }\n        };\n    }, []);\n\n    return {\n        content,\n        updateContent,\n        undo,\n        redo,\n        resetContent,\n        canUndo: historyIndex > 0,\n        canRedo: historyIndex < history.length - 1,\n        historyLength: history.length,\n        currentIndex: historyIndex,\n    };\n}\n\nexport default useEditorHistory;"
  },
  {
    "path": "hooks/useMarkdownState.ts",
    "content": "// components/markdown-editor/hooks/useMarkdownState.ts\n\nimport { useState, useCallback } from 'react';\n\nexport interface MarkdownState {\n    content: string;\n    history: string[];\n    historyIndex: number;\n    isDirty: boolean;\n}\n\nexport interface MarkdownActions {\n    setContent: (content: string) => void;\n    undo: () => void;\n    redo: () => void;\n    canUndo: () => boolean;\n    canRedo: () => boolean;\n    markClean: () => void;\n}\n\nexport interface UseMarkdownStateOptions {\n    initialContent?: string;\n    maxHistory?: number;\n    onChange?: (content: string) => void;\n}\n\nexport function useMarkdownState(options: UseMarkdownStateOptions = {}) {\n    const { initialContent = '', maxHistory = 50, onChange } = options;\n\n    const [state, setState] = useState<MarkdownState>({\n        content: initialContent,\n        history: [initialContent],\n        historyIndex: 0,\n        isDirty: false\n    });\n\n    const addToHistory = useCallback((content: string) => {\n        setState(prev => {\n            const newHistory = prev.history.slice(0, prev.historyIndex + 1);\n            newHistory.push(content);\n\n            // Limit history size\n            if (newHistory.length > maxHistory) {\n                newHistory.shift();\n            }\n\n            return {\n                ...prev,\n                content,\n                history: newHistory,\n                historyIndex: newHistory.length - 1,\n                isDirty: true\n            };\n        });\n        onChange?.(content);\n    }, [maxHistory, onChange]);\n\n    const setContent = useCallback((content: string) => {\n        addToHistory(content);\n    }, [addToHistory]);\n\n    const undo = useCallback(() => {\n        setState(prev => {\n            if (prev.historyIndex > 0) {\n                const newIndex = prev.historyIndex - 1;\n                const content = prev.history[newIndex];\n                onChange?.(content);\n                return {\n                    ...prev,\n                    content,\n                    historyIndex: newIndex,\n                    isDirty: true\n                };\n            }\n            return prev;\n        });\n    }, [onChange]);\n\n    const redo = useCallback(() => {\n        setState(prev => {\n            if (prev.historyIndex < prev.history.length - 1) {\n                const newIndex = prev.historyIndex + 1;\n                const content = prev.history[newIndex];\n                onChange?.(content);\n                return {\n                    ...prev,\n                    content,\n                    historyIndex: newIndex,\n                    isDirty: true\n                };\n            }\n            return prev;\n        });\n    }, [onChange]);\n\n    const canUndo = useCallback(() => state.historyIndex > 0, [state.historyIndex]);\n    const canRedo = useCallback(() => state.historyIndex < state.history.length - 1, [state.historyIndex, state.history.length]);\n\n    const markClean = useCallback(() => {\n        setState(prev => ({ ...prev, isDirty: false }));\n    }, []);\n\n    return {\n        state,\n        actions: {\n            setContent,\n            undo,\n            redo,\n            canUndo,\n            canRedo,\n            markClean\n        }\n    };\n}"
  },
  {
    "path": "hooks/useSlashCommands.ts",
    "content": "import { useState, useCallback, useEffect, useRef } from 'react';\nimport { AICompletionType } from '@/lib/utils/ai/types';\n\n// Command group definition\nexport interface CommandGroup {\n    name: string;\n    commands: Command[];\n}\n\n// Command definition\nexport interface Command {\n    name: string;\n    description: string;\n    icon: React.ReactNode;\n    action: (text?: string) => void;\n    shortcut?: string;\n    category?: 'basic' | 'format' | 'insert' | 'ai';\n}\n\n// Slash menu state\nexport interface SlashCommandState {\n    visible: boolean;\n    query: string;\n    position: { x: number; y: number } | null;\n    activeCommandIndex: number;\n}\n\n// Hook props\nexport interface UseSlashCommandsProps {\n    content: string;\n    cursorPosition: number;\n    onInsertText: (text: string) => void;\n    onWrapText: (prefix: string, suffix?: string) => void;\n    onAICommand?: (type: AICompletionType, customPrompt?: string) => void;\n    commandGroups?: CommandGroup[];\n    enableAICommands?: boolean;\n}\n\n/**\n * Hook for managing slash commands in the editor\n */\nexport function useSlashCommands({\n                                     content,\n                                     cursorPosition,\n                                     onInsertText,\n                                     onWrapText,\n                                     onAICommand,\n                                     commandGroups: propCommandGroups,\n                                     enableAICommands = true,\n                                 }: UseSlashCommandsProps) {\n    // Slash menu state\n    const [menuState, setMenuState] = useState<SlashCommandState>({\n        visible: false,\n        query: '',\n        position: null,\n        activeCommandIndex: 0,\n    });\n\n    // Use refs to track state without causing updates\n    const stateRef = useRef(menuState);\n    const lastKeyWasSlashRef = useRef(false);\n\n    // Update ref when state changes\n    useEffect(() => {\n        stateRef.current = menuState;\n    }, [menuState]);\n\n    // AI commands group\n    const aiCommandGroup: CommandGroup = {\n        name: 'AI',\n        commands: [\n            {\n                name: 'ai complete',\n                description: 'Complete your thought',\n                icon: '✨',\n                category: 'ai',\n                action: () => onAICommand?.(AICompletionType.COMPLETE),\n            },\n            {\n                name: 'ai expand',\n                description: 'Elaborate on this topic',\n                icon: '↔️',\n                category: 'ai',\n                action: () => onAICommand?.(AICompletionType.EXPAND),\n            },\n            {\n                name: 'ai improve',\n                description: 'Enhance writing style',\n                icon: '✏️',\n                category: 'ai',\n                action: () => onAICommand?.(AICompletionType.IMPROVE),\n            },\n            {\n                name: 'ai summarize',\n                description: 'Summarize content',\n                icon: '📝',\n                category: 'ai',\n                action: () => onAICommand?.(AICompletionType.SUMMARIZE),\n            },\n            {\n                name: 'ai custom',\n                description: 'Custom AI instruction',\n                icon: '🤖',\n                category: 'ai',\n                action: () => onAICommand?.(AICompletionType.CUSTOM),\n            },\n        ],\n    };\n\n    // Default basic commands\n    const defaultCommandGroups: CommandGroup[] = [\n        {\n            name: 'Basic',\n            commands: [\n                {\n                    name: 'Heading 1',\n                    description: 'Add a large heading',\n                    icon: '# H1',\n                    category: 'format',\n                    shortcut: '# ',\n                    action: () => onWrapText('# ', '\\n'),\n                },\n                {\n                    name: 'Heading 2',\n                    description: 'Add a medium heading',\n                    icon: '## H2',\n                    category: 'format',\n                    shortcut: '## ',\n                    action: () => onWrapText('## ', '\\n'),\n                },\n                {\n                    name: 'Heading 3',\n                    description: 'Add a small heading',\n                    icon: '### H3',\n                    category: 'format',\n                    shortcut: '### ',\n                    action: () => onWrapText('### ', '\\n'),\n                },\n                {\n                    name: 'Bold',\n                    description: 'Make text bold',\n                    icon: 'B',\n                    category: 'format',\n                    shortcut: 'Ctrl+B',\n                    action: () => onWrapText('**', '**'),\n                },\n                {\n                    name: 'Italic',\n                    description: 'Make text italic',\n                    icon: 'I',\n                    category: 'format',\n                    shortcut: 'Ctrl+I',\n                    action: () => onWrapText('_', '_'),\n                },\n                {\n                    name: 'Bullet List',\n                    description: 'Add a bulleted list',\n                    icon: '•',\n                    category: 'format',\n                    action: () => onInsertText('- '),\n                },\n                {\n                    name: 'Numbered List',\n                    description: 'Add a numbered list',\n                    icon: '1.',\n                    category: 'format',\n                    action: () => onInsertText('1. '),\n                },\n                {\n                    name: 'Task List',\n                    description: 'Add a checkbox item',\n                    icon: '☐',\n                    category: 'insert',\n                    action: () => onInsertText('- [ ] '),\n                },\n                {\n                    name: 'Code Block',\n                    description: 'Add a code block',\n                    icon: '</>',\n                    category: 'insert',\n                    action: () => onWrapText('```\\n', '\\n```'),\n                },\n                {\n                    name: 'Link',\n                    description: 'Add a link',\n                    icon: '🔗',\n                    category: 'insert',\n                    action: () => onWrapText('[', '](url)'),\n                },\n                {\n                    name: 'Image',\n                    description: 'Add an image',\n                    icon: '🖼️',\n                    category: 'insert',\n                    action: () => onWrapText('![', '](url)'),\n                },\n                {\n                    name: 'Quote',\n                    description: 'Add a blockquote',\n                    icon: '❝',\n                    category: 'insert',\n                    action: () => onInsertText('> '),\n                },\n                {\n                    name: 'Divider',\n                    description: 'Add a horizontal rule',\n                    icon: '―',\n                    category: 'insert',\n                    action: () => onInsertText('\\n---\\n'),\n                },\n                {\n                    name: 'Table',\n                    description: 'Add a table',\n                    icon: '⊞',\n                    category: 'insert',\n                    action: () => onInsertText('\\n| Column 1 | Column 2 |\\n| -------- | -------- |\\n| Cell 1   | Cell 2   |\\n'),\n                },\n            ],\n        }\n    ];\n\n    // Combine command groups\n    const allCommandGroups = propCommandGroups || [\n        ...defaultCommandGroups,\n        ...(enableAICommands && onAICommand ? [aiCommandGroup] : []),\n    ];\n\n    // Get filtered command groups based on query\n    const getFilteredCommandGroups = useCallback(() => {\n        const query = stateRef.current.query.toLowerCase();\n\n        return allCommandGroups\n            .map(group => ({\n                ...group,\n                commands: group.commands.filter(cmd =>\n                    cmd.name.toLowerCase().includes(query)\n                )\n            }))\n            .filter(group => group.commands.length > 0);\n    }, [allCommandGroups]);\n\n    // Calculate menu position\n    const calculateMenuPosition = useCallback(() => {\n        return { x: 0, y: 0 };\n    }, []);\n\n    // Handle keydown events for slash commands\n    const handleKeyDown = useCallback((e: React.KeyboardEvent) => {\n        // Track if the key is a slash to potentially show menu\n        if (e.key === '/') {\n            lastKeyWasSlashRef.current = true;\n            return;\n        }\n\n        // If not a slash, reset flag\n        if (e.key !== 'Shift') { // Ignore shift key presses\n            lastKeyWasSlashRef.current = false;\n        }\n\n        // Handle navigation when menu is visible\n        if (stateRef.current.visible) {\n            // Close menu on escape\n            if (e.key === 'Escape') {\n                e.preventDefault();\n                setMenuState(prev => ({ ...prev, visible: false }));\n                return;\n            }\n\n            // Get current filtered commands\n            const filteredGroups = getFilteredCommandGroups();\n            const filteredCommands = filteredGroups.flatMap(group => group.commands);\n\n            // Navigation keys\n            if (filteredCommands.length > 0) {\n                if (e.key === 'ArrowDown') {\n                    e.preventDefault();\n                    setMenuState(prev => ({\n                        ...prev,\n                        activeCommandIndex: (prev.activeCommandIndex + 1) % filteredCommands.length\n                    }));\n                    return;\n                }\n\n                if (e.key === 'ArrowUp') {\n                    e.preventDefault();\n                    setMenuState(prev => ({\n                        ...prev,\n                        activeCommandIndex: (prev.activeCommandIndex - 1 + filteredCommands.length) % filteredCommands.length\n                    }));\n                    return;\n                }\n\n                // Selection keys\n                if (e.key === 'Enter' || e.key === 'Tab') {\n                    e.preventDefault();\n\n                    try {\n                        const selectedCommand = filteredCommands[stateRef.current.activeCommandIndex];\n                        if (selectedCommand) {\n                            executeCommand(selectedCommand);\n                        }\n                    } catch (error) {\n                        console.error('Error executing command:', error);\n                        setMenuState(prev => ({ ...prev, visible: false }));\n                    }\n                    return;\n                }\n            }\n        }\n    }, [getFilteredCommandGroups]);\n\n    // Execute a command and hide the menu\n    const executeCommand = useCallback((command: Command) => {\n        try {\n            // Replace the slash command in the text if needed\n            const textBeforeCursor = content.substring(0, cursorPosition);\n            const lastSlashIndex = textBeforeCursor.lastIndexOf('/');\n\n            // If the command is an AI command, remove the slash command\n            if (command.category === 'ai' && onAICommand) {\n                // Temporarily store context\n                const beforeSlashText = content.substring(0, lastSlashIndex);\n\n                // Execute AI command\n                command.action(beforeSlashText);\n            } else {\n                // Execute regular command\n                command.action();\n            }\n\n            // Close menu\n            setMenuState(prev => ({ ...prev, visible: false }));\n        } catch (error) {\n            console.error('Error executing command:', error);\n            setMenuState(prev => ({ ...prev, visible: false }));\n        }\n    }, [content, cursorPosition, onAICommand]);\n\n    // Check if a slash was typed and show menu if needed\n    useEffect(() => {\n        try {\n            if (lastKeyWasSlashRef.current && !stateRef.current.visible) {\n                // Show menu when slash is pressed\n                setMenuState({\n                    visible: true,\n                    query: '',\n                    position: calculateMenuPosition(),\n                    activeCommandIndex: 0,\n                });\n            } else if (stateRef.current.visible) {\n                // Update query based on text after slash\n                const textBeforeCursor = content.substring(0, cursorPosition);\n                const lastSlashIndex = textBeforeCursor.lastIndexOf('/');\n\n                if (lastSlashIndex !== -1) {\n                    // Extract query from content\n                    const query = textBeforeCursor.substring(lastSlashIndex + 1);\n\n                    // Only update if query changed to avoid re-renders\n                    if (query !== stateRef.current.query) {\n                        setMenuState(prev => ({ ...prev, query }));\n                    }\n                } else {\n                    // If there's no slash anymore, hide the menu\n                    setMenuState(prev => ({ ...prev, visible: false }));\n                }\n            }\n        } catch (error) {\n            console.error('Error in slash command effect:', error);\n        }\n    }, [content, cursorPosition, calculateMenuPosition]);\n\n    // Show the slash menu\n    const showMenu = useCallback((position: { x: number; y: number } = { x: 0, y: 0 }) => {\n        setMenuState({\n            visible: true,\n            query: '',\n            position,\n            activeCommandIndex: 0,\n        });\n    }, []);\n\n    // Hide the slash menu\n    const hideMenu = useCallback(() => {\n        setMenuState(prev => ({ ...prev, visible: false }));\n    }, []);\n\n    // Handle command click\n    const handleCommandClick = useCallback((command: Command) => {\n        executeCommand(command);\n    }, [executeCommand]);\n\n    // Get current filtered data\n    const filteredCommandGroups = getFilteredCommandGroups();\n    const allCommands = allCommandGroups.flatMap(group => group.commands);\n    const filteredCommands = filteredCommandGroups.flatMap(group => group.commands);\n\n    return {\n        menu: {\n            ...menuState,\n            filteredCommandGroups,\n            allCommands,\n            filteredCommands,\n        },\n        actions: {\n            showMenu,\n            hideMenu,\n            handleKeyDown,\n            handleCommandClick,\n        },\n    };\n}\n\nexport default useSlashCommands;"
  },
  {
    "path": "hooks/useTelemetry.ts",
    "content": "import {TelemetryState} from '@/lib/types/telemetry';\nimport {useState, useEffect, useCallback} from 'react';\n\ninterface TelemetryConfig {\n    allowTelemetry: TelemetryState;\n    instanceId?: string;\n}\n\nexport const useTelemetry = (enabled: boolean = true) => {\n    const [config, setConfig] = useState<TelemetryConfig | null>(null);\n    const [showPrompt, setShowPrompt] = useState(false);\n    const [isLoading, setIsLoading] = useState(true);\n\n    useEffect(() => {\n        if (!enabled) {\n            setIsLoading(false);\n            return;\n        }\n\n        const loadConfig = async () => {\n            try {\n                // Use fetch to call our API endpoint instead of direct database access\n                const response = await fetch('/api/telemetry/config');\n                if (response.ok) {\n                    const currentConfig = await response.json();\n                    setConfig(currentConfig);\n\n                    if (currentConfig.allowTelemetry === 'prompt') {\n                        setShowPrompt(true);\n                    }\n                } else {\n                    // If API fails, default to prompt state\n                    setConfig({ allowTelemetry: 'prompt' });\n                    setShowPrompt(true);\n                }\n            } catch (error) {\n                console.error('Failed to load telemetry config:', error);\n                // Default to prompt state on error\n                setConfig({ allowTelemetry: 'prompt' });\n                setShowPrompt(true);\n            } finally {\n                setIsLoading(false);\n            }\n        };\n\n        loadConfig();\n    }, [enabled]);\n\n    const handleTelemetryChoice = useCallback(\n        async (choice: Extract<TelemetryState, 'enabled' | 'disabled'>) => {\n            if (!enabled || !config) return;\n\n            try {\n                const response = await fetch('/api/telemetry/config', {\n                    method: 'POST',\n                    headers: {\n                        'Content-Type': 'application/json',\n                    },\n                    body: JSON.stringify({ allowTelemetry: choice }),\n                });\n\n                if (response.ok) {\n                    const updatedConfig = await response.json();\n                    setConfig(updatedConfig);\n                    setShowPrompt(false);\n                } else {\n                    console.error('Failed to update telemetry config');\n                }\n            } catch (error) {\n                console.error('Failed to update telemetry config:', error);\n            }\n        },\n        [enabled, config]\n    );\n\n    return {\n        config,\n        showPrompt: enabled ? showPrompt : false,\n        isLoading,\n        handleTelemetryChoice,\n    };\n};"
  },
  {
    "path": "hooks/useWhatsNew.ts",
    "content": "'use client'\n\nimport { useState, useEffect } from 'react'\nimport { appInfo } from '@/lib/app-info'\nimport { compareVersions } from 'compare-versions'\nimport { WhatsNewContent } from '@/components/dashboard/WhatsNewModal'\n\nconst VERSION_STORAGE_KEY = 'changerawr-last-version'\nconst WHATS_NEW_ENDPOINT = 'https://dl.supers0ft.us/changerawr/whatsnew/'\n\nexport function useWhatsNew() {\n    const [lastSeenVersion, setLastSeenVersion] = useState<string | null>(null)\n    const [showWhatsNew, setShowWhatsNew] = useState(false)\n    const [whatsNewContent, setWhatsNewContent] = useState<WhatsNewContent | null>(null)\n    const [isLoading, setIsLoading] = useState(false)\n    const [error, setError] = useState<string | null>(null)\n\n    // On mount, check if we need to show the What's New modal\n    useEffect(() => {\n        // Only run on client-side\n        if (typeof window === 'undefined') return\n\n        // Get the last seen version from localStorage\n        const storedVersion = localStorage.getItem(VERSION_STORAGE_KEY)\n        setLastSeenVersion(storedVersion)\n\n        // No stored version means first time user, or localStorage was cleared\n        if (!storedVersion) {\n            // Instead of setting to current version, set to a lower version to ensure modal shows\n            // Using a very old version like '0.0.1' to ensure it's less than current app version\n            localStorage.setItem(VERSION_STORAGE_KEY, '0.0.1')\n            // Then fetch and show what's new content\n            fetchWhatsNewContent(appInfo.version)\n            return\n        }\n\n        // If the current version is newer than the stored version, fetch What's New content\n        if (compareVersions(appInfo.version, storedVersion) > 0) {\n            fetchWhatsNewContent(appInfo.version)\n        }\n    }, [])\n\n    // Fetch What's New content from the API\n    const fetchWhatsNewContent = async (version: string) => {\n        setIsLoading(true)\n        setError(null)\n\n        try {\n            // First try the external PHP endpoint\n            const response = await fetch(`${WHATS_NEW_ENDPOINT}?version=${version}`, {\n                headers: {\n                    'Accept': 'application/json',\n                    'Cache-Control': 'no-cache'\n                },\n                // Set a timeout in case the PHP endpoint is slow or unavailable\n                signal: AbortSignal.timeout(5000)\n            })\n\n            if (!response.ok) {\n                // If we get an error from the external endpoint, try the fallback\n                throw new Error(`External API returned status: ${response.status}`)\n            }\n\n            const data = await response.json()\n\n            // Validate the response structure\n            if (!data.version || !data.releaseDate || !data.title || !Array.isArray(data.items)) {\n                throw new Error('Invalid response format from external API')\n            }\n\n            setWhatsNewContent(data)\n            setShowWhatsNew(true)\n        } catch (externalError) {\n            console.warn('Error fetching from external What\\'s New API:', externalError)\n\n            // Try the local fallback API\n            try {\n                const fallbackResponse = await fetch(`/api/system/whatsnew?version=${version}`)\n\n                if (!fallbackResponse.ok) {\n                    throw new Error(`Fallback API returned status: ${fallbackResponse.status}`)\n                }\n\n                const fallbackData = await fallbackResponse.json()\n                setWhatsNewContent(fallbackData)\n                setShowWhatsNew(true)\n            } catch (fallbackError) {\n                console.error('Error fetching from fallback What\\'s New API:', fallbackError)\n                setError('Failed to fetch content from both primary and fallback sources')\n\n                // Use default data for the current version as a last resort\n                setWhatsNewContent(getDefaultWhatsNewData(version))\n                setShowWhatsNew(true)\n            }\n        } finally {\n            setIsLoading(false)\n        }\n    }\n\n    // Close the modal and update the last seen version\n    const closeWhatsNew = () => {\n        setShowWhatsNew(false)\n        // Update the last seen version in localStorage\n        localStorage.setItem(VERSION_STORAGE_KEY, appInfo.version)\n        setLastSeenVersion(appInfo.version)\n    }\n\n    // Manual trigger for What's New\n    const manuallyShowWhatsNew = async () => {\n        if (!whatsNewContent) {\n            await fetchWhatsNewContent(appInfo.version)\n        } else {\n            setShowWhatsNew(true)\n        }\n    }\n\n    // Provide fallback data in case both APIs fail\n    const getDefaultWhatsNewData = (version: string): WhatsNewContent => {\n        const now = new Date().toISOString().split('T')[0]\n\n        return {\n            version: version,\n            releaseDate: now,\n            title: `Changerawr ${version}`,\n            description: \"This is an update to the Changerawr application.\",\n            items: [\n                {\n                    title: \"New Features\",\n                    description: \"This version includes several new features and improvements.\",\n                    type: \"improvement\"\n                }\n            ]\n        }\n    }\n\n    // Force a check for new content - useful if version is changed manually\n    const checkForUpdates = () => {\n        const storedVersion = localStorage.getItem(VERSION_STORAGE_KEY)\n\n        if (!storedVersion || compareVersions(appInfo.version, storedVersion) > 0) {\n            fetchWhatsNewContent(appInfo.version)\n        }\n    }\n\n    // Force the modal to show by setting a lower version\n    const forceWhatsNewModal = () => {\n        localStorage.setItem(VERSION_STORAGE_KEY, '0.0.1')\n        fetchWhatsNewContent(appInfo.version)\n        return true\n    }\n\n    return {\n        showWhatsNew,\n        whatsNewContent,\n        isLoading,\n        error,\n        closeWhatsNew,\n        manuallyShowWhatsNew,\n        checkForUpdates,\n        forceWhatsNewModal,\n        lastSeenVersion,\n        currentVersion: appInfo.version\n    }\n}\n\nexport default useWhatsNew"
  },
  {
    "path": "ideas.md",
    "content": "- [ ] custom fields\n- [x] tags can have a color assigned to them (requires updating the SDKs) - partially done DB side, UI implementation\n  must be done.\n    - update tag UI to allow for assigning colors\n    - update PHP & React SDK to return this value\n    - update changelog entries display, changelog tag picker, changelog API to return tag color\n\n- [x] pwnedpasswords password check\n- [x] custom domains (what a headache to implement!)\n- [x] scheduled publishing\n- [x] full-text search\n- [x] version range comparison (catch-up) thinking a digest of what's been done from here-to-there, could be fun!\n- [ ] more changelog customization\n    - set SEO for public changelog\n    - custom scripting for public changelog ( custom js, CSS makes no sense due to changerawr being a CMS)\n    - set a logo for your changelog\n        - requirements:\n        - media manager\n        - storage providers (s3, local, maybe google drive not sure)\n        - enables for reusable media that can be uploaded to the content editor.\n\n- [ ] ability to specify a custom logo for an SSO provider (will be added when I get around to media storage!)\n- [ ] update the MCP server so it can use scheduled publishing\n- [ ] collaboration ( real time, this is complicated! )\n- [ ] allow for inviting other staff members to work on the same changelog entry - collaboration\n- [x] CLI for Changerawr\n- [ ] do stuff with the syncCommit and syncCommitMetadata (no ideas what I could do as of writing)\n- [x] extend markdown with custom elements (perhaps, can call it Changerawr Universal Markdown engine) - CUM\n- [x] allow for importing a full changelog.md into changerawr to jump-start a project from existing data. look into\n  canny importing as well.\n- [x] add a configuration option for project email notifications to allow emails to be sent out on publish\n- [x] allow users to bookmark the entry they are working on to their favorites\n- [x] upgrade to next 16\n- [x] allow for manually giving a publish date.\n- [x] add scopes to API keys for permissioning, also added project-level API keys.\n- [ ] allow for collecting user feedback ( NPS )\n- [ ] publish in multiple languages\n- [ ] allow for emails to support custom domains\n\n# widget ideas\n- [x] classic widget\n- [x] floating widget ( needs fixes, perhaps full rewrite! ) - last thing to do before launching v1.0.5!\n- [x] announcement bar widget\n- [x] modal widget\n\n### CLI Ideas\n\n- [x] get all changelogs under .changerawr/changelogs/${date} via ( changerawr pull --skip-single-file )\n- [ ] figure out if providing a **changerawr push** command is a good idea | add to **changerawr doctor** if implemented.\n\n### Changerawr Universal Markdown Engine Ideas\n\n- [x] Support Subtext\n- [x] Support Tables\n- [x] Support the cool thing Reddit does where you can bold text in a heading ( how did they figure this out? )\n- [x] migrate from internal engine to the package. not sure if this a good idea, maybe? - would require a full rewrite of the editor, but also ensures feature-parity.\n\n### Sponsorship ideas\nHonestly, I need some sort of funding for this at some point. Here's a list of stuff that might incentivise you to support my efforts.\n- [ ] the about page has a list of every single sponsorer of the Changerawr project\n- [ ] dedicated page on the Changerawr website for viewing all sponsors.\n- [ ] sponsor tiers are overrated, will separate it by business/personal though. Any level of support is appreciated.\n- [x] add a license key system to Changerawr that literally just gives a \"supporter\" badge in the about page. \n- [ ] higher feature request priority. I am extremely talented and can most likely add any feature that is requested, perhaps this is a good incentive?\n"
  },
  {
    "path": "instrumentation.ts",
    "content": "export async function register() {\n    if (process.env.NEXT_RUNTIME === 'nodejs') {\n        const {startBackgroundServices} = await import('@/app/startup')\n        await startBackgroundServices()\n    }\n}\n"
  },
  {
    "path": "issues.md",
    "content": "- [x] audit log doesn't populate all actions in filter\n- [x] audit log should load all chunked data in chunks but load all data\n- [x] fix links in AI assistant settings\n- [x] fix theme switcher handling\n- [x] allow for setting custom URLs for SSO Providers\n- [x] changelog analytics\n- [x] fix version compare issues ( better changelog editor UI by a little bit )\n- [x] allow for inviting team members in the setup wizard\n- [x] fix user deletion\n- [x] automatic updates if installed on Easypanel\n- [x] not all audit logs load from the database\n- [x] automatically configure OAuth2 for Easypanel installations\n- [x] If updating an OAuth2 ( SSO ) provider, you won't be able to log in with it anymore\n- [x] for whatever reason, the email you invite has to be lowercase. cool, check for that!"
  },
  {
    "path": "jsdoc.json",
    "content": "{\n  \"tags\": {\n    \"allowUnknownTags\": true,\n    \"dictionaries\": [\"jsdoc\", \"closure\"]\n  },\n  \"source\": {\n    \"include\": [\"app/api\"],\n    \"includePattern\": \".+\\\\.ts$\",\n    \"excludePattern\": \"(^|\\\\/|\\\\\\\\)_\"\n  },\n  \"opts\": {\n    \"destination\": \"./tempDoc.json\",\n    \"recurse\": true,\n    \"template\": \"node_modules/jsdoc/templates/json\"\n  }\n}"
  },
  {
    "path": "lib/api/README.md",
    "content": "# API Permission System\n\nThis directory contains the permission-based API authentication and authorization system for Changerawr.\n\n## Overview\n\nThe system provides:\n- **Automatic route-based permission checking** using route patterns\n- **API key and JWT authentication**\n- **Project-scoped access control**\n- **Granular permissions** for different operations\n\n## Files\n\n- `permissions.ts` - Permission definitions and helper functions\n- `route-permissions.ts` - Route-to-permission mapping configuration\n- `middleware.ts` - Automatic permission enforcement utilities\n\n## Usage\n\n### Method 1: Using `withPermissions` wrapper (Recommended)\n\nThe easiest way to enforce permissions is to wrap your route handlers:\n\n```typescript\n// app/api/projects/[projectId]/changelog/route.ts\nimport { withPermissions } from '@/lib/api/middleware';\nimport { NextRequest, NextResponse } from 'next/server';\n\nexport const GET = withPermissions(async (request, { params }) => {\n    // Permissions are automatically checked based on route-permissions.ts\n    // If user doesn't have access, they'll get a 403 before reaching here\n\n    const { projectId } = params;\n\n    // Your route logic here\n    return NextResponse.json({ data: 'success' });\n});\n\nexport const POST = withPermissions(async (request, { params }) => {\n    const { projectId } = params;\n    const body = await request.json();\n\n    // Create changelog entry\n    return NextResponse.json({ created: true });\n});\n```\n\n### Method 2: Manual permission checking\n\nFor more control, use the helper functions:\n\n```typescript\nimport { authenticateRequest, hasProjectAccess } from '@/lib/auth/api-key';\nimport { API_PERMISSIONS } from '@/lib/api/permissions';\nimport { requirePermission, requireProjectAccess } from '@/lib/api/middleware';\n\nexport async function GET(request: NextRequest) {\n    // Check specific permission\n    const permCheck = await requirePermission(request, API_PERMISSIONS.CHANGELOG_READ);\n    if ('error' in permCheck) {\n        return permCheck.error;\n    }\n\n    // Check project access\n    const accessCheck = await requireProjectAccess(request, projectId);\n    if ('error' in accessCheck) {\n        return accessCheck.error;\n    }\n\n    // Your route logic here\n}\n```\n\n### Method 3: Using validateAuthAndGetUser (Legacy, still works)\n\nThis is the existing method used throughout the codebase:\n\n```typescript\nimport { validateAuthAndGetUser } from '@/lib/utils/changelog';\n\nexport async function GET(request: NextRequest) {\n    try {\n        const user = await validateAuthAndGetUser();\n        // User is authenticated, continue with logic\n    } catch (error) {\n        return NextResponse.json(\n            { error: 'Unauthorized' },\n            { status: 401 }\n        );\n    }\n}\n```\n\nNote: This method authenticates but doesn't enforce specific permissions. It's best used for routes that just need authentication without specific permission requirements.\n\n## Route Configuration\n\nRoutes are configured in `route-permissions.ts`. Example:\n\n```typescript\nexport const ROUTE_PERMISSIONS: Record<string, RoutePermissionConfig> = {\n    // Public route (no auth required)\n    '/api/health': { public: true },\n\n    // Admin-only route\n    '/api/admin/users': { requiresAdmin: true },\n\n    // Requires specific permission\n    '/api/changelog/:projectId/entries': {\n        permissions: API_PERMISSIONS.CHANGELOG_READ,\n        requiresProjectAccess: true,\n        methods: ['GET']  // Only applies to GET requests\n    },\n\n    // Requires ANY of multiple permissions (OR logic)\n    '/api/projects/:projectId/changelog': {\n        permissions: [\n            API_PERMISSIONS.CHANGELOG_READ,\n            API_PERMISSIONS.CHANGELOG_WRITE\n        ],\n        requiresProjectAccess: true\n    },\n};\n```\n\n## Permission Types\n\nDefined in `permissions.ts`:\n\n- **Changelog**: `changelog:read`, `changelog:write`, `changelog:publish`, `changelog:delete`\n- **Project**: `project:read`, `project:write`\n- **Analytics**: `analytics:read`\n- **Subscribers**: `subscribers:read`, `subscribers:write`\n- **Email**: `email:send`\n- **GitHub**: `github:read`, `github:write`\n- **Tags**: `tags:read`, `tags:write`\n- **Webhooks**: `webhooks:read`, `webhooks:write`\n\n## Permission Groups\n\nPre-configured groups for common use cases:\n\n- `READ_ONLY` - Read access to all resources\n- `CHANGELOG_MANAGER` - Full changelog management without project config changes\n- `FULL_ACCESS` - All permissions\n\n## Authentication Flow\n\n1. Request comes in with either:\n   - JWT token in cookie (`accessToken`)\n   - API key in Authorization header (`Bearer chr_...`)\n\n2. `authenticateRequest()` validates the token/key and returns:\n   ```typescript\n   {\n       userId: string;\n       apiKeyId?: string;\n       projectId?: string | null;  // null = global access\n       permissions: string[];\n       isApiKey: boolean;\n   }\n   ```\n\n3. Permission checks:\n   - **JWT tokens** have full access (no permission checks)\n   - **API keys** are checked against their permission array\n   - **Project-scoped keys** can only access their assigned project\n   - **Global keys** can access all projects (if they have permission)\n\n4. Route-based enforcement (if using `withPermissions`):\n   - Matches request path to route pattern\n   - Checks if route is public\n   - Validates authentication\n   - Checks admin requirement\n   - Validates project access\n   - Validates permissions (OR logic for arrays)\n\n## Migration Guide\n\nTo migrate an existing route to the new system:\n\n1. **Add route to `route-permissions.ts`**:\n   ```typescript\n   '/api/your/route/:id': {\n       permissions: API_PERMISSIONS.YOUR_PERMISSION,\n       requiresProjectAccess: true  // if needed\n   }\n   ```\n\n2. **Wrap your handler**:\n   ```typescript\n   // Before\n   export async function GET(request: NextRequest) {\n       const user = await validateAuthAndGetUser();\n       // logic\n   }\n\n   // After\n   export const GET = withPermissions(async (request, { params }) => {\n       // Permission already checked, just implement logic\n   });\n   ```\n\n3. **Test with API keys**:\n   - Create an API key with limited permissions\n   - Verify it can access allowed routes\n   - Verify it's blocked from restricted routes\n\n## Best Practices\n\n1. **Always use route configuration** - Define all routes in `route-permissions.ts` for centralized permission management\n\n2. **Use wrapper when possible** - `withPermissions` provides automatic enforcement and is easier to maintain\n\n3. **Prefer specific permissions** - Use specific permissions rather than `FULL_ACCESS` for better security\n\n4. **Document permission requirements** - Add comments in route files explaining why certain permissions are needed\n\n5. **Test with restricted keys** - Always test API endpoints with limited-permission API keys to ensure enforcement works\n\n## Security Notes\n\n- JWT tokens bypass permission checks (they're for authenticated users)\n- API keys must have explicit permissions\n- Admin-only routes require ADMIN role regardless of permissions\n- Project-scoped keys can ONLY access their assigned project\n- Public routes skip all authentication\n- Permission arrays use OR logic (user needs ANY permission, not all)"
  },
  {
    "path": "lib/api/middleware.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server';\nimport { authenticateRequest, hasProjectAccess } from '@/lib/auth/api-key';\nimport { matchRoute, extractProjectId, methodAllowed, type RoutePermissionConfig } from './route-permissions';\nimport { db } from '@/lib/db';\nimport { ApiPermission } from './permissions';\n\nexport interface ApiError {\n    error: string;\n    code?: string;\n    status: number;\n}\n\n/**\n * Automatic route-based permission enforcement middleware.\n * Checks authentication, project access, and permissions based on route configuration.\n */\nexport async function enforceRoutePermissions(\n    request: NextRequest,\n    path: string\n): Promise<NextResponse | null> {\n    // Match route to configuration\n    const routeConfig = matchRoute(path);\n    if (!routeConfig) {\n        // No matching route found - require authentication by default\n        return createErrorResponse('Route not found', 404);\n    }\n\n    // Public routes don't require authentication\n    if (routeConfig.public) {\n        return null; // Continue\n    }\n\n    // Check if method is allowed for this route\n    if (!methodAllowed(routeConfig, request.method)) {\n        return createErrorResponse(`Method ${request.method} not allowed for this route`, 405);\n    }\n\n    // Authenticate the request\n    const ctx = await authenticateRequest(request);\n    if (!ctx) {\n        return createErrorResponse('Authentication required', 401);\n    }\n\n    // Get user to check role\n    const user = await db.user.findUnique({\n        where: { id: ctx.userId },\n        select: { role: true }\n    });\n\n    if (!user) {\n        return createErrorResponse('User not found', 401);\n    }\n\n    // Admin-only routes\n    if (routeConfig.requiresAdmin) {\n        if (user.role !== 'ADMIN') {\n            return createErrorResponse('Admin access required', 403);\n        }\n        return null; // Admin has access, continue\n    }\n\n    // Check project access if required\n    if (routeConfig.requiresProjectAccess) {\n        const projectId = extractProjectId(path);\n        if (!projectId) {\n            return createErrorResponse('Project ID not found in route', 400);\n        }\n\n        if (!hasProjectAccess(ctx, projectId)) {\n            return createErrorResponse('Access denied to this project', 403);\n        }\n    }\n\n    // Check permissions if required\n    if (routeConfig.permissions) {\n        // JWT tokens (not API keys) have full access\n        if (!ctx.isApiKey) {\n            return null; // Continue\n        }\n\n        // API keys need to have the required permissions\n        const requiredPerms = Array.isArray(routeConfig.permissions)\n            ? routeConfig.permissions\n            : [routeConfig.permissions];\n\n        // User needs ANY of the permissions (OR logic)\n        const hasRequiredPerm = requiredPerms.some(perm =>\n            ctx.permissions.includes(perm)\n        );\n\n        if (!hasRequiredPerm) {\n            return createErrorResponse(\n                `Missing required permission. Need one of: ${requiredPerms.join(', ')}`,\n                403\n            );\n        }\n    }\n\n    // All checks passed\n    return null;\n}\n\n/**\n * Wrapper for API route handlers that automatically enforces permissions.\n * Use this to wrap your route handlers for automatic permission checking.\n *\n * @example\n * export const GET = withPermissions(async (request, ctx) => {\n *     // Your route logic here\n *     // ctx contains authenticated user info and permissions\n *     return NextResponse.json({ data: 'success' });\n * });\n */\nexport function withPermissions<T extends Record<string, unknown>>(\n    handler: (\n        request: NextRequest,\n        context: { params: T }\n    ) => Promise<NextResponse>\n) {\n    return async (\n        request: NextRequest,\n        context: { params: T }\n    ): Promise<NextResponse> => {\n        // Get the pathname from the request\n        const path = new URL(request.url).pathname;\n\n        // Enforce permissions\n        const error = await enforceRoutePermissions(request, path);\n        if (error) {\n            return error;\n        }\n\n        // Call the actual handler\n        return handler(request, context);\n    };\n}\n\n/**\n * Helper to create standardized error responses\n */\nfunction createErrorResponse(message: string, status: number): NextResponse {\n    return NextResponse.json(\n        { error: message },\n        { status }\n    );\n}\n\n/**\n * Validate permission for a specific action.\n * Use this within route handlers for additional permission checks.\n */\nexport async function requirePermission(\n    request: NextRequest,\n    permission: ApiPermission\n): Promise<{ error: NextResponse } | { success: true }> {\n    const ctx = await authenticateRequest(request);\n\n    if (!ctx) {\n        return { error: createErrorResponse('Authentication required', 401) };\n    }\n\n    // JWT tokens have all permissions\n    if (!ctx.isApiKey) {\n        return { success: true };\n    }\n\n    if (!ctx.permissions.includes(permission)) {\n        return {\n            error: createErrorResponse(\n                `Missing required permission: ${permission}`,\n                403\n            )\n        };\n    }\n\n    return { success: true };\n}\n\n/**\n * Validate project access.\n * Use this within route handlers for additional project scope checks.\n */\nexport async function requireProjectAccess(\n    request: NextRequest,\n    projectId: string\n): Promise<{ error: NextResponse } | { success: true }> {\n    const ctx = await authenticateRequest(request);\n\n    if (!ctx) {\n        return { error: createErrorResponse('Authentication required', 401) };\n    }\n\n    if (!hasProjectAccess(ctx, projectId)) {\n        return {\n            error: createErrorResponse(\n                'Access denied to this project',\n                403\n            )\n        };\n    }\n\n    return { success: true };\n}"
  },
  {
    "path": "lib/api/permissions.ts",
    "content": "/**\n * API Key Permission Definitions\n *\n * Permissions follow the format: resource:action\n * Used for scope-based access control on API keys.\n */\n\nexport const API_PERMISSIONS = {\n    // Changelog entry operations\n    CHANGELOG_READ: 'changelog:read',           // Fetch changelog entries and metadata\n    CHANGELOG_WRITE: 'changelog:write',         // Create and update changelog entries\n    CHANGELOG_PUBLISH: 'changelog:publish',     // Publish or unpublish entries\n    CHANGELOG_DELETE: 'changelog:delete',       // Delete changelog entries\n\n    // Project configuration\n    PROJECT_READ: 'project:read',               // Read project settings and configuration\n    PROJECT_WRITE: 'project:write',             // Modify project settings\n\n    // Analytics and metrics\n    ANALYTICS_READ: 'analytics:read',           // Access analytics data and metrics\n\n    // Subscription management\n    SUBSCRIBERS_READ: 'subscribers:read',       // List and view subscribers\n    SUBSCRIBERS_WRITE: 'subscribers:write',     // Add, update, or remove subscribers\n\n    // Email notifications\n    EMAIL_SEND: 'email:send',                   // Trigger email notifications for entries\n\n    // GitHub integration\n    GITHUB_READ: 'github:read',                 // Read GitHub integration settings\n    GITHUB_WRITE: 'github:write',               // Modify GitHub integration and generate entries\n\n    // Tags and categorization\n    TAGS_READ: 'tags:read',                     // List and view tags\n    TAGS_WRITE: 'tags:write',                   // Create, update, or delete tags\n\n    // Webhook operations (placeholder for future implementation)\n    WEBHOOKS_READ: 'webhooks:read',             // List webhook configurations\n    WEBHOOKS_WRITE: 'webhooks:write',           // Create, update, or delete webhooks\n} as const;\n\nexport type ApiPermission = typeof API_PERMISSIONS[keyof typeof API_PERMISSIONS];\n\n/**\n * Permission groups for common use cases.\n * These are convenience sets for typical access patterns.\n */\nexport const PERMISSION_GROUPS = {\n    // Read-only access to all resources\n    READ_ONLY: [\n        API_PERMISSIONS.CHANGELOG_READ,\n        API_PERMISSIONS.PROJECT_READ,\n        API_PERMISSIONS.ANALYTICS_READ,\n        API_PERMISSIONS.SUBSCRIBERS_READ,\n        API_PERMISSIONS.GITHUB_READ,\n        API_PERMISSIONS.TAGS_READ,\n        API_PERMISSIONS.WEBHOOKS_READ,\n    ],\n\n    // Full changelog management without project config changes\n    CHANGELOG_MANAGER: [\n        API_PERMISSIONS.CHANGELOG_READ,\n        API_PERMISSIONS.CHANGELOG_WRITE,\n        API_PERMISSIONS.CHANGELOG_PUBLISH,\n        API_PERMISSIONS.CHANGELOG_DELETE,\n        API_PERMISSIONS.TAGS_READ,\n        API_PERMISSIONS.TAGS_WRITE,\n    ],\n\n    // Full access to everything\n    FULL_ACCESS: Object.values(API_PERMISSIONS),\n} as const;\n\n/**\n * Check if an API key has a specific permission.\n *\n * @param keyPermissions - Array of permissions from the API key\n * @param requiredPermission - Permission to check for\n * @returns true if key has the permission\n */\nexport function hasPermission(\n    keyPermissions: string[],\n    requiredPermission: ApiPermission\n): boolean {\n    return keyPermissions.includes(requiredPermission);\n}\n\n/**\n * Check if an API key has all required permissions.\n *\n * @param keyPermissions - Array of permissions from the API key\n * @param requiredPermissions - Permissions to check for\n * @returns true if key has all permissions\n */\nexport function hasAllPermissions(\n    keyPermissions: string[],\n    requiredPermissions: ApiPermission[]\n): boolean {\n    return requiredPermissions.every(perm => keyPermissions.includes(perm));\n}\n\n/**\n * Check if an API key has any of the required permissions.\n *\n * @param keyPermissions - Array of permissions from the API key\n * @param requiredPermissions - Permissions to check for\n * @returns true if key has at least one permission\n */\nexport function hasAnyPermission(\n    keyPermissions: string[],\n    requiredPermissions: ApiPermission[]\n): boolean {\n    return requiredPermissions.some(perm => keyPermissions.includes(perm));\n}"
  },
  {
    "path": "lib/api/route-permissions.ts",
    "content": "import {API_PERMISSIONS} from './permissions';\n\n/**\n * Route permission configuration\n * Maps API route patterns to required permissions and project scope requirements\n */\n\nexport interface RoutePermissionConfig {\n    /** Required permission(s) for this route. If array, user needs ANY of them (OR logic) */\n    permissions?: string | string[];\n    /** Whether this route requires project-specific access */\n    requiresProjectAccess?: boolean;\n    /** Whether this route requires admin role (bypasses API key permissions) */\n    requiresAdmin?: boolean;\n    /** Whether this route is public (no auth required) */\n    public?: boolean;\n    /** HTTP methods this config applies to. If not specified, applies to all methods */\n    methods?: ('GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE')[];\n}\n\n/**\n * Route permission mapping organized by category\n * Routes are matched from most specific to least specific\n * Use :param syntax for dynamic segments (e.g., /projects/:projectId)\n */\nexport const ROUTE_PERMISSIONS: Record<string, RoutePermissionConfig> = {\n    // ==========================================\n    // PUBLIC ROUTES (No authentication required)\n    // ==========================================\n    '/api/health': {public: true},\n    '/api/check-setup': {public: true},\n    '/api/system/version': {public: true},\n    '/api/config/timezone': {public: true},\n\n    // Setup routes (initial installation)\n    '/api/setup': {public: true},\n    '/api/setup/status': {public: true},\n    '/api/setup/admin': {public: true},\n    '/api/setup/invitations': {public: true},\n    '/api/setup/oauth': {public: true},\n    '/api/setup/oauth/auto': {public: true},\n    '/api/setup/oauth/debug': {public: true},\n    '/api/setup/settings': {public: true},\n\n    // Authentication routes (public by nature)\n    '/api/auth/login': {public: true},\n    '/api/auth/login/second-factor': {public: true},\n    '/api/auth/register': {public: true},\n    '/api/auth/forgot-password': {public: true},\n    '/api/auth/reset-password/request': {public: true},\n    '/api/auth/reset-password/:token': {public: true},\n    '/api/auth/invitation/:token': {public: true},\n    '/api/auth/oauth/providers': {public: true},\n    '/api/auth/oauth/authorize/:providerName': {public: true},\n    '/api/auth/oauth/callback/:providerName': {public: true},\n    '/api/auth/passkeys/authenticate/options': {public: true},\n    '/api/auth/passkeys/authenticate/verify': {public: true},\n\n    // Public changelog/widget routes\n    '/api/changelog/subscribe': {public: true},\n    '/api/changelog/unsubscribe/:token': {public: true},\n    '/api/changelog/verify-domain': {public: true},\n    '/api/integrations/widget/:projectId': {public: true},\n\n    // ==========================================\n    // ADMIN-ONLY ROUTES (Require ADMIN role)\n    // ==========================================\n\n    // User management\n    '/api/admin/users': {requiresAdmin: true},\n    '/api/admin/users/:userId': {requiresAdmin: true},\n    '/api/admin/users/:userId/role': {requiresAdmin: true},\n    '/api/admin/users/invitations': {requiresAdmin: true},\n    '/api/admin/users/invitations/:id': {requiresAdmin: true},\n\n    // System configuration\n    '/api/admin/config': {requiresAdmin: true},\n    '/api/admin/config/system-email': {requiresAdmin: true},\n    '/api/admin/oauth/providers': {requiresAdmin: true},\n    '/api/admin/oauth/providers/:id': {requiresAdmin: true},\n\n    // License\n    '/api/admin/sponsor': {requiresAdmin: true},\n\n    // AI settings\n    '/api/admin/ai-settings': {requiresAdmin: true},\n    '/api/admin/ai-settings/test-key': {requiresAdmin: true},\n\n    // Audit logs\n    '/api/admin/audit-logs': {requiresAdmin: true},\n    '/api/admin/audit-logs/actions': {requiresAdmin: true},\n\n    // Admin analytics & dashboard\n    '/api/admin/analytics': {requiresAdmin: true},\n    '/api/admin/dashboard': {requiresAdmin: true},\n\n    // API key management (admin level)\n    '/api/admin/api-keys': {requiresAdmin: true},\n    '/api/admin/api-keys/:keyId': {requiresAdmin: true},\n\n    // System management\n    '/api/system/easypanel/status': {requiresAdmin: true},\n    '/api/system/perform-update': {requiresAdmin: true},\n    '/api/system/update-status': {requiresAdmin: true},\n\n    // Telemetry\n    '/api/telemetry/config': {requiresAdmin: true},\n    '/api/telemetry/debug': {requiresAdmin: true},\n\n    // ==========================================\n    // CHANGELOG ROUTES (API-accessible)\n    // ==========================================\n\n    // Read changelog entries\n    '/api/changelog/:projectId/entries': {\n        permissions: API_PERMISSIONS.CHANGELOG_READ,\n        requiresProjectAccess: true,\n        methods: ['GET']\n    },\n\n    // Individual entry operations\n    '/api/changelog/entries/:entryId': {\n        permissions: [API_PERMISSIONS.CHANGELOG_READ, API_PERMISSIONS.CHANGELOG_WRITE, API_PERMISSIONS.CHANGELOG_DELETE]\n    },\n\n    // Changelog requests (feature requests)\n    '/api/changelog/requests': {\n        permissions: API_PERMISSIONS.CHANGELOG_READ,\n        methods: ['GET']\n    },\n    '/api/changelog/requests/:requestId': {\n        permissions: API_PERMISSIONS.CHANGELOG_WRITE\n    },\n\n    // ==========================================\n    // PROJECT ROUTES (API-accessible)\n    // ==========================================\n\n    // List/create projects\n    '/api/projects': {\n        permissions: API_PERMISSIONS.PROJECT_READ\n    },\n\n    // Project details\n    '/api/projects/:projectId': {\n        permissions: API_PERMISSIONS.PROJECT_READ,\n        requiresProjectAccess: true\n    },\n\n    // Project settings\n    '/api/projects/:projectId/settings': {\n        permissions: API_PERMISSIONS.PROJECT_WRITE,\n        requiresProjectAccess: true\n    },\n\n    // Project versions\n    '/api/projects/:projectId/versions': {\n        permissions: API_PERMISSIONS.PROJECT_READ,\n        requiresProjectAccess: true\n    },\n\n    // ==========================================\n    // PROJECT CHANGELOG ROUTES\n    // ==========================================\n\n    // Changelog entries (CRUD)\n    '/api/projects/:projectId/changelog': {\n        permissions: [API_PERMISSIONS.CHANGELOG_READ, API_PERMISSIONS.CHANGELOG_WRITE],\n        requiresProjectAccess: true\n    },\n    '/api/projects/:projectId/changelog/:entryId': {\n        permissions: [API_PERMISSIONS.CHANGELOG_READ, API_PERMISSIONS.CHANGELOG_WRITE, API_PERMISSIONS.CHANGELOG_DELETE],\n        requiresProjectAccess: true\n    },\n\n    // Scheduling & publishing\n    '/api/projects/:projectId/changelog/:entryId/schedule': {\n        permissions: API_PERMISSIONS.CHANGELOG_PUBLISH,\n        requiresProjectAccess: true\n    },\n    '/api/projects/:projectId/changelog/:entryId/schedule/approval': {\n        permissions: API_PERMISSIONS.CHANGELOG_PUBLISH,\n        requiresProjectAccess: true\n    },\n\n    // Tags management\n    '/api/projects/:projectId/changelog/tags': {\n        permissions: [API_PERMISSIONS.TAGS_READ, API_PERMISSIONS.TAGS_WRITE],\n        requiresProjectAccess: true\n    },\n    '/api/projects/:projectId/changelog/tags/:tagId': {\n        permissions: [API_PERMISSIONS.TAGS_READ, API_PERMISSIONS.TAGS_WRITE],\n        requiresProjectAccess: true\n    },\n\n    // ==========================================\n    // PROJECT ANALYTICS ROUTES\n    // ==========================================\n\n    '/api/projects/:projectId/analytics': {\n        permissions: API_PERMISSIONS.ANALYTICS_READ,\n        requiresProjectAccess: true\n    },\n    '/api/projects/:projectId/analytics/export': {\n        permissions: API_PERMISSIONS.ANALYTICS_READ,\n        requiresProjectAccess: true\n    },\n\n    // ==========================================\n    // PROJECT INTEGRATIONS\n    // ==========================================\n\n    // Email integration\n    '/api/projects/:projectId/integrations/email': {\n        permissions: [API_PERMISSIONS.PROJECT_READ, API_PERMISSIONS.PROJECT_WRITE],\n        requiresProjectAccess: true\n    },\n    '/api/projects/:projectId/integrations/email/send': {\n        permissions: API_PERMISSIONS.EMAIL_SEND,\n        requiresProjectAccess: true\n    },\n    '/api/projects/:projectId/integrations/email/test': {\n        permissions: API_PERMISSIONS.EMAIL_SEND,\n        requiresProjectAccess: true\n    },\n\n    // GitHub integration\n    '/api/projects/:projectId/integrations/github': {\n        permissions: [API_PERMISSIONS.GITHUB_READ, API_PERMISSIONS.GITHUB_WRITE],\n        requiresProjectAccess: true\n    },\n    '/api/projects/:projectId/integrations/github/generate': {\n        permissions: API_PERMISSIONS.GITHUB_WRITE,\n        requiresProjectAccess: true\n    },\n    '/api/projects/:projectId/integrations/github/tags': {\n        permissions: API_PERMISSIONS.GITHUB_READ,\n        requiresProjectAccess: true\n    },\n    '/api/projects/:projectId/integrations/github/test': {\n        permissions: API_PERMISSIONS.GITHUB_READ,\n        requiresProjectAccess: true\n    },\n\n    // ==========================================\n    // PROJECT CLI ROUTES\n    // ==========================================\n\n    '/api/projects/:projectId/cli/link': {\n        permissions: API_PERMISSIONS.PROJECT_WRITE,\n        requiresProjectAccess: true\n    },\n    '/api/projects/:projectId/cli/unlink': {\n        permissions: API_PERMISSIONS.PROJECT_WRITE,\n        requiresProjectAccess: true\n    },\n    '/api/projects/:projectId/cli/sync': {\n        permissions: API_PERMISSIONS.CHANGELOG_WRITE,\n        requiresProjectAccess: true\n    },\n    '/api/projects/:projectId/cli/sync/status': {\n        permissions: API_PERMISSIONS.CHANGELOG_READ,\n        requiresProjectAccess: true\n    },\n\n    // ==========================================\n    // PROJECT API KEYS (Project-scoped)\n    // ==========================================\n\n    '/api/projects/:projectId/api-keys': {\n        permissions: API_PERMISSIONS.PROJECT_WRITE,\n        requiresProjectAccess: true\n    },\n    '/api/projects/:projectId/api-keys/:keyId': {\n        permissions: API_PERMISSIONS.PROJECT_WRITE,\n        requiresProjectAccess: true\n    },\n\n    // ==========================================\n    // PROJECT CATCH-UP / AI SUMMARY\n    // ==========================================\n\n    '/api/projects/:projectId/catch-up': {\n        permissions: API_PERMISSIONS.CHANGELOG_READ,\n        requiresProjectAccess: true\n    },\n    '/api/projects/:projectId/catch-up/ai-summary': {\n        permissions: API_PERMISSIONS.CHANGELOG_READ,\n        requiresProjectAccess: true\n    },\n\n    // ==========================================\n    // SUBSCRIBERS ROUTES\n    // ==========================================\n\n    '/api/subscribers': {\n        permissions: [API_PERMISSIONS.SUBSCRIBERS_READ, API_PERMISSIONS.SUBSCRIBERS_WRITE]\n    },\n    '/api/subscribers/:subscriberId': {\n        permissions: API_PERMISSIONS.SUBSCRIBERS_WRITE\n    },\n    '/api/subscribers/generate-mock': {\n        permissions: API_PERMISSIONS.SUBSCRIBERS_WRITE\n    },\n\n    // ==========================================\n    // PROJECT IMPORT ROUTES\n    // ==========================================\n\n    '/api/projects/import/canny/validate': {\n        permissions: API_PERMISSIONS.PROJECT_WRITE\n    },\n    '/api/projects/import/canny/fetch': {\n        permissions: API_PERMISSIONS.PROJECT_WRITE\n    },\n    '/api/projects/import/parse': {\n        permissions: API_PERMISSIONS.PROJECT_WRITE\n    },\n    '/api/projects/import/process': {\n        permissions: API_PERMISSIONS.PROJECT_WRITE\n    },\n\n    // ==========================================\n    // USER ROUTES (Authenticated, no special perms)\n    // ==========================================\n\n    // Auth session management\n    '/api/auth/me': {},\n    '/api/auth/logout': {},\n    '/api/auth/refresh': {},\n    '/api/auth/validate': {},\n    '/api/auth/preview': {},\n\n    // User settings\n    '/api/auth/change-password': {},\n    '/api/auth/connections': {},\n    '/api/auth/security-settings': {},\n    '/api/auth/settings': {},\n\n    // Passkeys (authenticated user managing their own)\n    '/api/auth/passkeys': {},\n    '/api/auth/passkeys/:id': {},\n    '/api/auth/passkeys/register/options': {},\n    '/api/auth/passkeys/register/verify': {},\n\n    // CLI authentication\n    '/api/auth/cli/generate': {},\n    '/api/auth/cli/token': {},\n    '/api/auth/cli/refresh': {},\n\n    // User dashboard\n    '/api/dashboard/stats': {},\n\n    // Search\n    '/api/search': {},\n\n    // Feature requests\n    '/api/requests': {},\n\n    // AI settings (user-level)\n    '/api/ai/settings': {},\n    '/api/ai/decrypt': {},\n\n    // Analytics tracking (user actions)\n    '/api/analytics/track': {},\n\n    // Custom domains (auth enforced directly in route handlers)\n    '/api/custom-domains/list': {},\n    '/api/custom-domains/add': {},\n    '/api/custom-domains/verify': {},\n    '/api/custom-domains/:domain': {},\n\n    // Internal endpoints (protected by INTERNAL_API_SECRET header, not JWT)\n    '/api/internal/ip-config': {public: true},\n};\n\n/**\n * Match a request path to a route pattern\n * Converts Next.js dynamic segments to pattern format\n */\nexport function matchRoute(path: string): RoutePermissionConfig | null {\n    // Normalize path (remove trailing slash, ensure leading slash)\n    const normalizedPath = path.replace(/\\/$/, '') || '/';\n\n    // Try exact match first\n    if (ROUTE_PERMISSIONS[normalizedPath]) {\n        return ROUTE_PERMISSIONS[normalizedPath];\n    }\n\n    // Try pattern matching for dynamic routes\n    // Sort patterns by specificity (more segments = more specific)\n    const patterns = Object.entries(ROUTE_PERMISSIONS).sort((a, b) => {\n        const aSegments = a[0].split('/').length;\n        const bSegments = b[0].split('/').length;\n        return bSegments - aSegments; // More segments first\n    });\n\n    for (const [pattern, config] of patterns) {\n        const regex = patternToRegex(pattern);\n        if (regex.test(normalizedPath)) {\n            return config;\n        }\n    }\n\n    // No match found - require authentication by default\n    return {};\n}\n\n/**\n * Convert route pattern to regex\n * Converts :param to regex capture groups\n */\nfunction patternToRegex(pattern: string): RegExp {\n    const regexPattern = pattern\n        .replace(/:[^/]+/g, '[^/]+') // Replace :param with [^/]+\n        .replace(/\\//g, '\\\\/');       // Escape slashes\n    return new RegExp(`^${regexPattern}$`);\n}\n\n/**\n * Extract project ID from request path if present\n */\nexport function extractProjectId(path: string): string | null {\n    const match = path.match(/\\/projects\\/([^/]+)/);\n    return match ? match[1] : null;\n}\n\n/**\n * Check if a route config allows a specific HTTP method\n */\nexport function methodAllowed(config: RoutePermissionConfig, method: string): boolean {\n    if (!config.methods) return true; // No method restriction\n    return config.methods.includes(method as 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE');\n}"
  },
  {
    "path": "lib/app-info.ts",
    "content": "/**\n * Application information and metadata\n * This is a central place to manage version information and other app details.\n */\n\nexport const appInfo = {\n    name: 'Changerawr',\n    version: '1.0.6',\n    status: 'Stable',\n    environment: process.env.NODE_ENV || 'development',\n    license: 'CNC OSL',\n    releaseDate: '2025-06-15', // this is when Changerawr 1.0.0 first released\n\n    framework: 'Next.js App Router',\n    database: 'PostgreSQL with Prisma ORM',\n    cumEngine: '1.2.0', // package version of @changerawr/markdown\n\n    // Repository and documentation links\n    repository: 'https://github.com/supernova3339/changerawr',\n    author: 'supernova3339',\n    sponsors_url: 'https://github.com/sponsors/Supernova3339',\n    documentation: '/api-docs',\n};\n\n/**\n * Get the application version with status\n */\nexport function getVersionString(): string {\n    return `${appInfo.version}${appInfo.status ? ` (${appInfo.status})` : ''}`;\n}\n\n/**\n * Get the copyright year range\n */\nexport function getCopyrightYears(): string {\n    const startYear = 2025; // Founding year\n    const currentYear = new Date().getFullYear();\n    return startYear === currentYear ? `${startYear}` : `${startYear}-${currentYear}`;\n}\n"
  },
  {
    "path": "lib/auth/api-key.ts",
    "content": "import {NextRequest} from 'next/server';\nimport {db} from '@/lib/db';\nimport {verifyAccessToken} from '@/lib/auth/tokens';\nimport {ApiPermission, hasPermission, hasAllPermissions} from '@/lib/api/permissions';\n\nexport interface AuthContext {\n    userId: string;\n    apiKeyId?: string;\n    projectId?: string | null;  // null = global access, string = project-specific\n    permissions: string[];\n    isApiKey: boolean;           // true if authenticated via API key, false if JWT\n}\n\n// Minimal request interface for authentication\nexport interface AuthRequest {\n    headers: {\n        get(name: string): string | null;\n    };\n    cookies: {\n        get(name: string): { value: string } | undefined;\n    };\n}\n\n/**\n * Extract authentication token from request.\n * Checks both cookies and Authorization header.\n */\nfunction getTokenFromRequest(request: AuthRequest): {type: 'jwt' | 'apikey', token: string} | null {\n    // Check Authorization header first\n    const authHeader = request.headers.get('authorization');\n    if (authHeader) {\n        if (authHeader.startsWith('Bearer ')) {\n            const token = authHeader.substring(7);\n            // API keys start with 'chr_', JWT tokens don't\n            if (token.startsWith('chr_')) {\n                return {type: 'apikey', token};\n            }\n            return {type: 'jwt', token};\n        }\n    }\n\n    // Fall back to cookies for JWT\n    const accessToken = request.cookies.get('accessToken')?.value;\n    if (accessToken) {\n        return {type: 'jwt', token: accessToken};\n    }\n\n    return null;\n}\n\n/**\n * Authenticate request using JWT access token.\n */\nasync function authenticateWithJWT(token: string): Promise<AuthContext | null> {\n    try {\n        const userId = await verifyAccessToken(token);\n        if (!userId) {\n            return null;\n        }\n\n        // JWT tokens have full access to everything\n        return {\n            userId,\n            projectId: null,  // Global access\n            permissions: [],  // Empty = full access for JWT\n            isApiKey: false,\n        };\n    } catch (error) {\n        console.error('JWT verification failed:', error);\n        return null;\n    }\n}\n\n/**\n * Authenticate request using API key.\n * Updates lastUsed timestamp on successful auth.\n */\nasync function authenticateWithAPIKey(key: string): Promise<AuthContext | null> {\n    try {\n        const apiKey = await db.apiKey.findUnique({\n            where: {key},\n            select: {\n                id: true,\n                userId: true,\n                projectId: true,\n                permissions: true,\n                isRevoked: true,\n                expiresAt: true,\n                lastUsed: true,\n            }\n        });\n\n        if (!apiKey) {\n            return null;\n        }\n\n        // Check if key is revoked\n        if (apiKey.isRevoked) {\n            return null;\n        }\n\n        // Check if key is expired\n        if (apiKey.expiresAt && new Date() > apiKey.expiresAt) {\n            return null;\n        }\n\n        // Update lastUsed timestamp (non-blocking)\n        db.apiKey.update({\n            where: {id: apiKey.id},\n            data: {lastUsed: new Date()}\n        }).catch(err => console.error('Failed to update API key lastUsed:', err));\n\n        return {\n            userId: apiKey.userId,\n            apiKeyId: apiKey.id,\n            projectId: apiKey.projectId,\n            permissions: apiKey.permissions,\n            isApiKey: true,\n        };\n    } catch (error) {\n        console.error('API key verification failed:', error);\n        return null;\n    }\n}\n\n/**\n * Authenticate incoming request using either JWT or API key.\n * Returns auth context with user info, project scope, and permissions.\n * Accepts either a full NextRequest or a minimal AuthRequest interface.\n */\nexport async function authenticateRequest(request: AuthRequest | NextRequest): Promise<AuthContext | null> {\n    const auth = getTokenFromRequest(request);\n    if (!auth) {\n        return null;\n    }\n\n    if (auth.type === 'jwt') {\n        return authenticateWithJWT(auth.token);\n    }\n\n    return authenticateWithAPIKey(auth.token);\n}\n\n/**\n * Check if authenticated context has access to a specific project.\n * JWT tokens and global API keys have access to all projects.\n * Project-specific API keys only have access to their assigned project.\n */\nexport function hasProjectAccess(ctx: AuthContext, projectId: string): boolean {\n    // JWT tokens have global access\n    if (!ctx.isApiKey) {\n        return true;\n    }\n\n    // Global API keys (projectId = null) have access to all projects\n    if (ctx.projectId === null) {\n        return true;\n    }\n\n    // Project-specific keys only have access to their project\n    return ctx.projectId === projectId;\n}\n\n/**\n * Check if authenticated context has a specific permission.\n * JWT tokens have all permissions.\n * API keys are checked against their permission array.\n */\nexport function hasRequiredPermission(ctx: AuthContext, permission: ApiPermission): boolean {\n    // JWT tokens have all permissions\n    if (!ctx.isApiKey) {\n        return true;\n    }\n\n    return hasPermission(ctx.permissions, permission);\n}\n\n/**\n * Check if authenticated context has all required permissions.\n */\nexport function hasRequiredPermissions(ctx: AuthContext, permissions: ApiPermission[]): boolean {\n    // JWT tokens have all permissions\n    if (!ctx.isApiKey) {\n        return true;\n    }\n\n    return hasAllPermissions(ctx.permissions, permissions);\n}\n\n/**\n * Validate request has required permission and project access.\n * Throws error with appropriate status code if validation fails.\n */\nexport async function validateRequestAuth(\n    request: NextRequest,\n    options: {\n        projectId?: string;\n        permission?: ApiPermission;\n        permissions?: ApiPermission[];\n    } = {}\n): Promise<AuthContext> {\n    const ctx = await authenticateRequest(request);\n\n    if (!ctx) {\n        throw new Error('Authentication required');\n    }\n\n    // Check project access if projectId specified\n    if (options.projectId && !hasProjectAccess(ctx, options.projectId)) {\n        throw new Error('Access denied to this project');\n    }\n\n    // Check single permission if specified\n    if (options.permission && !hasRequiredPermission(ctx, options.permission)) {\n        throw new Error(`Missing required permission: ${options.permission}`);\n    }\n\n    // Check multiple permissions if specified\n    if (options.permissions && !hasRequiredPermissions(ctx, options.permissions)) {\n        throw new Error('Missing required permissions');\n    }\n\n    return ctx;\n}"
  },
  {
    "path": "lib/auth/authorization.ts",
    "content": "export function getTokenFromHeader(req: Request) {\n    const authHeader = req.headers.get('authorization');\n    if (!authHeader?.startsWith('Bearer ')) return null;\n    return authHeader.split(' ')[1];\n}"
  },
  {
    "path": "lib/auth/claim-validator.ts",
    "content": "/**\n * Claim validation for SSO/SAML providers\n * Allows dynamic validation of user claims/attributes from OAuth/SAML responses\n */\n\nexport interface ClaimRule {\n    claimName: string;\n    requiredValue: string;\n    caseSensitive?: boolean;\n}\n\nexport interface ClaimValidationResult {\n    allowed: boolean;\n    reason?: string;\n}\n\n/**\n * Validates user claims against required claim rules\n * @param userClaims - Claims from OAuth userInfo or SAML assertion\n * @param requiredClaims - JSON object containing claim rules (e.g., { \"organizations\": \"my-org\", \"role\": \"admin\" })\n * @returns Validation result with allowed status and reason\n */\nexport function validateClaims(\n    userClaims: Record<string, any>,\n    requiredClaims: Record<string, string> | null | undefined\n): ClaimValidationResult {\n    // If no required claims, allow all\n    if (!requiredClaims || Object.keys(requiredClaims).length === 0) {\n        return {\n            allowed: true,\n        };\n    }\n\n    // Check each required claim\n    for (const [claimName, requiredValue] of Object.entries(requiredClaims)) {\n        if (!requiredValue) continue; // Skip empty values\n\n        const userClaimValue = userClaims[claimName];\n\n        // Claim is missing\n        if (userClaimValue === undefined || userClaimValue === null) {\n            return {\n                allowed: false,\n                reason: `Missing required claim '${claimName}'. This SSO provider requires specific attributes that were not provided by your identity provider.`,\n            };\n        }\n\n        // Handle array claims (e.g., groups, organizations)\n        if (Array.isArray(userClaimValue)) {\n            const hasMatch = userClaimValue.some(val =>\n                String(val).toLowerCase() === String(requiredValue).toLowerCase()\n            );\n\n            if (!hasMatch) {\n                return {\n                    allowed: false,\n                    reason: `Claim '${claimName}' must contain '${requiredValue}', but got [${userClaimValue.join(', ')}]. Please contact your administrator if you believe you should have access.`,\n                };\n            }\n        } else {\n            // Handle string/number claims (case-insensitive comparison)\n            const userValueStr = String(userClaimValue).toLowerCase();\n            const requiredValueStr = String(requiredValue).toLowerCase();\n\n            if (userValueStr !== requiredValueStr) {\n                return {\n                    allowed: false,\n                    reason: `Claim '${claimName}' must be '${requiredValue}', but got '${userClaimValue}'. Please contact your administrator if you believe you should have access.`,\n                };\n            }\n        }\n    }\n\n    // All claims validated successfully\n    return {\n        allowed: true,\n    };\n}\n\n/**\n * Helper to safely parse required claims from JSON\n */\nexport function parseRequiredClaims(requiredClaimsJson: any): Record<string, string> | null {\n    if (!requiredClaimsJson) return null;\n\n    try {\n        if (typeof requiredClaimsJson === 'string') {\n            const parsed = JSON.parse(requiredClaimsJson);\n            return typeof parsed === 'object' ? parsed : null;\n        }\n\n        if (typeof requiredClaimsJson === 'object') {\n            return requiredClaimsJson;\n        }\n\n        return null;\n    } catch (error) {\n        console.error('Failed to parse required claims:', error);\n        return null;\n    }\n}\n"
  },
  {
    "path": "lib/auth/cli-auth.ts",
    "content": "import { nanoid } from 'nanoid';\nimport { db } from '@/lib/db';\n\n/**\n * Generate a temporary authorization code for CLI authentication\n * @param userId - The user ID to associate with the code\n * @param callbackUrl - The CLI callback URL\n * @returns Object containing the code and expiration time\n */\nexport async function generateCLIAuthCode(userId: string, callbackUrl: string) {\n    // Generate a secure random code\n    const code = nanoid(32);\n\n    // Set expiration to 10 minutes from now\n    const expiresAt = new Date();\n    expiresAt.setMinutes(expiresAt.getMinutes() + 10);\n\n    // Clean up any existing expired codes for this user\n    await db.cliAuthCode.deleteMany({\n        where: {\n            userId,\n            expiresAt: {\n                lt: new Date(),\n            },\n        },\n    });\n\n    // Create the new authorization code\n    const authCode = await db.cliAuthCode.create({\n        data: {\n            code,\n            userId,\n            callbackUrl,\n            expiresAt,\n        },\n    });\n\n    return {\n        code: authCode.code,\n        expires: authCode.expiresAt.getTime(),\n        expiresAt: authCode.expiresAt,\n    };\n}\n\n/**\n * Validate and consume a CLI authorization code\n * @param code - The authorization code to validate\n * @returns User information if valid, null if invalid\n */\nexport async function validateCLIAuthCode(code: string) {\n    const authCode = await db.cliAuthCode.findUnique({\n        where: { code },\n        include: {\n            user: {\n                select: {\n                    id: true,\n                    email: true,\n                    name: true,\n                    role: true,\n                },\n            },\n        },\n    });\n\n    if (!authCode) {\n        return null;\n    }\n\n    // Check if expired\n    if (authCode.expiresAt < new Date()) {\n        // Clean up expired code\n        await db.cliAuthCode.delete({\n            where: { code },\n        });\n        return null;\n    }\n\n    // Check if already used\n    if (authCode.usedAt) {\n        return null;\n    }\n\n    return {\n        code: authCode.code,\n        user: authCode.user,\n        callbackUrl: authCode.callbackUrl,\n        expiresAt: authCode.expiresAt,\n    };\n}\n\n/**\n * Mark a CLI authorization code as used\n * @param code - The authorization code to mark as used\n */\nexport async function markCLIAuthCodeAsUsed(code: string) {\n    await db.cliAuthCode.update({\n        where: { code },\n        data: { usedAt: new Date() },\n    });\n}\n\n/**\n * Clean up expired CLI authorization codes\n * This should be called periodically to prevent database bloat\n */\nexport async function cleanupExpiredCLIAuthCodes() {\n    const result = await db.cliAuthCode.deleteMany({\n        where: {\n            OR: [\n                {\n                    expiresAt: {\n                        lt: new Date(),\n                    },\n                },\n                {\n                    usedAt: {\n                        not: null,\n                    },\n                    createdAt: {\n                        lt: new Date(Date.now() - 24 * 60 * 60 * 1000), // 24 hours ago\n                    },\n                },\n            ],\n        },\n    });\n\n    return result.count;\n}\n\n/**\n * Get all active CLI authorization codes for a user\n * @param userId - The user ID\n * @returns Array of active authorization codes\n */\nexport async function getActiveCLIAuthCodes(userId: string) {\n    return await db.cliAuthCode.findMany({\n        where: {\n            userId,\n            expiresAt: {\n                gt: new Date(),\n            },\n            usedAt: null,\n        },\n        select: {\n            code: true,\n            callbackUrl: true,\n            expiresAt: true,\n            createdAt: true,\n        },\n        orderBy: {\n            createdAt: 'desc',\n        },\n    });\n}\n\n/**\n * Revoke all active CLI authorization codes for a user\n * @param userId - The user ID\n */\nexport async function revokeAllCLIAuthCodes(userId: string) {\n    const result = await db.cliAuthCode.deleteMany({\n        where: {\n            userId,\n            usedAt: null,\n        },\n    });\n\n    return result.count;\n}"
  },
  {
    "path": "lib/auth/email-domain-validator.ts",
    "content": "/**\n * Email domain validation for SSO/SAML providers\n */\n\nexport interface DomainRestrictionConfig {\n    allowedEmailDomains: string[];\n    blockExistingUsers: boolean;\n}\n\nexport interface DomainValidationResult {\n    allowed: boolean;\n    isNewUser: boolean;\n    reason?: string;\n}\n\n/**\n * Extracts the domain from an email address (case-insensitive)\n * @param email - The email address\n * @returns The domain in lowercase, or null if invalid\n */\nexport function extractEmailDomain(email: string): string | null {\n    const match = email.match(/@(.+)$/);\n    if (!match) return null;\n    return match[1].toLowerCase();\n}\n\n/**\n * Checks if an email domain is allowed based on provider configuration\n * @param email - The user's email address\n * @param config - The domain restriction configuration\n * @param userExists - Whether the user already exists in the database\n * @returns Validation result with allowed status and reason\n */\nexport function validateEmailDomain(\n    email: string,\n    config: DomainRestrictionConfig,\n    userExists: boolean\n): DomainValidationResult {\n    // If no domain restrictions, allow all\n    if (!config.allowedEmailDomains || config.allowedEmailDomains.length === 0) {\n        return {\n            allowed: true,\n            isNewUser: !userExists,\n        };\n    }\n\n    const domain = extractEmailDomain(email);\n    if (!domain) {\n        return {\n            allowed: false,\n            isNewUser: !userExists,\n            reason: 'Invalid email format',\n        };\n    }\n\n    // Check if domain is in allowed list (case-insensitive)\n    const normalizedAllowedDomains = config.allowedEmailDomains.map(d => d.toLowerCase());\n    const isDomainAllowed = normalizedAllowedDomains.includes(domain);\n\n    if (!isDomainAllowed) {\n        return {\n            allowed: false,\n            isNewUser: !userExists,\n            reason: `Email domain '@${domain}' is not allowed for this SSO provider. Allowed domains: ${config.allowedEmailDomains.join(', ')}`,\n        };\n    }\n\n    // Domain is allowed, now check if we should block existing users\n    if (userExists && config.blockExistingUsers) {\n        return {\n            allowed: false,\n            isNewUser: false,\n            reason: `This SSO provider is configured to only allow new user registration. The email '${email}' is already registered.`,\n        };\n    }\n\n    // All checks passed\n    return {\n        allowed: true,\n        isNewUser: !userExists,\n    };\n}\n"
  },
  {
    "path": "lib/auth/jwt-utils.ts",
    "content": ""
  },
  {
    "path": "lib/auth/oauth.ts",
    "content": "import { OAuthUserInfo } from '@/lib/types/oauth';\nimport { db } from '@/lib/db';\nimport { generateTokens } from '@/lib/auth/tokens';\nimport { Role } from '@prisma/client';\nimport { validateEmailDomain } from '@/lib/auth/email-domain-validator';\nimport { validateClaims, parseRequiredClaims } from '@/lib/auth/claim-validator';\n\nexport async function getOAuthProviders(includeDisabled = false) {\n    const providers = await db.oAuthProvider.findMany({\n        where: includeDisabled ? {} : { enabled: true },\n        orderBy: { name: 'asc' }\n    });\n\n    // Add a normalized name property for URL slug generation\n    return providers.map(provider => ({\n        ...provider,\n        // Add a normalized version of the name for URL paths\n        urlName: provider.name.toLowerCase().replace(/\\s+/g, '-')\n    }));\n}\n\nexport async function getDefaultProvider() {\n    const provider = await db.oAuthProvider.findFirst({\n        where: { isDefault: true, enabled: true }\n    });\n\n    return provider;\n}\n\nexport async function getOAuthLoginUrl(providerId: string, state?: string) {\n    const provider = await db.oAuthProvider.findUnique({\n        where: { id: providerId }\n    });\n\n    if (!provider) {\n        throw new Error('Provider not found');\n    }\n\n    const params = new URLSearchParams({\n        client_id: provider.clientId,\n        redirect_uri: provider.callbackUrl,\n        response_type: 'code',\n        scope: provider.scopes.join(' ')\n    });\n\n    if (state) {\n        params.append('state', state);\n    }\n\n    return `${provider.authorizationUrl}?${params.toString()}`;\n}\n\nexport async function exchangeCodeForToken(providerId: string, code: string) {\n    try {\n        const provider = await db.oAuthProvider.findUnique({\n            where: { id: providerId }\n        });\n\n        if (!provider) {\n            throw new Error(`Provider not found with ID: ${providerId}`);\n        }\n\n        console.log('Exchanging code for token with provider:', {\n            providerName: provider.name,\n            authorizationUrl: provider.authorizationUrl,\n            tokenUrl: provider.tokenUrl,\n            callbackUrl: provider.callbackUrl\n        });\n\n        // Construct the token request\n        const tokenRequestBody = new URLSearchParams({\n            client_id: provider.clientId,\n            client_secret: provider.clientSecret,\n            code,\n            redirect_uri: provider.callbackUrl,\n            grant_type: 'authorization_code'\n        }).toString();\n\n        console.log('Token request parameters:', {\n            clientIdLength: provider.clientId.length,\n            clientSecretLength: provider.clientSecret.length,\n            codeLength: code.length,\n            redirectUri: provider.callbackUrl\n        });\n\n        // Make the token request\n        const response = await fetch(provider.tokenUrl, {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/x-www-form-urlencoded',\n                'Accept': 'application/json'\n            },\n            body: tokenRequestBody\n        });\n\n        // Log response status\n        console.log('Token response status:', response.status);\n\n        // Handle non-OK responses with detailed error\n        if (!response.ok) {\n            const errorText = await response.text();\n            console.error('Token exchange failed:', {\n                status: response.status,\n                statusText: response.statusText,\n                errorText,\n                providerName: provider.name\n            });\n            throw new Error(`Failed to exchange code for token: ${response.status} ${response.statusText} - ${errorText}`);\n        }\n\n        // Parse response\n        const responseData = await response.json();\n\n        // Log token response (with sensitive data masked)\n        console.log('Token response received:', {\n            responseKeys: Object.keys(responseData),\n            accessTokenReceived: !!responseData.access_token,\n            refreshTokenReceived: !!responseData.refresh_token,\n            expiresIn: responseData.expires_in,\n            tokenType: responseData.token_type,\n            userInfoReceived: !!responseData.userInfo\n        });\n\n        // Add additional error checking for missing access token\n        if (!responseData.access_token) {\n            throw new Error('Access token missing from OAuth provider response');\n        }\n\n        // If the response includes an id_token (common in OpenID Connect), decode it to get user info\n        if (responseData.id_token) {\n            try {\n                console.log('ID token found, attempting to decode');\n                // Simple base64 decoding for JWT (for demonstration)\n                const idTokenParts = responseData.id_token.split('.');\n                if (idTokenParts.length >= 2) {\n                    const payload = JSON.parse(\n                        Buffer.from(idTokenParts[1], 'base64').toString()\n                    );\n                    console.log('Decoded ID token payload:', {\n                        sub: payload.sub,\n                        email: payload.email,\n                        hasName: !!payload.name\n                    });\n                    // Add userInfo to the response\n                    responseData.userInfo = payload;\n                }\n            } catch (decodeError) {\n                console.warn('Failed to decode ID token:', decodeError);\n                // Continue without userInfo from ID token\n            }\n        }\n\n        return responseData;\n    } catch (error) {\n        console.error('Token exchange error:', {\n            message: (error as Error).message,\n            stack: (error as Error).stack\n        });\n        throw error;\n    }\n}\n\nexport async function fetchUserInfo(providerId: string, accessToken: string): Promise<OAuthUserInfo> {\n    const provider = await db.oAuthProvider.findUnique({\n        where: { id: providerId }\n    });\n\n    if (!provider) {\n        throw new Error('Provider not found');\n    }\n\n    const response = await fetch(provider.userInfoUrl, {\n        headers: {\n            Authorization: `Bearer ${accessToken}`\n        }\n    });\n\n    if (!response.ok) {\n        console.error('User info fetch failed:', await response.text());\n        throw new Error('Failed to fetch user info');\n    }\n\n    return await response.json();\n}\n\nexport async function handleOAuthCallback(providerName: string, code: string) {\n    // Find provider by name\n    const provider = await db.oAuthProvider.findFirst({\n        where: {\n            name: {\n                equals: providerName,\n                mode: 'insensitive'\n            },\n            enabled: true\n        }\n    });\n\n    if (!provider) {\n        throw new Error(`Provider not found: ${providerName}`);\n    }\n\n    // Use the provider ID for the rest of the process\n    const providerId = provider.id;\n\n    try {\n        // 1. Exchange code for token\n        const tokenResponse = await exchangeCodeForToken(providerId, code);\n        const { access_token, refresh_token, expires_in, userInfo: tokenUserInfo } = tokenResponse;\n\n        // Try to get user info from token response first\n        let userInfo = tokenUserInfo;\n\n        // If userInfo is not in token response, fetch it separately\n        if (!userInfo) {\n            try {\n                console.log('User info not found in token response, fetching from userInfo endpoint...');\n                userInfo = await fetchUserInfo(providerId, access_token);\n            } catch (userInfoError) {\n                console.error('Failed to fetch user info:', userInfoError);\n                throw new Error('Failed to retrieve user information from OAuth provider');\n            }\n        }\n\n        // Validate user info\n        if (!userInfo) {\n            throw new Error('No user information received from OAuth provider');\n        }\n\n        const userDetails = {\n            id: userInfo.sub || userInfo.id,\n            email: userInfo.email,\n            name: userInfo.name || userInfo.email\n        };\n\n        if (!userDetails.email) {\n            throw new Error('Email is required from OAuth provider');\n        }\n\n        // 3. Find or create user and establish connection\n        const existingConnection = await db.oAuthConnection.findUnique({\n            where: {\n                providerId_providerUserId: {\n                    providerId,\n                    providerUserId: userDetails.id\n                }\n            },\n            include: {\n                user: true\n            }\n        });\n\n        // If connection exists, use existing user\n        if (existingConnection) {\n            const expiresAt = expires_in ? new Date(Date.now() + expires_in * 1000) : null;\n\n            // Update connection with new tokens\n            await db.oAuthConnection.update({\n                where: { id: existingConnection.id },\n                data: {\n                    accessToken: access_token,\n                    refreshToken: refresh_token || null,\n                    expiresAt\n                }\n            });\n\n            // Update last login timestamp\n            await db.user.update({\n                where: { id: existingConnection.user.id },\n                data: { lastLoginAt: new Date() }\n            });\n\n            // Generate app tokens\n            const tokens = await generateTokens(existingConnection.user.id);\n\n            return {\n                user: existingConnection.user,\n                ...tokens\n            };\n        }\n\n        // Check if user exists with the same email\n        const existingUser = await db.user.findUnique({\n            where: { email: userDetails.email }\n        });\n\n        // Validate email domain restrictions\n        const validation = validateEmailDomain(\n            userDetails.email,\n            {\n                allowedEmailDomains: provider.allowedEmailDomains,\n                blockExistingUsers: provider.blockExistingUsers,\n            },\n            !!existingUser\n        );\n\n        if (!validation.allowed) {\n            throw new Error(validation.reason || 'Email domain not allowed for this SSO provider');\n        }\n\n        // Validate required claims\n        const requiredClaims = parseRequiredClaims(provider.requiredClaims);\n        const claimValidation = validateClaims(userInfo, requiredClaims);\n\n        if (!claimValidation.allowed) {\n            throw new Error(claimValidation.reason || 'Required claims validation failed');\n        }\n\n        let user;\n\n        if (existingUser) {\n            // Link existing user to the OAuth provider\n            user = existingUser;\n        } else {\n            // Create new user\n            user = await db.user.create({\n                data: {\n                    email: userDetails.email,\n                    name: userDetails.name || null,\n                    password: '', // Empty password for OAuth users\n                    role: Role.STAFF, // Default to STAFF role for new OAuth users\n                    lastLoginAt: new Date()\n                }\n            });\n\n            // Create default settings for new user\n            await db.settings.create({\n                data: {\n                    userId: user.id,\n                    theme: 'light'\n                }\n            });\n        }\n\n        // Create or update OAuth connection using upsert\n        const expiresAt = expires_in ? new Date(Date.now() + expires_in * 1000) : null;\n        await db.oAuthConnection.upsert({\n            where: {\n                providerId_userId: {\n                    providerId,\n                    userId: user.id\n                }\n            },\n            create: {\n                providerId,\n                userId: user.id,\n                providerUserId: userDetails.id,\n                accessToken: access_token,\n                refreshToken: refresh_token || null,\n                expiresAt\n            },\n            update: {\n                providerUserId: userDetails.id,\n                accessToken: access_token,\n                refreshToken: refresh_token || null,\n                expiresAt,\n                updatedAt: new Date()\n            }\n        });\n\n        // Generate app tokens\n        const tokens = await generateTokens(user.id);\n\n        return {\n            user,\n            ...tokens\n        };\n    } catch (error) {\n        console.error('OAuth Callback Error:', {\n            providerName,\n            errorMessage: (error as Error).message,\n            stack: (error as Error).stack\n        });\n        throw error;\n    }\n}"
  },
  {
    "path": "lib/auth/password.ts",
    "content": "import { hash, compare } from 'bcryptjs';\n\nexport async function hashPassword(password: string) {\n    return hash(password, 12);\n}\n\nexport async function verifyPassword(password: string, hashedPassword: string) {\n    return compare(password, hashedPassword);\n}"
  },
  {
    "path": "lib/auth/providers/easypanel/auto-setup.ts",
    "content": "// lib/services/auth/providers/easypanel/auto-setup.ts\nimport { nanoid } from 'nanoid';\nimport {\n    EasypanelApiError,\n    createEasypanelApiClient,\n    isAutoOAuthAvailable\n} from './client';\nimport { setupEasypanelProvider } from '@/lib/auth/providers/easypanel';\n\nexport interface AutoSetupResult {\n    success: boolean;\n    client?: {\n        id: string;\n        name: string;\n        clientId: string;\n        clientSecret: string;\n        redirectUri: string;\n    };\n    error?: string;\n    details?: string;\n}\n\nexport interface AutoSetupOptions {\n    appName?: string;\n    persistent?: boolean;\n}\n\n/**\n * Automatically create and configure OAuth client with the remote server\n */\nexport async function performAutoOAuthSetup(\n    options: AutoSetupOptions = {}\n): Promise<AutoSetupResult> {\n    // console.log('🦖 performAutoOAuthSetup called with options:', options);\n\n    // Check if auto setup is available\n    if (!isAutoOAuthAvailable()) {\n        // console.log('🦖 Auto setup not available - missing env vars');\n        return {\n            success: false,\n            error: 'Auto OAuth setup not available',\n            details: 'Missing CHR_EPOA2_SERV_URL or CHR_EPOA2_SERV_API_KEY environment variables'\n        };\n    }\n\n    // console.log('🦖 Environment variables check passed');\n    // console.log('🦖 Server URL:', process.env.CHR_EPOA2_SERV_URL);\n    // console.log('🦖 API Key:', process.env.CHR_EPOA2_SERV_API_KEY ? 'SET' : 'NOT SET');\n\n    const apiClient = createEasypanelApiClient();\n    if (!apiClient) {\n        console.log('🦖 Failed to create API client');\n        return {\n            success: false,\n            error: 'Failed to create API client',\n            details: 'Could not initialize OAuth server API client'\n        };\n    }\n\n    try {\n        // Step 1: Test connection to OAuth server\n        // console.log('🦖 Testing connection to OAuth server...');\n        const connectionTest = await apiClient.testConnection();\n        if (!connectionTest.success) {\n            // console.log('🦖 Connection test failed:', connectionTest.error);\n            return {\n                success: false,\n                error: 'Connection to OAuth server failed',\n                details: connectionTest.error\n            };\n        }\n        console.log('🦖 Connection test successful');\n\n        // Step 2: Generate client configuration\n        const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';\n        const clientName = options.appName || `Changerawr-${nanoid(8)}`;\n\n        // Fixed redirect URI for Easypanel provider - cannot be changed\n        const redirectUri = `${appUrl}/api/auth/oauth/callback/easypanel`;\n\n        // Required scopes for OAuth authentication\n        const allowedScopes = ['openid', 'profile', 'email'];\n\n        const clientData = {\n            name: clientName,\n            redirectUris: [redirectUri],\n            allowedScopes,\n            persistent: options.persistent ?? true\n        };\n\n        console.log('🦖 Creating OAuth client with data:', clientData);\n\n        // Step 3: Create OAuth client on remote server\n        const createdClient = await apiClient.createClient(clientData);\n\n        console.log('🦖 Client created successfully:', {\n            id: createdClient.id,\n            name: createdClient.name,\n            hasSecret: !!createdClient.secret\n        });\n\n        if (!createdClient.secret) {\n            // console.log('🦖 No client secret in response');\n            return {\n                success: false,\n                error: 'Client created but no secret returned',\n                details: 'The OAuth server did not return a client secret'\n            };\n        }\n\n        // Step 4: Configure local OAuth provider using our existing setup function\n        try {\n            const serverUrl = process.env.CHR_EPOA2_SERV_URL;\n            if (!serverUrl) {\n                throw new Error('CHR_EPOA2_SERV_URL not configured');\n            }\n\n            // console.log('🦖 Setting up local OAuth provider...');\n            await setupEasypanelProvider({\n                baseUrl: serverUrl,\n                clientId: createdClient.id,\n                clientSecret: createdClient.secret\n            });\n\n            // console.log('🦖 Local OAuth provider setup complete');\n\n            return {\n                success: true,\n                client: {\n                    id: createdClient.id,\n                    name: createdClient.name,\n                    clientId: createdClient.id,\n                    clientSecret: createdClient.secret,\n                    redirectUri: redirectUri\n                }\n            };\n\n        } catch (setupError) {\n            console.error('🦖 Local setup failed:', setupError);\n            // If local setup fails, try to clean up the remote client\n            try {\n                console.log('🦖 Attempting to cleanup remote client...');\n                await apiClient.deleteClient(createdClient.id);\n                console.log('🦖 Remote client cleanup successful');\n            } catch (cleanupError) {\n                console.warn('🦖 Failed to cleanup remote client after setup failure:', cleanupError);\n            }\n\n            return {\n                success: false,\n                error: 'Failed to configure local OAuth provider',\n                details: setupError instanceof Error ? setupError.message : 'Unknown setup error'\n            };\n        }\n\n    } catch (error) {\n        console.error('🦖 Auto setup error:', error);\n\n        if (error instanceof EasypanelApiError) {\n            return {\n                success: false,\n                error: `OAuth server error: ${error.message}`,\n                details: error.details\n            };\n        }\n\n        return {\n            success: false,\n            error: 'Unexpected error during auto setup',\n            details: error instanceof Error ? error.message : 'Unknown error'\n        };\n    }\n}\n\n/**\n * Verify that auto setup configuration is working\n */\nexport async function verifyAutoSetupConfiguration(): Promise<{\n    available: boolean;\n    connected: boolean;\n    error?: string;\n}> {\n    const available = isAutoOAuthAvailable();\n\n    if (!available) {\n        return {\n            available: false,\n            connected: false,\n            error: 'Environment variables not configured'\n        };\n    }\n\n    const apiClient = createEasypanelApiClient();\n    if (!apiClient) {\n        return {\n            available: true,\n            connected: false,\n            error: 'Failed to create API client'\n        };\n    }\n\n    const connectionTest = await apiClient.testConnection();\n\n    return {\n        available: true,\n        connected: connectionTest.success,\n        error: connectionTest.error\n    };\n}\n\n/**\n * Get OAuth server information for display purposes\n */\nexport function getOAuthServerInfo(): {\n    serverUrl?: string;\n    hasApiKey: boolean;\n    isConfigured: boolean;\n} {\n    const serverUrl = process.env.CHR_EPOA2_SERV_URL;\n    const hasApiKey = !!process.env.CHR_EPOA2_SERV_API_KEY;\n    const isConfigured = isAutoOAuthAvailable();\n\n    return {\n        serverUrl,\n        hasApiKey,\n        isConfigured\n    };\n}"
  },
  {
    "path": "lib/auth/providers/easypanel/client.ts",
    "content": "// lib/services/auth/providers/easypanel/client.ts\nimport { z } from 'zod';\n\n// Response schemas for validation\nconst ClientResponseSchema = z.object({\n    success: z.boolean(),\n    data: z.object({\n        id: z.string(),\n        name: z.string(),\n        secret: z.string().optional(),\n        redirectUris: z.array(z.string()),\n        allowedScopes: z.array(z.string()),\n        createdAt: z.string(),\n        persistent: z.boolean()\n    }).optional(),\n    client: z.object({\n        id: z.string(),\n        name: z.string(),\n        secret: z.string().optional(),\n        redirectUris: z.array(z.string()),\n        allowedScopes: z.array(z.string()),\n        createdAt: z.string(),\n        persistent: z.boolean()\n    }).optional(),\n    error: z.string().optional(),\n    details: z.string().optional()\n});\n\nconst ClientsListResponseSchema = z.object({\n    success: z.boolean(),\n    data: z.array(z.object({\n        id: z.string(),\n        name: z.string(),\n        redirectUris: z.array(z.string()),\n        allowedScopes: z.array(z.string()),\n        createdAt: z.string(),\n        persistent: z.boolean()\n    })).optional(),\n    count: z.number().optional(),\n    error: z.string().optional(),\n    details: z.string().optional()\n});\n\nexport interface CreateClientRequest {\n    name: string;\n    redirectUris: string[];\n    allowedScopes: string[];\n    persistent?: boolean;\n}\n\nexport interface UpdateClientRequest {\n    name?: string;\n    redirectUris?: string[];\n    allowedScopes?: string[];\n    persistent?: boolean;\n}\n\nexport interface EasypanelClient {\n    id: string;\n    name: string;\n    secret?: string;\n    redirectUris: string[];\n    allowedScopes: string[];\n    createdAt: string;\n    persistent: boolean;\n}\n\nexport class EasypanelApiError extends Error {\n    constructor(\n        message: string,\n        public status: number,\n        public details?: string\n    ) {\n        super(message);\n        this.name = 'EasypanelApiError';\n    }\n}\n\nexport class EasypanelApiClient {\n    private baseUrl: string;\n    private apiKey: string;\n\n    constructor(baseUrl: string, apiKey: string) {\n        this.baseUrl = baseUrl.replace(/\\/$/, ''); // Remove trailing slash\n        this.apiKey = apiKey;\n    }\n\n    private async makeRequest<T>(\n        endpoint: string,\n        options: RequestInit = {}\n    ): Promise<T> {\n        const url = `${this.baseUrl}${endpoint}`;\n\n        const requestOptions = {\n            ...options,\n            headers: {\n                'Content-Type': 'application/json',\n                'X-API-Key': this.apiKey,\n                ...options.headers,\n            },\n        };\n\n        console.log('🦖 Making request to:', url);\n        console.log('🦖 Request method:', options.method || 'GET');\n        console.log('🦖 Request headers:', {\n            'Content-Type': 'application/json',\n            'X-API-Key': this.apiKey ? `${this.apiKey.substring(0, 8)}...` : 'not set'\n        });\n\n        if (options.body) {\n            console.log('🦖 Request body:', options.body);\n        }\n\n        const response = await fetch(url, requestOptions);\n\n        console.log('🦖 Response status:', response.status);\n        console.log('🦖 Response ok:', response.ok);\n\n        if (!response.ok) {\n            let errorMessage = `HTTP ${response.status}: ${response.statusText}`;\n            let responseText = '';\n\n            try {\n                responseText = await response.text();\n                console.log('🦖 Error response body:', responseText);\n\n                const errorData = JSON.parse(responseText);\n                if (errorData.error) {\n                    errorMessage = errorData.error;\n                }\n                if (errorData.details) {\n                    errorMessage += ` - ${errorData.details}`;\n                }\n            } catch (parseError) {\n                console.log('🦖 Could not parse error response as JSON:', parseError);\n                if (responseText) {\n                    errorMessage += ` - ${responseText}`;\n                }\n            }\n\n            throw new EasypanelApiError(errorMessage, response.status);\n        }\n\n        const responseText = await response.text();\n        console.log('🦖 Response body:', responseText);\n\n        try {\n            const data = JSON.parse(responseText);\n            return data as T;\n        } catch (parseError) {\n            console.error('🦖 Failed to parse response JSON:', parseError);\n            throw new EasypanelApiError('Invalid JSON response from server', 500);\n        }\n    }\n\n    /**\n     * Test connection to the OAuth server\n     */\n    async testConnection(): Promise<{ success: boolean; error?: string }> {\n        try {\n            const response = await this.makeRequest<typeof ClientsListResponseSchema._type>(\n                '/api/clients'\n            );\n\n            const parsed = ClientsListResponseSchema.safeParse(response);\n            if (!parsed.success) {\n                return {\n                    success: false,\n                    error: 'Invalid response format from OAuth server'\n                };\n            }\n\n            if (!parsed.data.success) {\n                return {\n                    success: false,\n                    error: parsed.data.error || 'API returned failure status'\n                };\n            }\n\n            return { success: true };\n        } catch (error) {\n            if (error instanceof EasypanelApiError) {\n                return {\n                    success: false,\n                    error: error.message\n                };\n            }\n\n            return {\n                success: false,\n                error: error instanceof Error ? error.message : 'Unknown error occurred'\n            };\n        }\n    }\n\n    /**\n     * List all OAuth clients\n     */\n    async listClients(): Promise<EasypanelClient[]> {\n        const response = await this.makeRequest<typeof ClientsListResponseSchema._type>(\n            '/api/clients'\n        );\n\n        const parsed = ClientsListResponseSchema.parse(response);\n\n        if (!parsed.success || !parsed.data) {\n            throw new EasypanelApiError(\n                parsed.error || 'Failed to list clients',\n                400,\n                parsed.details\n            );\n        }\n\n        return parsed.data;\n    }\n\n    /**\n     * Create a new OAuth client\n     */\n    async createClient(clientData: CreateClientRequest): Promise<EasypanelClient> {\n        const response = await this.makeRequest<typeof ClientResponseSchema._type>(\n            '/api/clients',\n            {\n                method: 'POST',\n                body: JSON.stringify(clientData),\n            }\n        );\n\n        console.log('🦖 Parsing createClient response:', response);\n\n        const parsed = ClientResponseSchema.parse(response);\n\n        if (!parsed.success) {\n            throw new EasypanelApiError(\n                parsed.error || 'Failed to create client',\n                400,\n                parsed.details\n            );\n        }\n\n        // The response has the client data in the \"client\" field, not \"data\"\n        const clientData_response = parsed.client || parsed.data;\n\n        if (!clientData_response) {\n            throw new EasypanelApiError(\n                'No client data in response',\n                400,\n                'Server returned success but no client data'\n            );\n        }\n\n        console.log('🦖 Successfully parsed client data:', clientData_response);\n\n        return clientData_response;\n    }\n\n    /**\n     * Get a specific client by ID\n     */\n    async getClient(clientId: string): Promise<EasypanelClient> {\n        const response = await this.makeRequest<typeof ClientResponseSchema._type>(\n            `/api/clients/${clientId}`\n        );\n\n        const parsed = ClientResponseSchema.parse(response);\n\n        if (!parsed.success || !parsed.data) {\n            throw new EasypanelApiError(\n                parsed.error || 'Failed to get client',\n                404,\n                parsed.details\n            );\n        }\n\n        return parsed.data;\n    }\n\n    /**\n     * Update an existing client\n     */\n    async updateClient(clientId: string, updates: UpdateClientRequest): Promise<EasypanelClient> {\n        const response = await this.makeRequest<typeof ClientResponseSchema._type>(\n            `/api/clients/${clientId}`,\n            {\n                method: 'PUT',\n                body: JSON.stringify(updates),\n            }\n        );\n\n        const parsed = ClientResponseSchema.parse(response);\n\n        if (!parsed.success || !parsed.data) {\n            throw new EasypanelApiError(\n                parsed.error || 'Failed to update client',\n                400,\n                parsed.details\n            );\n        }\n\n        return parsed.data;\n    }\n\n    /**\n     * Delete a client\n     */\n    async deleteClient(clientId: string): Promise<void> {\n        await this.makeRequest(\n            `/api/clients/${clientId}`,\n            {\n                method: 'DELETE',\n            }\n        );\n    }\n\n    /**\n     * Regenerate client secret\n     */\n    async regenerateSecret(clientId: string): Promise<{ newSecret: string }> {\n        const response = await this.makeRequest<{\n            success: boolean;\n            data?: { newSecret: string };\n            error?: string;\n        }>(\n            `/api/clients/${clientId}/regenerate-secret`,\n            {\n                method: 'POST',\n            }\n        );\n\n        if (!response.success || !response.data) {\n            throw new EasypanelApiError(\n                response.error || 'Failed to regenerate secret',\n                400\n            );\n        }\n\n        return response.data;\n    }\n}\n\n/**\n * Create an API client instance from environment variables\n */\nexport function createEasypanelApiClient(): EasypanelApiClient | null {\n    const baseUrl = process.env.CHR_EPOA2_SERV_URL;\n    const apiKey = process.env.CHR_EPOA2_SERV_API_KEY;\n\n    if (!baseUrl || !apiKey) {\n        return null;\n    }\n\n    return new EasypanelApiClient(baseUrl, apiKey);\n}\n\n/**\n * Check if automatic OAuth setup is available\n */\nexport function isAutoOAuthAvailable(): boolean {\n    return !!(process.env.CHR_EPOA2_SERV_URL && process.env.CHR_EPOA2_SERV_API_KEY);\n}"
  },
  {
    "path": "lib/auth/providers/easypanel.ts",
    "content": "import { db } from '@/lib/db';\n\nexport interface EasypanelProviderConfig {\n    baseUrl: string;\n    clientId: string;\n    clientSecret: string;\n}\n\nexport async function setupEasypanelProvider(config: EasypanelProviderConfig) {\n    const { baseUrl, clientId, clientSecret } = config;\n\n    // Normalize base URL (remove trailing slash)\n    const normalizedBaseUrl = baseUrl.endsWith('/')\n        ? baseUrl.slice(0, -1)\n        : baseUrl;\n\n    // Get app URL for callback\n    const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';\n\n    // First, check if an Easypanel provider already exists\n    const existingProvider = await db.oAuthProvider.findFirst({\n        where: {\n            name: 'Easypanel'\n        }\n    });\n\n    // Create callback URL using the provider name\n    const callbackUrl = `${appUrl}/api/auth/oauth/callback/easypanel`;\n\n    if (existingProvider) {\n        // Update existing provider\n        return await db.oAuthProvider.update({\n            where: {\n                id: existingProvider.id\n            },\n            data: {\n                clientId,\n                clientSecret,\n                authorizationUrl: `${normalizedBaseUrl}/oauth/authorize`,\n                tokenUrl: `${normalizedBaseUrl}/oauth/token`,\n                userInfoUrl: `${normalizedBaseUrl}/oauth/userinfo`,\n                callbackUrl,\n                enabled: true\n            }\n        });\n    } else {\n        // Create new provider\n        return await db.oAuthProvider.create({\n            data: {\n                name: 'Easypanel',\n                clientId,\n                clientSecret,\n                authorizationUrl: `${normalizedBaseUrl}/oauth/authorize`,\n                tokenUrl: `${normalizedBaseUrl}/oauth/token`,\n                userInfoUrl: `${normalizedBaseUrl}/oauth/userinfo`,\n                callbackUrl,\n                scopes: ['openid', 'profile', 'email'],\n                enabled: true,\n                isDefault: true\n            }\n        });\n    }\n}"
  },
  {
    "path": "lib/auth/providers/pocketid.ts",
    "content": "import {db} from '@/lib/db';\n\nexport interface PocketIDProviderConfig {\n    baseUrl: string;\n    clientId: string;\n    clientSecret: string;\n}\n\nexport async function setupPocketIDProvider(config: PocketIDProviderConfig) {\n    const {baseUrl, clientId, clientSecret} = config;\n\n    // Normalize base URL (remove trailing slash)\n    const normalizedBaseUrl = baseUrl.endsWith('/')\n        ? baseUrl.slice(0, -1)\n        : baseUrl;\n\n    // Get app URL for callback\n    const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';\n\n    // First, check if a PocketID provider already exists\n    const existingProvider = await db.oAuthProvider.findFirst({\n        where: {\n            name: 'PocketID'\n        }\n    });\n\n    // Create callback URL using the provider name\n    const callbackUrl = `${appUrl}/api/auth/oauth/callback/pocketid`;\n\n    if (existingProvider) {\n        // Update existing provider\n        return db.oAuthProvider.update({\n            where: {\n                id: existingProvider.id\n            },\n            data: {\n                clientId,\n                clientSecret,\n                authorizationUrl: `${normalizedBaseUrl}/authorize`,\n                tokenUrl: `${normalizedBaseUrl}/api/oidc/token`,\n                userInfoUrl: `${normalizedBaseUrl}/api/oidc/userinfo`,\n                callbackUrl,\n                enabled: true\n            }\n        });\n    } else {\n        // Create new provider\n        return db.oAuthProvider.create({\n            data: {\n                name: 'PocketID',\n                clientId,\n                clientSecret,\n                authorizationUrl: `${normalizedBaseUrl}/authorize`,\n                tokenUrl: `${normalizedBaseUrl}/api/oidc/token`,\n                userInfoUrl: `${normalizedBaseUrl}/api/oidc/userinfo`,\n                callbackUrl,\n                scopes: ['openid', 'profile', 'email'],\n                enabled: true,\n                isDefault: true\n            }\n        });\n    }\n}"
  },
  {
    "path": "lib/auth/saml.ts",
    "content": "import { SAML } from '@node-saml/node-saml';\nimport { db } from '@/lib/db';\nimport { generateTokens } from '@/lib/auth/tokens';\nimport { Role } from '@prisma/client';\nimport { SAMLUserInfo } from '@/lib/types/saml';\nimport { validateEmailDomain } from '@/lib/auth/email-domain-validator';\nimport { validateClaims, parseRequiredClaims } from '@/lib/auth/claim-validator';\n\nfunction getAppUrl(): string {\n    return process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';\n}\n\nfunction buildSAMLInstance(provider: {\n    ssoUrl: string;\n    certificate: string;\n    spEntityId?: string | null;\n    nameIdFormat: string;\n    name: string;\n}) {\n    const appUrl = getAppUrl();\n    const providerSlug = provider.name.toLowerCase().replace(/\\s+/g, '-');\n\n    // Normalize certificate: strip PEM headers if present, then re-wrap\n    let cert = provider.certificate.trim();\n    cert = cert\n        .replace(/-----BEGIN CERTIFICATE-----/g, '')\n        .replace(/-----END CERTIFICATE-----/g, '')\n        .replace(/\\s+/g, '');\n\n    return new SAML({\n        callbackUrl: `${appUrl}/api/auth/saml/callback/${providerSlug}`,\n        entryPoint: provider.ssoUrl,\n        issuer: provider.spEntityId || `${appUrl}/api/auth/saml/metadata/${providerSlug}`,\n        idpCert: cert,\n        identifierFormat: provider.nameIdFormat,\n        wantAssertionsSigned: false,\n        wantAuthnResponseSigned: true,\n    });\n}\n\nexport async function getSAMLProviders(includeDisabled = false) {\n    return db.sAMLProvider.findMany({\n        where: includeDisabled ? {} : { enabled: true },\n        orderBy: { name: 'asc' },\n    });\n}\n\nexport async function getSAMLLoginUrl(providerName: string, relayState?: string): Promise<string> {\n    const provider = await db.sAMLProvider.findFirst({\n        where: {\n            name: { equals: providerName, mode: 'insensitive' },\n            enabled: true,\n        },\n    });\n\n    if (!provider) {\n        throw new Error(`SAML provider not found: ${providerName}`);\n    }\n\n    const saml = buildSAMLInstance(provider);\n    const url = await saml.getAuthorizeUrlAsync(relayState || '', undefined, {});\n    return url;\n}\n\nexport async function validateSAMLResponse(\n    providerName: string,\n    samlResponse: string,\n): Promise<SAMLUserInfo> {\n    const provider = await db.sAMLProvider.findFirst({\n        where: {\n            name: { equals: providerName, mode: 'insensitive' },\n            enabled: true,\n        },\n    });\n\n    if (!provider) {\n        throw new Error(`SAML provider not found: ${providerName}`);\n    }\n\n    const saml = buildSAMLInstance(provider);\n\n    const { profile } = await saml.validatePostResponseAsync({\n        SAMLResponse: samlResponse,\n    });\n\n    if (!profile) {\n        throw new Error('No profile returned from SAML assertion');\n    }\n\n    // Extract email using configured attribute or fallbacks\n    const rawProfile = profile as Record<string, unknown>;\n    const email =\n        (rawProfile[provider.emailAttribute] as string) ||\n        (rawProfile['email'] as string) ||\n        (rawProfile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] as string) ||\n        (rawProfile['nameID'] as string);\n\n    if (!email) {\n        throw new Error('Could not extract email from SAML assertion');\n    }\n\n    // Extract name using configured attribute or fallbacks\n    const name =\n        (rawProfile[provider.nameAttribute] as string) ||\n        (rawProfile['name'] as string) ||\n        (rawProfile['displayName'] as string) ||\n        (rawProfile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'] as string) ||\n        undefined;\n\n    return {\n        nameId: profile.nameID || email,\n        email,\n        name,\n        sessionIndex: profile.sessionIndex || undefined,\n        rawProfile,\n    };\n}\n\nexport async function handleSAMLCallback(providerName: string, samlResponse: string) {\n    const provider = await db.sAMLProvider.findFirst({\n        where: {\n            name: { equals: providerName, mode: 'insensitive' },\n            enabled: true,\n        },\n    });\n\n    if (!provider) {\n        throw new Error(`SAML provider not found: ${providerName}`);\n    }\n\n    const userInfo = await validateSAMLResponse(providerName, samlResponse);\n\n    if (!userInfo.email) {\n        throw new Error('Email is required from SAML provider');\n    }\n\n    // Look for existing SAML connection by nameId\n    const existingConnection = await db.sAMLConnection.findFirst({\n        where: {\n            providerId: provider.id,\n            nameId: userInfo.nameId,\n        },\n        include: { user: true },\n    });\n\n    if (existingConnection) {\n        // Update session index if changed\n        await db.sAMLConnection.update({\n            where: { id: existingConnection.id },\n            data: {\n                sessionIndex: userInfo.sessionIndex || null,\n                updatedAt: new Date(),\n            },\n        });\n\n        await db.user.update({\n            where: { id: existingConnection.user.id },\n            data: { lastLoginAt: new Date() },\n        });\n\n        const tokens = await generateTokens(existingConnection.user.id);\n        return { user: existingConnection.user, ...tokens };\n    }\n\n    // Find or create user by email\n    let user = await db.user.findUnique({ where: { email: userInfo.email } });\n\n    // Validate email domain restrictions\n    const validation = validateEmailDomain(\n        userInfo.email,\n        {\n            allowedEmailDomains: provider.allowedEmailDomains,\n            blockExistingUsers: provider.blockExistingUsers,\n        },\n        !!user\n    );\n\n    if (!validation.allowed) {\n        throw new Error(validation.reason || 'Email domain not allowed for this SSO provider');\n    }\n\n    // Validate required claims from SAML attributes\n    const requiredClaims = parseRequiredClaims(provider.requiredClaims);\n    const claimValidation = validateClaims(userInfo.rawProfile || {}, requiredClaims);\n\n    if (!claimValidation.allowed) {\n        throw new Error(claimValidation.reason || 'Required SAML attributes validation failed');\n    }\n\n    if (!user) {\n        user = await db.user.create({\n            data: {\n                email: userInfo.email,\n                name: userInfo.name || null,\n                password: '',\n                role: Role.STAFF,\n                lastLoginAt: new Date(),\n            },\n        });\n\n        await db.settings.create({\n            data: { userId: user.id, theme: 'light' },\n        });\n    } else {\n        await db.user.update({\n            where: { id: user.id },\n            data: { lastLoginAt: new Date() },\n        });\n    }\n\n    // Create or update SAML connection\n    await db.sAMLConnection.upsert({\n        where: {\n            providerId_userId: {\n                providerId: provider.id,\n                userId: user.id,\n            },\n        },\n        create: {\n            providerId: provider.id,\n            userId: user.id,\n            nameId: userInfo.nameId,\n            sessionIndex: userInfo.sessionIndex || null,\n        },\n        update: {\n            nameId: userInfo.nameId,\n            sessionIndex: userInfo.sessionIndex || null,\n            updatedAt: new Date(),\n        },\n    });\n\n    const tokens = await generateTokens(user.id);\n    return { user, ...tokens };\n}\n\nexport async function getSAMLMetadata(providerName: string): Promise<string> {\n    const provider = await db.sAMLProvider.findFirst({\n        where: {\n            name: { equals: providerName, mode: 'insensitive' },\n        },\n    });\n\n    if (!provider) {\n        throw new Error(`SAML provider not found: ${providerName}`);\n    }\n\n    const saml = buildSAMLInstance(provider);\n    return saml.generateServiceProviderMetadata(null, null);\n}\n"
  },
  {
    "path": "lib/auth/tokens.ts",
    "content": "import {SignJWT, jwtVerify} from 'jose';\nimport {db} from '../db';\nimport {nanoid} from 'nanoid';\nimport {User} from '@prisma/client';\n\nconst ACCESS_SECRET = new TextEncoder().encode(\n    process.env.JWT_ACCESS_SECRET || 'your-access-secret-key'\n);\n\nexport async function generateTokens(userId: string) {\n    console.log(`Generating tokens for user: ${userId}`);\n\n    try {\n        // Generate access token (short-lived - 15 minutes)\n        const accessToken = await new SignJWT({userId})\n            .setProtectedHeader({alg: 'HS256'})\n            .setExpirationTime('15m')\n            .setIssuedAt()\n            .sign(ACCESS_SECRET);\n\n        console.log('Access token generated successfully');\n\n        // Generate refresh token (long-lived - 7 days)\n        const refreshToken = nanoid(64);\n        const expiresAt = new Date();\n        expiresAt.setDate(expiresAt.getDate() + 7);\n\n        // Store refresh token in database\n        await db.refreshToken.create({\n            data: {\n                userId,\n                token: refreshToken,\n                expiresAt,\n            },\n        });\n\n        console.log('Refresh token created and stored in database');\n\n        return {\n            accessToken,\n            refreshToken\n        };\n    } catch (error) {\n        console.error('Token generation error:', error);\n        throw error;\n    }\n}\n\nexport async function generateCLITokens(userId: string) {\n    console.log(`Generating CLI tokens for user: ${userId}`);\n\n    try {\n        // Generate access token (30 days for CLI usage)\n        const accessToken = await new SignJWT({userId, type: 'cli'})\n            .setProtectedHeader({alg: 'HS256'})\n            .setExpirationTime('30d')\n            .setIssuedAt()\n            .sign(ACCESS_SECRET);\n\n        console.log('CLI access token generated successfully');\n\n        // Generate refresh token (90 days for CLI usage)\n        const refreshToken = nanoid(64);\n        const expiresAt = new Date();\n        expiresAt.setDate(expiresAt.getDate() + 90);\n\n        // Store refresh token in database\n        await db.refreshToken.create({\n            data: {\n                userId,\n                token: refreshToken,\n                expiresAt,\n            },\n        });\n\n        console.log('CLI refresh token created and stored in database');\n\n        return {\n            accessToken,\n            refreshToken\n        };\n    } catch (error) {\n        console.error('CLI token generation error:', error);\n        throw error;\n    }\n}\n\nexport async function verifyAccessToken(token: string) {\n    try {\n        const {payload} = await jwtVerify(token, ACCESS_SECRET);\n        return payload.userId as string;\n    } catch {\n        return null;\n    }\n}\n\ninterface TokenResponse {\n    accessToken: string;\n    refreshToken: string;\n    user: User;\n}\n\nexport async function refreshAccessToken(currentRefreshToken: string): Promise<TokenResponse | null> {\n    try {\n        // Find and validate the refresh token\n        const existingToken = await db.refreshToken.findUnique({\n            where: {\n                token: currentRefreshToken\n            },\n            include: {\n                user: true\n            }\n        });\n\n        // Check if token exists and is valid\n        if (!existingToken || existingToken.invalidated || existingToken.expiresAt < new Date()) {\n            // Invalidate the token if it exists but is expired\n            if (existingToken) {\n                await db.refreshToken.update({\n                    where: {id: existingToken.id},\n                    data: {invalidated: true}\n                });\n            }\n            return null;\n        }\n\n        // Generate new access token\n        const accessToken = await new SignJWT({userId: existingToken.user.id})\n            .setProtectedHeader({alg: 'HS256'})\n            .setExpirationTime('15m')\n            .setIssuedAt()\n            .sign(ACCESS_SECRET);\n\n        // Generate new refresh token\n        const refreshToken = nanoid(64);\n        const expiresAt = new Date();\n        expiresAt.setDate(expiresAt.getDate() + 7);\n\n        // Store new refresh token\n        await db.refreshToken.create({\n            data: {\n                userId: existingToken.user.id,\n                token: refreshToken,\n                expiresAt,\n            },\n        });\n\n        // Invalidate the old refresh token\n        await db.refreshToken.update({\n            where: {id: existingToken.id},\n            data: {invalidated: true}\n        });\n\n        return {\n            accessToken,\n            refreshToken,\n            user: existingToken.user\n        };\n    } catch (error) {\n        console.error('Error refreshing token:', error);\n        return null;\n    }\n}\n\nexport async function refreshCLIAccessToken(currentRefreshToken: string): Promise<TokenResponse | null> {\n    try {\n        // Find and validate the refresh token\n        const existingToken = await db.refreshToken.findUnique({\n            where: {\n                token: currentRefreshToken\n            },\n            include: {\n                user: true\n            }\n        });\n\n        // Check if token exists and is valid\n        if (!existingToken || existingToken.invalidated || existingToken.expiresAt < new Date()) {\n            // Invalidate the token if it exists but is expired\n            if (existingToken) {\n                await db.refreshToken.update({\n                    where: {id: existingToken.id},\n                    data: {invalidated: true}\n                });\n            }\n            return null;\n        }\n\n        // Generate new CLI access token (30 days)\n        const accessToken = await new SignJWT({userId: existingToken.user.id, type: 'cli'})\n            .setProtectedHeader({alg: 'HS256'})\n            .setExpirationTime('30d')\n            .setIssuedAt()\n            .sign(ACCESS_SECRET);\n\n        // Generate new refresh token (90 days)\n        const refreshToken = nanoid(64);\n        const expiresAt = new Date();\n        expiresAt.setDate(expiresAt.getDate() + 90);\n\n        // Store new refresh token\n        await db.refreshToken.create({\n            data: {\n                userId: existingToken.user.id,\n                token: refreshToken,\n                expiresAt,\n            },\n        });\n\n        // Invalidate the old refresh token\n        await db.refreshToken.update({\n            where: {id: existingToken.id},\n            data: {invalidated: true}\n        });\n\n        return {\n            accessToken,\n            refreshToken,\n            user: existingToken.user\n        };\n    } catch (error) {\n        console.error('Error refreshing CLI token:', error);\n        return null;\n    }\n}"
  },
  {
    "path": "lib/auth/webauthn.ts",
    "content": "import {\n    generateRegistrationOptions,\n    verifyRegistrationResponse,\n    generateAuthenticationOptions,\n    verifyAuthenticationResponse,\n    type VerifiedRegistrationResponse,\n    type VerifiedAuthenticationResponse,\n} from '@simplewebauthn/server';\nimport type {\n    RegistrationResponseJSON,\n    AuthenticationResponseJSON,\n    PublicKeyCredentialCreationOptionsJSON,\n    PublicKeyCredentialRequestOptionsJSON,\n} from '@simplewebauthn/types';\n\nconst rpName = 'Changerawr';\nconst rpID = process.env.NEXT_PUBLIC_APP_URL\n    ? new URL(process.env.NEXT_PUBLIC_APP_URL).hostname\n    : 'localhost';\nconst origin = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';\n\nexport async function generateRegistrationOptionsForUser(\n    userId: string,\n    userName: string,\n    userEmail: string,\n    excludeCredentials: { id: string; transports?: string[] }[] = []\n): Promise<PublicKeyCredentialCreationOptionsJSON> {\n    return generateRegistrationOptions({\n        rpName,\n        rpID,\n        userID: userId,\n        userName: userEmail,\n        userDisplayName: userName || userEmail,\n        attestationType: 'none',\n        excludeCredentials: excludeCredentials.map(cred => ({\n            id: Buffer.from(cred.id, 'base64url'),\n            type: 'public-key',\n            transports: cred.transports as AuthenticatorTransport[],\n        })),\n        authenticatorSelection: {\n            residentKey: 'preferred',\n            userVerification: 'preferred',\n        },\n    });\n}\n\nexport async function verifyRegistration(\n    response: RegistrationResponseJSON,\n    expectedChallenge: string\n): Promise<VerifiedRegistrationResponse> {\n    return verifyRegistrationResponse({\n        response,\n        expectedChallenge,\n        expectedOrigin: origin,\n        expectedRPID: rpID,\n    });\n}\n\nexport async function generateAuthenticationOptionsForUser(\n    allowCredentials: { id: string; transports?: string[] }[] = []\n): Promise<PublicKeyCredentialRequestOptionsJSON> {\n    return generateAuthenticationOptions({\n        rpID,\n        allowCredentials: allowCredentials.map(cred => ({\n            id: Buffer.from(cred.id, 'base64url'),\n            type: 'public-key',\n            transports: cred.transports as AuthenticatorTransport[],\n        })),\n        userVerification: 'preferred',\n    });\n}\n\nexport async function verifyAuthentication(\n    response: AuthenticationResponseJSON,\n    expectedChallenge: string,\n    authenticatorPublicKey: string,\n    authenticatorCounter: number\n): Promise<VerifiedAuthenticationResponse> {\n    return verifyAuthenticationResponse({\n        response,\n        expectedChallenge,\n        expectedOrigin: origin,\n        expectedRPID: rpID,\n        authenticator: {\n            credentialPublicKey: Buffer.from(authenticatorPublicKey, 'base64'),\n            credentialID: Buffer.from(response.id, 'base64url'),\n            counter: authenticatorCounter,\n        },\n    });\n}"
  },
  {
    "path": "lib/constants/timezones.ts",
    "content": "export const TIMEZONES = [\n    // UTC\n    {value: 'UTC', label: 'UTC', region: 'Universal'},\n\n    // Americas\n    {value: 'America/New_York', label: 'Eastern Time (US)', region: 'Americas'},\n    {value: 'America/Chicago', label: 'Central Time (US)', region: 'Americas'},\n    {value: 'America/Denver', label: 'Mountain Time (US)', region: 'Americas'},\n    {value: 'America/Los_Angeles', label: 'Pacific Time (US)', region: 'Americas'},\n    {value: 'America/Anchorage', label: 'Alaska Time (US)', region: 'Americas'},\n    {value: 'Pacific/Honolulu', label: 'Hawaii Time (US)', region: 'Americas'},\n    {value: 'America/Toronto', label: 'Eastern Time (Canada)', region: 'Americas'},\n    {value: 'America/Vancouver', label: 'Pacific Time (Canada)', region: 'Americas'},\n    {value: 'America/Sao_Paulo', label: 'Brasilia Time', region: 'Americas'},\n    {value: 'America/Argentina/Buenos_Aires', label: 'Argentina Time', region: 'Americas'},\n    {value: 'America/Mexico_City', label: 'Central Time (Mexico)', region: 'Americas'},\n    {value: 'America/Bogota', label: 'Colombia Time', region: 'Americas'},\n    {value: 'America/Santiago', label: 'Chile Time', region: 'Americas'},\n\n    // Europe\n    {value: 'Europe/London', label: 'London (GMT/BST)', region: 'Europe'},\n    {value: 'Europe/Paris', label: 'Central European Time', region: 'Europe'},\n    {value: 'Europe/Berlin', label: 'Berlin (CET/CEST)', region: 'Europe'},\n    {value: 'Europe/Madrid', label: 'Madrid (CET/CEST)', region: 'Europe'},\n    {value: 'Europe/Rome', label: 'Rome (CET/CEST)', region: 'Europe'},\n    {value: 'Europe/Amsterdam', label: 'Amsterdam (CET/CEST)', region: 'Europe'},\n    {value: 'Europe/Stockholm', label: 'Stockholm (CET/CEST)', region: 'Europe'},\n    {value: 'Europe/Warsaw', label: 'Warsaw (CET/CEST)', region: 'Europe'},\n    {value: 'Europe/Athens', label: 'Athens (EET/EEST)', region: 'Europe'},\n    {value: 'Europe/Helsinki', label: 'Helsinki (EET/EEST)', region: 'Europe'},\n    {value: 'Europe/Moscow', label: 'Moscow Time', region: 'Europe'},\n    {value: 'Europe/Istanbul', label: 'Istanbul (TRT)', region: 'Europe'},\n\n    // Middle East & Africa\n    {value: 'Asia/Dubai', label: 'Dubai (GST)', region: 'Middle East & Africa'},\n    {value: 'Asia/Riyadh', label: 'Riyadh (AST)', region: 'Middle East & Africa'},\n    {value: 'Asia/Tehran', label: 'Tehran (IRST)', region: 'Middle East & Africa'},\n    {value: 'Asia/Jerusalem', label: 'Jerusalem (IST)', region: 'Middle East & Africa'},\n    {value: 'Africa/Cairo', label: 'Cairo (EET)', region: 'Middle East & Africa'},\n    {value: 'Africa/Johannesburg', label: 'Johannesburg (SAST)', region: 'Middle East & Africa'},\n    {value: 'Africa/Lagos', label: 'Lagos (WAT)', region: 'Middle East & Africa'},\n\n    // Asia\n    {value: 'Asia/Kolkata', label: 'India Standard Time', region: 'Asia'},\n    {value: 'Asia/Dhaka', label: 'Bangladesh Time', region: 'Asia'},\n    {value: 'Asia/Bangkok', label: 'Indochina Time', region: 'Asia'},\n    {value: 'Asia/Jakarta', label: 'Western Indonesia Time', region: 'Asia'},\n    {value: 'Asia/Shanghai', label: 'China Standard Time', region: 'Asia'},\n    {value: 'Asia/Hong_Kong', label: 'Hong Kong Time', region: 'Asia'},\n    {value: 'Asia/Taipei', label: 'Taipei Time', region: 'Asia'},\n    {value: 'Asia/Tokyo', label: 'Japan Standard Time', region: 'Asia'},\n    {value: 'Asia/Seoul', label: 'Korea Standard Time', region: 'Asia'},\n    {value: 'Asia/Singapore', label: 'Singapore Time', region: 'Asia'},\n    {value: 'Asia/Manila', label: 'Philippine Time', region: 'Asia'},\n\n    // Oceania\n    {value: 'Australia/Sydney', label: 'Australian Eastern Time', region: 'Oceania'},\n    {value: 'Australia/Melbourne', label: 'Melbourne (AEST/AEDT)', region: 'Oceania'},\n    {value: 'Australia/Brisbane', label: 'Brisbane (AEST)', region: 'Oceania'},\n    {value: 'Australia/Adelaide', label: 'Adelaide (ACST/ACDT)', region: 'Oceania'},\n    {value: 'Australia/Perth', label: 'Australian Western Time', region: 'Oceania'},\n    {value: 'Pacific/Auckland', label: 'New Zealand Time', region: 'Oceania'},\n    {value: 'Pacific/Fiji', label: 'Fiji Time', region: 'Oceania'},\n] as const\n\nexport type TimezoneValue = typeof TIMEZONES[number]['value']\nexport type TimezoneRegion = typeof TIMEZONES[number]['region']\n\nexport const TIMEZONE_REGIONS = [...new Set(TIMEZONES.map(tz => tz.region))] as const\n\nexport function getTimezoneLabel(value: string): string {\n    const tz = TIMEZONES.find(t => t.value === value)\n    return tz ? `${tz.label} (${tz.value})` : value\n}\n\nexport function getTimezonesByRegion() {\n    const grouped: Record<string, typeof TIMEZONES[number][]> = {}\n    for (const tz of TIMEZONES) {\n        if (!grouped[tz.region]) grouped[tz.region] = []\n        grouped[tz.region].push(tz)\n    }\n    return grouped\n}\n"
  },
  {
    "path": "lib/custom-domains/constants.ts",
    "content": "export const DOMAIN_CONSTANTS = {\n    VERIFICATION_SUBDOMAIN: '_chrverify',\n    VERIFICATION_PREFIX: 'changerawr-domain-verification',\n    MAX_DOMAINS_PER_PROJECT: 5,\n    DNS_PROPAGATION_TIMEOUT: 48 * 60 * 60 * 1000, // 48 hours in ms\n} as const\n\nexport const BLOCKED_DOMAINS = [\n    'localhost',\n    '127.0.0.1',\n    '0.0.0.0',\n    'example.com',\n    'test.com',\n    'invalid.com',\n] as const\n\nexport const DOMAIN_ERRORS = {\n    INVALID_FORMAT: 'Invalid domain format',\n    ALREADY_EXISTS: 'Domain is already configured',\n    PROJECT_NOT_FOUND: 'Project not found',\n    DOMAIN_NOT_FOUND: 'Domain configuration not found',\n    MAX_DOMAINS_EXCEEDED: 'Maximum domains per project exceeded',\n    BLOCKED_DOMAIN: 'This domain cannot be used',\n    APP_DOMAIN_CONFLICT: 'Cannot use the application domain',\n    DNS_VERIFICATION_FAILED: 'DNS verification failed',\n    UNAUTHORIZED: 'Unauthorized to manage this domain',\n} as const"
  },
  {
    "path": "lib/custom-domains/dns.ts",
    "content": "import dns from 'dns/promises'\nimport type {DNSVerificationResult} from '@/lib/types/custom-domains'\nimport {DOMAIN_CONSTANTS} from './constants'\n\nexport async function verifyDNSRecords(\n    domain: string,\n    expectedCnameTarget: string,\n    verificationToken: string\n): Promise<DNSVerificationResult> {\n    const result: DNSVerificationResult = {\n        cnameValid: false,\n        txtValid: false,\n        errors: []\n    }\n\n    try {\n        // First, try standard DNS verification\n        await verifyCNAME(domain, expectedCnameTarget, result)\n        await verifyTXTRecord(domain, verificationToken, result, false) // set to true if testing UI locally,\n        // we aren't able to get TXT records.\n\n        // If CNAME verification failed, try HTTP fallback\n        if (!result.cnameValid) {\n            console.log(`CNAME verification failed for ${domain}, attempting HTTP fallback...`)\n            const httpVerification = await verifyDomainViaHTTP(domain, verificationToken)\n\n            if (httpVerification.success) {\n                result.cnameValid = true\n                // Remove CNAME-related errors since HTTP verification succeeded\n                result.errors = result.errors?.filter(error => !error.includes('CNAME')) || []\n                console.log(`HTTP fallback verification successful for ${domain}`)\n            } else {\n                result.errors?.push('HTTP fallback verification failed - domain may not be pointing to our servers')\n            }\n        }\n\n    } catch (error) {\n        result.errors?.push(`DNS verification error: ${error instanceof Error ? error.message : 'Unknown error'}`)\n    }\n\n    return result\n}\n\nasync function verifyCNAME(\n    domain: string,\n    expectedTarget: string,\n    result: DNSVerificationResult\n): Promise<void> {\n    try {\n        const cnameRecords = await dns.resolveCname(domain)\n        result.cnameTarget = cnameRecords[0]\n\n        result.cnameValid = cnameRecords.some(record =>\n            record === expectedTarget || record.endsWith(expectedTarget)\n        )\n\n        if (!result.cnameValid) {\n            result.errors?.push(\n                `CNAME record should point to ${expectedTarget}, found ${cnameRecords.join(', ')}`\n            )\n        }\n    } catch (error) {\n        result.errors?.push(\n            `CNAME verification failed: ${error instanceof Error ? error.message : 'No CNAME record found'}`\n        )\n    }\n}\n\nasync function verifyTXTRecord(\n    domain: string,\n    verificationToken: string,\n    result: DNSVerificationResult,\n    debug = false\n): Promise<void> {\n    if (debug) {\n        result.txtRecord = `${DOMAIN_CONSTANTS.VERIFICATION_PREFIX}=${verificationToken}`\n        result.txtValid = true\n        return\n    }\n\n    try {\n        const verificationDomain = `${DOMAIN_CONSTANTS.VERIFICATION_SUBDOMAIN}.${domain}`\n        const txtRecords = await dns.resolveTxt(verificationDomain)\n        const flatRecords = txtRecords.flat()\n\n        result.txtRecord = flatRecords.find(record =>\n            record.includes(DOMAIN_CONSTANTS.VERIFICATION_PREFIX)\n        )\n\n        result.txtValid = flatRecords.some(record => record.includes(verificationToken))\n\n        if (!result.txtValid) {\n            result.errors?.push(\n                `TXT record ${verificationDomain} should contain ${verificationToken}`\n            )\n        }\n    } catch (error) {\n        result.errors?.push(\n            `TXT verification failed: ${error instanceof Error ? error.message : 'No TXT record found'}`\n        )\n    }\n}\n\n\nasync function verifyDomainViaHTTP(\n    domain: string,\n    verificationToken: string\n): Promise<{ success: boolean; error?: string }> {\n    try {\n        // Make HTTP request to the domain's verification endpoint\n        const verificationUrl = `https://${domain}/api/changelog/verify-domain?domain=${encodeURIComponent(domain)}&token=${encodeURIComponent(verificationToken)}`\n\n        console.log(`Attempting HTTP verification: ${verificationUrl}`)\n\n        const response = await fetch(verificationUrl, {\n            method: 'GET',\n            headers: {\n                'User-Agent': 'ChangeRawr-Domain-Verification/1.0',\n            },\n            // Set a reasonable timeout\n            signal: AbortSignal.timeout(10000) // 10 seconds\n        })\n\n        if (!response.ok) {\n            return {\n                success: false,\n                error: `HTTP verification failed: ${response.status} ${response.statusText}`\n            }\n        }\n\n        const data = await response.json()\n\n        if (data.success && data.verified) {\n            return {success: true}\n        } else {\n            return {\n                success: false,\n                error: data.error || 'HTTP verification endpoint returned unsuccessful response'\n            }\n        }\n\n    } catch (error) {\n        return {\n            success: false,\n            error: `HTTP verification error: ${error instanceof Error ? error.message : 'Unknown error'}`\n        }\n    }\n}\n\nexport async function checkDomainResolution(domain: string): Promise<boolean> {\n    try {\n        await dns.lookup(domain)\n        return true\n    } catch {\n        return false\n    }\n}"
  },
  {
    "path": "lib/custom-domains/service.ts",
    "content": "import {db} from '@/lib/db'\nimport type {CustomDomain} from '@/lib/types/custom-domains'\nimport {validateDomain, validateProjectId, generateVerificationToken} from './validation'\nimport {DOMAIN_ERRORS, DOMAIN_CONSTANTS} from './constants'\nimport {notifyAgent} from './ssl/webhook'\n\nexport async function createCustomDomain(\n    domain: string,\n    projectId: string,\n    userId?: string\n): Promise<CustomDomain> {\n    const cleanDomain = domain.toLowerCase().trim()\n\n    // Validate domain format\n    const domainValidation = validateDomain(cleanDomain)\n    if (!domainValidation.valid) {\n        throw new Error(domainValidation.error || DOMAIN_ERRORS.INVALID_FORMAT)\n    }\n\n    // Validate project ID\n    if (!validateProjectId(projectId)) {\n        throw new Error('Invalid project ID format')\n    }\n\n    // Check if project exists\n    const project = await db.project.findUnique({\n        where: {id: projectId}\n    })\n\n    if (!project) {\n        throw new Error(DOMAIN_ERRORS.PROJECT_NOT_FOUND)\n    }\n\n    // Check if domain already exists\n    const existingDomain = await db.customDomain.findUnique({\n        where: {domain: cleanDomain}\n    })\n\n    if (existingDomain) {\n        throw new Error(DOMAIN_ERRORS.ALREADY_EXISTS)\n    }\n\n    // Check domain limit per project\n    const existingDomainsCount = await db.customDomain.count({\n        where: {projectId}\n    })\n\n    if (existingDomainsCount >= DOMAIN_CONSTANTS.MAX_DOMAINS_PER_PROJECT) {\n        throw new Error(DOMAIN_ERRORS.MAX_DOMAINS_EXCEEDED)\n    }\n\n    const verificationToken = generateVerificationToken()\n\n    return db.customDomain.create({\n        data: {\n            domain: cleanDomain,\n            projectId,\n            verificationToken,\n            userId\n        },\n        include: {\n            project: true\n        }\n    })\n}\n\nexport async function getDomainByDomain(domain: string): Promise<CustomDomain | null> {\n    return db.customDomain.findUnique({\n        where: {domain: domain.toLowerCase().trim()},\n        include: {\n            project: true,\n            certificates: {\n                orderBy: {createdAt: 'desc'},\n                take: 5\n            },\n            browserRules: {\n                orderBy: {createdAt: 'desc'}\n            },\n            throttleConfig: true\n        }\n    })\n}\n\nexport async function getDomainsByProject(projectId: string): Promise<CustomDomain[]> {\n    return db.customDomain.findMany({\n        where: {projectId},\n        include: {\n            project: true,\n            certificates: {\n                orderBy: {createdAt: 'desc'},\n                take: 5\n            },\n            browserRules: {\n                orderBy: {createdAt: 'desc'}\n            },\n            throttleConfig: true\n        },\n        orderBy: {createdAt: 'desc'}\n    })\n}\n\nexport async function getDomainsByUser(userId: string): Promise<CustomDomain[]> {\n    return db.customDomain.findMany({\n        where: {userId},\n        include: {\n            project: true,\n            certificates: {\n                orderBy: {createdAt: 'desc'},\n                take: 5\n            },\n            browserRules: {\n                orderBy: {createdAt: 'desc'}\n            },\n            throttleConfig: true\n        },\n        orderBy: {createdAt: 'desc'}\n    })\n}\n\nexport async function getAllDomains(): Promise<CustomDomain[]> {\n    return db.customDomain.findMany({\n        include: {\n            project: true,\n            certificates: {\n                orderBy: {createdAt: 'desc'},\n                take: 5\n            },\n            browserRules: {\n                orderBy: {createdAt: 'desc'}\n            },\n            throttleConfig: true\n        },\n        orderBy: {createdAt: 'desc'}\n    })\n}\n\nexport async function updateDomainVerification(\n    domain: string,\n    verified: boolean\n): Promise<CustomDomain> {\n    const cleanDomain = domain.toLowerCase().trim()\n\n    const result = await db.customDomain.update({\n        where: {domain: cleanDomain},\n        data: {\n            verified,\n            verifiedAt: verified ? new Date() : null\n        },\n        include: {\n            project: true\n        }\n    })\n\n    // Notify nginx-agent when domain is verified\n    if (verified) {\n        await notifyAgent({\n            event: 'domain.added',\n            domain: cleanDomain,\n        })\n    }\n\n    return result\n}\n\nexport async function deleteDomain(domain: string): Promise<void> {\n    const cleanDomain = domain.toLowerCase().trim()\n\n    await db.customDomain.delete({\n        where: {domain: cleanDomain}\n    })\n\n    // Notify nginx-agent that domain was removed\n    await notifyAgent({\n        event: 'domain.removed',\n        domain: cleanDomain,\n    })\n}\n\nexport async function canUserManageDomain(\n    domain: string,\n    userId: string,\n    isAdmin: boolean = false\n): Promise<boolean> {\n    if (isAdmin) return true\n\n    const domainConfig = await getDomainByDomain(domain)\n    if (!domainConfig) return false\n\n    // Check if user owns the domain directly\n    if (domainConfig.userId === userId) return true\n\n    // Check if user owns the project (if you have project ownership in your schema)\n    // Uncomment and adjust based on your Project model structure\n    // if (domainConfig.project.userId === userId) return true\n\n    return false\n}"
  },
  {
    "path": "lib/custom-domains/ssl/acme-account.ts",
    "content": "import * as acme from 'acme-client'\nimport {db} from '@/lib/db'\nimport {encrypt, decrypt} from '@/lib/custom-domains/ssl/encryption'\n\nconst isAcmeStagingEnabled = () => (\n    process.env.ACME_STAGING === 'true' || process.env.ACME_SANDBOX_MODE === 'true'\n)\n\nconst getDirectoryUrl = () => {\n    return isAcmeStagingEnabled()\n        ? acme.directory.letsencrypt.staging\n        : acme.directory.letsencrypt.production\n}\n\nconst getAccountId = (directoryUrl: string) => {\n    return directoryUrl === acme.directory.letsencrypt.staging\n        ? 'global-staging'\n        : 'global-production'\n}\n\n// Creates and persists the global ACME account on first call,\n// loads it from DB on every subsequent call.\nexport async function getAcmeClient(): Promise<acme.Client> {\n    const directoryUrl = getDirectoryUrl()\n    const accountId = getAccountId(directoryUrl)\n    const existing = await db.acmeAccount.findUnique({where: {id: accountId}})\n\n    if (existing) {\n        const accountKey = Buffer.from(decrypt(existing.accountKeyPem))\n        return new acme.Client({\n            directoryUrl,\n            accountKey,\n            accountUrl: existing.accountUrl,\n        })\n    }\n\n    // ECDSA P-256 — smaller and faster than RSA\n    const accountKey = await acme.crypto.createPrivateEcdsaKey()\n    const client = new acme.Client({directoryUrl, accountKey})\n\n    const email = process.env.ACME_EMAIL\n    if (!email) {\n        throw new Error('ACME_EMAIL is required for certificate issuance')\n    }\n\n    await client.createAccount({\n        termsOfServiceAgreed: true,\n        contact: [`mailto:${email}`],\n    })\n\n    const accountUrl = client.getAccountUrl()\n\n    await db.acmeAccount.create({\n        data: {\n            id: accountId,\n            accountKeyPem: encrypt(accountKey.toString()),\n            accountUrl,\n            email,\n        },\n    })\n\n    return client\n}\n\nexport {isAcmeStagingEnabled}"
  },
  {
    "path": "lib/custom-domains/ssl/auto-renewal.ts",
    "content": "import { db } from '@/lib/db'\nimport { renewCertificate } from './service'\n\n// ─── Auto-Renewal Configuration ──────────────────────────────────────────────\n\n// Renew certificates when they have less than this many days remaining\nconst RENEWAL_THRESHOLD_DAYS = parseInt(process.env.SSL_RENEWAL_THRESHOLD_DAYS || '30', 10)\n\n// Maximum number of certificates to process in one run (prevents overwhelming the system)\nconst MAX_BATCH_SIZE = parseInt(process.env.SSL_RENEWAL_BATCH_SIZE || '10', 10)\n\n// ─── Auto-Renewal Job ────────────────────────────────────────────────────────\n\nexport async function runAutoRenewal(): Promise<{\n    checked: number\n    renewed: number\n    failed: number\n    errors: Array<{ domain: string; error: string }>\n}> {\n    const now = Date.now()\n    const thresholdDate = new Date(now + RENEWAL_THRESHOLD_DAYS * 24 * 60 * 60 * 1000)\n\n    console.log('[auto-renewal] Starting renewal check...')\n    console.log(`[auto-renewal] Threshold: ${RENEWAL_THRESHOLD_DAYS} days (expires before ${thresholdDate.toISOString()})`)\n\n    // Find certificates that are:\n    // 1. Currently ISSUED\n    // 2. Expiring within the threshold\n    // 3. Not already being renewed (no pending cert for the same domain)\n    const expiringCerts = await db.domainCertificate.findMany({\n        where: {\n            status: 'ISSUED',\n            expiresAt: {\n                lte: thresholdDate,\n            },\n        },\n        include: {\n            domain: {\n                include: {\n                    certificates: {\n                        where: {\n                            status: {\n                                in: ['PENDING_HTTP01', 'PENDING_DNS01'],\n                            },\n                        },\n                    },\n                },\n            },\n        },\n        take: MAX_BATCH_SIZE,\n        orderBy: {\n            expiresAt: 'asc', // Process the ones expiring soonest first\n        },\n    })\n\n    // Filter out domains that already have a pending renewal\n    const certsToRenew = expiringCerts.filter(\n        cert => cert.domain.certificates.length === 0\n    )\n\n    console.log(`[auto-renewal] Found ${expiringCerts.length} expiring certificates`)\n    console.log(`[auto-renewal] ${certsToRenew.length} eligible for renewal (${expiringCerts.length - certsToRenew.length} already pending)`)\n\n    let renewed = 0\n    let failed = 0\n    const errors: Array<{ domain: string; error: string }> = []\n\n    for (const cert of certsToRenew) {\n        const daysUntilExpiry = cert.expiresAt\n            ? Math.floor((cert.expiresAt.getTime() - now) / (24 * 60 * 60 * 1000))\n            : 'unknown'\n\n        console.log(`[auto-renewal] Renewing ${cert.domain.domain} (expires in ${daysUntilExpiry} days)`)\n\n        try {\n            await renewCertificate(cert)\n            renewed++\n            console.log(`[auto-renewal] ✓ Renewal initiated for ${cert.domain.domain}`)\n        } catch (error) {\n            failed++\n            const errorMessage = error instanceof Error ? error.message : String(error)\n            errors.push({ domain: cert.domain.domain, error: errorMessage })\n            console.error(`[auto-renewal] ✗ Failed to renew ${cert.domain.domain}:`, errorMessage)\n\n            // Mark the error in the database\n            await db.domainCertificate.update({\n                where: { id: cert.id },\n                data: {\n                    lastError: `Auto-renewal failed: ${errorMessage}`,\n                    renewalAttempts: { increment: 1 },\n                },\n            }).catch(() => {})\n        }\n    }\n\n    const summary = {\n        checked: expiringCerts.length,\n        renewed,\n        failed,\n        errors,\n    }\n\n    console.log('[auto-renewal] Summary:', summary)\n\n    return summary\n}\n\n// ─── Manual Trigger (for testing) ────────────────────────────────────────────\n\nexport async function checkCertificateHealth(): Promise<{\n    total: number\n    issued: number\n    expiringSoon: number\n    expired: number\n    pending: number\n    failed: number\n}> {\n    const now = new Date()\n    const thresholdDate = new Date(now.getTime() + RENEWAL_THRESHOLD_DAYS * 24 * 60 * 60 * 1000)\n\n    const [total, issued, expiringSoon, expired, pending, failed] = await Promise.all([\n        db.domainCertificate.count(),\n        db.domainCertificate.count({ where: { status: 'ISSUED' } }),\n        db.domainCertificate.count({\n            where: {\n                status: 'ISSUED',\n                expiresAt: { lte: thresholdDate },\n            },\n        }),\n        db.domainCertificate.count({ where: { status: 'EXPIRED' } }),\n        db.domainCertificate.count({\n            where: {\n                status: { in: ['PENDING_HTTP01', 'PENDING_DNS01'] },\n            },\n        }),\n        db.domainCertificate.count({ where: { status: 'FAILED' } }),\n    ])\n\n    return {\n        total,\n        issued,\n        expiringSoon,\n        expired,\n        pending,\n        failed,\n    }\n}\n"
  },
  {
    "path": "lib/custom-domains/ssl/cron-setup.md",
    "content": "# SSL Auto-Renewal Cron Setup\n\nThis document explains how to set up automatic SSL certificate renewal using cron jobs.\n\n## Overview\n\nThe auto-renewal system checks for certificates expiring within 30 days and automatically renews them. This prevents service disruptions from expired certificates.\n\n## Cron Job Configuration\n\n### Option 1: System Cron (Linux/macOS)\n\nAdd this to your crontab (`crontab -e`):\n\n```bash\n# Run SSL auto-renewal every day at 3 AM\n0 3 * * * curl -X POST https://your-domain.com/api/cron/ssl-renewal -H \"Authorization: Bearer YOUR_INTERNAL_API_SECRET\" >> /var/log/ssl-renewal.log 2>&1\n```\n\n### Option 2: Docker Cron Container\n\nCreate a separate container that runs the cron job:\n\n**docker-compose.yml:**\n```yaml\nservices:\n  ssl-renewal-cron:\n    image: alpine:latest\n    command: >\n      sh -c \"echo '0 3 * * * wget --header=\\\"Authorization: Bearer $$INTERNAL_API_SECRET\\\" --post-data=\\\"\\\" https://your-domain.com/api/cron/ssl-renewal -O - >> /var/log/ssl-renewal.log 2>&1' | crontab - && crond -f\"\n    environment:\n      - INTERNAL_API_SECRET=${INTERNAL_API_SECRET}\n    volumes:\n      - ./logs:/var/log\n    restart: unless-stopped\n```\n\n### Option 3: External Cron Service\n\nUse a service like:\n- **Cron-job.org** - Free web-based cron service\n- **EasyCron** - Scheduled HTTP requests\n- **GitHub Actions** - Scheduled workflows\n\n**Example GitHub Action (.github/workflows/ssl-renewal.yml):**\n```yaml\nname: SSL Auto-Renewal\non:\n  schedule:\n    - cron: '0 3 * * *' # Daily at 3 AM UTC\n  workflow_dispatch: # Allow manual trigger\n\njobs:\n  renew:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Trigger SSL Renewal\n        run: |\n          curl -X POST https://your-domain.com/api/cron/ssl-renewal \\\n            -H \"Authorization: Bearer ${{ secrets.INTERNAL_API_SECRET }}\"\n```\n\n### Option 4: Vercel Cron (Vercel Deployments)\n\n**vercel.json:**\n```json\n{\n  \"crons\": [\n    {\n      \"path\": \"/api/cron/ssl-renewal\",\n      \"schedule\": \"0 3 * * *\"\n    }\n  ]\n}\n```\n\nNote: You'll need to modify the API route to check for Vercel's cron secret instead of the Authorization header.\n\n## Monitoring\n\n### Check Certificate Health\n\n```bash\ncurl -H \"Authorization: Bearer YOUR_INTERNAL_API_SECRET\" \\\n  \"https://your-domain.com/api/cron/ssl-renewal?action=health\"\n```\n\nResponse:\n```json\n{\n  \"success\": true,\n  \"health\": {\n    \"total\": 45,\n    \"issued\": 42,\n    \"expiringSoon\": 5,\n    \"expired\": 0,\n    \"pending\": 2,\n    \"failed\": 1\n  }\n}\n```\n\n### Manual Renewal Trigger\n\n```bash\ncurl -X POST -H \"Authorization: Bearer YOUR_INTERNAL_API_SECRET\" \\\n  https://your-domain.com/api/cron/ssl-renewal\n```\n\nResponse:\n```json\n{\n  \"success\": true,\n  \"result\": {\n    \"checked\": 5,\n    \"renewed\": 4,\n    \"failed\": 1,\n    \"errors\": [\n      {\n        \"domain\": \"example.com\",\n        \"error\": \"DNS-01 certificate requires manual renewal\"\n      }\n    ]\n  }\n}\n```\n\n## Configuration\n\n### Environment Variables\n\n```bash\n# Required: Secret for authenticating cron jobs\nINTERNAL_API_SECRET=your-random-secret-here\n\n# Optional: Customize renewal threshold (default: 30 days)\nSSL_RENEWAL_THRESHOLD_DAYS=30\n\n# Optional: Maximum certificates to process per run (default: 10)\nSSL_RENEWAL_BATCH_SIZE=10\n```\n\n## How It Works\n\n1. **Daily Check**: Cron job runs daily (recommended: 3 AM)\n2. **Find Expiring**: Queries database for certificates expiring within 30 days\n3. **Filter Pending**: Skips domains that already have a pending renewal\n4. **Batch Process**: Renews up to 10 certificates per run (prevents overwhelming the system)\n5. **Prioritize**: Processes certificates expiring soonest first\n6. **Log Results**: Returns summary of renewed/failed certificates\n\n## Important Notes\n\n- ✅ **HTTP-01 certificates renew automatically**\n- ❌ **DNS-01 certificates require manual renewal** (user must re-add TXT record)\n- 🔒 **Secured with INTERNAL_API_SECRET** - never expose this publicly\n- 📊 **Logs all operations** for debugging and monitoring\n- ⚡ **Rate limit aware** - respects Let's Encrypt rate limits\n- 🎯 **Smart batching** - processes a limited number per run to prevent overload\n\n## Troubleshooting\n\n### Renewal Failed\n\nCheck the certificate's `lastError` field in the database:\n```sql\nSELECT domain.domain, status, lastError, renewalAttempts\nFROM DomainCertificate\nJOIN CustomDomain ON DomainCertificate.domainId = CustomDomain.id;\n```\n\n### Manual Renewal Required\n\nFor DNS-01 certificates, notify users to manually renew:\n1. User goes to domain settings\n2. Clicks \"Renew\" button\n3. Follows DNS-01 wizard to add new TXT record\n4. System issues new certificate\n\n### Rate Limit Hit\n\nLet's Encrypt limits:\n- 50 certificates per registered domain per week\n- Auto-renewal respects this limit\n- Failed renewals increment `renewalAttempts` counter\n- Consider spreading renewals across the week if you hit limits\n"
  },
  {
    "path": "lib/custom-domains/ssl/encryption.ts",
    "content": "import crypto from 'crypto'\n\nconst ALGORITHM = 'aes-256-gcm' as const\nconst IV_LENGTH = 12  // NIST-recommended for GCM\nconst SEPARATOR = ':'\n\nfunction getKey(): Buffer {\n    const raw = process.env.ENCRYPTION_KEY\n    if (!raw) throw new Error('ENCRYPTION_KEY is not set')\n    const key = Buffer.from(raw, 'base64')\n    if (key.length !== 32) {\n        throw new Error(`ENCRYPTION_KEY must be 32 bytes (got ${key.length})`)\n    }\n    return key\n}\n\n// Returns \"iv_b64:authTag_b64:ciphertext_b64\"\nexport function encrypt(plaintext: string): string {\n    const key = getKey()\n    const iv = crypto.randomBytes(IV_LENGTH)\n    const cipher = crypto.createCipheriv(ALGORITHM, key, iv)\n\n    const encrypted = Buffer.concat([\n        cipher.update(plaintext, 'utf8'),\n        cipher.final(),\n    ])\n\n    return [\n        iv.toString('base64'),\n        cipher.getAuthTag().toString('base64'),\n        encrypted.toString('base64'),\n    ].join(SEPARATOR)\n}\n\n// Throws on auth tag mismatch — ciphertext has been tampered with\nexport function decrypt(data: string): string {\n    const key = getKey()\n    const parts = data.split(SEPARATOR)\n\n    if (parts.length !== 3) {\n        throw new Error('Invalid encrypted data format')\n    }\n\n    const [ivB64, tagB64, ctB64] = parts\n    const decipher = crypto.createDecipheriv(\n        ALGORITHM,\n        key,\n        Buffer.from(ivB64, 'base64'),\n    )\n    decipher.setAuthTag(Buffer.from(tagB64, 'base64'))\n\n    return Buffer.concat([\n        decipher.update(Buffer.from(ctB64, 'base64')),\n        decipher.final(),\n    ]).toString('utf8')\n}"
  },
  {
    "path": "lib/custom-domains/ssl/is-supported.ts",
    "content": "// Only true when running inside the Docker deployment.\n// DOCKER_DEPLOYMENT=true is set by docker-compose.yml and never by npm start.\nexport const sslSupported = process.env.DOCKER_DEPLOYMENT === 'true'"
  },
  {
    "path": "lib/custom-domains/ssl/service.ts",
    "content": "import * as acme from 'acme-client'\nimport {db} from '@/lib/db'\nimport {encrypt, decrypt} from '@/lib/custom-domains/ssl/encryption'\nimport {getAcmeClient} from '@/lib/custom-domains/ssl/acme-account'\nimport {assertNotInternal} from '@/lib/custom-domains/ssl/ssrf-guard'\nimport {notifyAgent} from '@/lib/custom-domains/ssl/webhook'\nimport type {DomainCertificate} from '@prisma/client'\n\nfunction getOrderByUrl(client: acme.Client, orderUrl: string): Promise<acme.Order> {\n    // acme-client#getOrder expects an order-like object ({ url }), not a raw URL string.\n    // Passing a string causes `order.url` to be undefined and the lookup to fail.\n    return client.getOrder({url: orderUrl} as acme.Order)\n}\n\nasync function createDns01Order(client: acme.Client, hostname: string) {\n    const order = await client.createOrder({\n        identifiers: [{type: 'dns', value: hostname}],\n    })\n\n    const authorizations = await client.getAuthorizations(order)\n    const authz = authorizations[0]\n    if (!authz) {\n        throw new Error('No ACME authorization found for DNS-01 order')\n    }\n\n    const challenge = authz.challenges.find(c => c.type === 'dns-01')\n    if (!challenge) {\n        throw new Error('DNS-01 challenge not available')\n    }\n\n    const dnsTxtValue = await client.getChallengeKeyAuthorization(challenge)\n    return {order, dnsTxtValue}\n}\n\n\nfunction isOrderExpired(order: acme.Order): boolean {\n    const expiresAt = (order as { expires?: string | Date }).expires\n    if (!expiresAt) return false\n\n    const expiresTime = new Date(expiresAt).getTime()\n    if (Number.isNaN(expiresTime)) return false\n\n    return expiresTime <= Date.now()\n}\n\nfunction isUnusableDnsOrder(order: acme.Order): boolean {\n    const status = (order as { status?: string }).status\n    return status === 'invalid' || isOrderExpired(order)\n}\n\n\nfunction shouldReplaceOrderAfterLookupFailure(error: unknown): boolean {\n    const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase()\n\n    return (\n        message.includes('order not found') ||\n        message.includes('urn:ietf:params:acme:error:malformed') ||\n        message.includes('urn:ietf:params:acme:error:unauthorized') ||\n        message.includes('404') ||\n        message.includes('expired') ||\n        message.includes('invalid')\n    )\n}\n\nasync function verifyDnsTxtPropagation(hostname: string, expectedValue: string): Promise<void> {\n    const {Resolver} = await import('dns').then(m => m.promises)\n    const recordName = `_acme-challenge.${hostname}`\n\n    // Check multiple resolvers to reduce false negatives from stale local DNS caches.\n    const resolverConfigs = [\n        {name: 'system', servers: null as string[] | null},\n        {name: 'cloudflare', servers: ['1.1.1.1', '1.0.0.1']},\n        {name: 'google', servers: ['8.8.8.8', '8.8.4.4']},\n    ]\n\n    const maxAttempts = 4\n    let lastError = ''\n\n    for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n        const matchedResolvers: string[] = []\n        const observedByResolver: string[] = []\n\n        for (const cfg of resolverConfigs) {\n            const resolver = new Resolver()\n            if (cfg.servers) {\n                resolver.setServers(cfg.servers)\n            }\n\n            try {\n                const txtRecords = await resolver.resolveTxt(recordName)\n                const flatRecords = txtRecords.flat()\n                observedByResolver.push(`${cfg.name}=[${flatRecords.join(', ')}]`)\n\n                if (flatRecords.includes(expectedValue)) {\n                    matchedResolvers.push(cfg.name)\n                }\n            } catch (error) {\n                const message = error instanceof Error ? error.message : String(error)\n                observedByResolver.push(`${cfg.name}=<lookup-error:${message}>`)\n            }\n        }\n\n        console.log(`[ssl/dns01]    DNS attempt ${attempt}/${maxAttempts} observations: ${observedByResolver.join(' | ')}`)\n\n        if (matchedResolvers.length > 0) {\n            console.log(`[ssl/dns01] ✅ Self-check passed! TXT record visible via resolver(s): ${matchedResolvers.join(', ')}`)\n            return\n        }\n\n        lastError = `Expected TXT value not yet visible for ${recordName}`\n        if (attempt < maxAttempts) {\n            const waitMs = attempt * 3000\n            console.log(`[ssl/dns01] ⏳ TXT not visible yet, retrying in ${waitMs}ms...`)\n            await new Promise(resolve => setTimeout(resolve, waitMs))\n        }\n    }\n\n    throw new Error(`DNS TXT record not yet propagated. ${lastError}. Please wait a few minutes and try again.`)\n}\n\n// ─── Rate limiting ────────────────────────────────────────────────────────────\n// In-memory per-registered-domain counter. Move to Redis for multi-instance.\n\nconst recentIssuances = new Map<string, number[]>()\nconst MAX_PER_WEEK = 45  // LE hard limit is 50, stay under it\nconst ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000\n\nfunction getRegisteredDomain(hostname: string): string {\n    // TODO: replace with tldts or publicsuffix.js for accuracy\n    return hostname.split('.').slice(-2).join('.')\n}\n\nfunction checkRateLimit(hostname: string): void {\n    const root = getRegisteredDomain(hostname)\n    const now = Date.now()\n    const timestamps = (recentIssuances.get(root) ?? []).filter(\n        t => now - t < ONE_WEEK_MS,\n    )\n    if (timestamps.length >= MAX_PER_WEEK) {\n        throw new Error(\n            `Too many certificate issuances for ${root} this week (${timestamps.length}/${MAX_PER_WEEK})`,\n        )\n    }\n    timestamps.push(now)\n    recentIssuances.set(root, timestamps)\n}\n\n// ─── HTTP-01 ──────────────────────────────────────────────────────────────────\n\n// Kicks off HTTP-01 issuance and returns immediately with the cert record ID.\n// Completion happens asynchronously — poll /api/acme/status/[certId].\nexport async function initiateHttp01Certificate(\n    domainId: string,\n    hostname: string,\n): Promise<string> {\n    console.log(`[ssl/http01] 🔵 Starting HTTP-01 certificate issuance for ${hostname}`)\n\n    await assertNotInternal(hostname)\n    checkRateLimit(hostname)\n\n    console.log(`[ssl/http01] 📡 Requesting certificate order from Let's Encrypt...`)\n    const client = await getAcmeClient()\n\n    const order = await client.createOrder({\n        identifiers: [{type: 'dns', value: hostname}],\n    })\n    console.log(`[ssl/http01] ✅ Order created: ${order.url}`)\n\n    const authorizations = await client.getAuthorizations(order)\n    const authz = authorizations[0]\n    if (!authz) {\n        throw new Error('No ACME authorization found for HTTP-01 order')\n    }\n\n    const challenge = authz.challenges.find(c => c.type === 'http-01')\n\n    if (!challenge) {\n        console.log(`[ssl/http01] ❌ HTTP-01 challenge not available from Let's Encrypt`)\n        throw new Error(\n            'HTTP-01 challenge not available for this domain. Try DNS-01 instead.',\n        )\n    }\n\n    const keyAuthorization = await client.getChallengeKeyAuthorization(challenge)\n    console.log(`[ssl/http01] 🔑 Challenge token: ${challenge.token}`)\n    console.log(`[ssl/http01] 📝 Challenge will be served at: http://${hostname}/.well-known/acme-challenge/${challenge.token}`)\n\n    const [certKeyBuffer, csrBuffer] = await acme.crypto.createCsr({\n        altNames: [hostname],\n    })\n\n    const cert = await db.domainCertificate.create({\n        data: {\n            domainId,\n            status: 'PENDING_HTTP01',\n            challengeType: 'HTTP01',\n            privateKeyPem: encrypt(certKeyBuffer.toString()),\n            csrPem: csrBuffer.toString(),\n            acmeOrderUrl: order.url,\n            challengeToken: challenge.token,\n            challengeKeyAuth: keyAuthorization,\n        },\n    })\n    console.log(`[ssl/http01] 💾 Certificate record saved to database (ID: ${cert.id})`)\n    console.log(`[ssl/http01] ⏳ Waiting for Let's Encrypt to verify the challenge...`)\n\n    void completeHttp01Challenge(cert.id, client, authz, challenge, csrBuffer)\n        .catch(err => markFailed(cert.id, err))\n\n    return cert.id\n}\n\nasync function completeHttp01Challenge(\n    certId: string,\n    client: acme.Client,\n    authz: acme.Authorization,\n    challenge: any,\n    csr: Buffer,\n): Promise<void> {\n    try {\n        // Get certificate details for self-check\n        const certRecord = await db.domainCertificate.findUnique({\n            where: {id: certId},\n            include: {domain: true},\n        })\n\n        if (!certRecord) {\n            throw new Error('Certificate not found')\n        }\n\n        const hostname = certRecord.domain.domain\n        const token = certRecord.challengeToken\n        const expectedKeyAuth = certRecord.challengeKeyAuth\n\n        if (!token || !expectedKeyAuth) {\n            throw new Error('Challenge token or key authorization missing')\n        }\n\n        // Self-check: verify we can retrieve the challenge via HTTP before telling Let's Encrypt\n        console.log(`[ssl/http01] 🔍 Self-checking challenge endpoint...`)\n        const challengeUrl = `http://${hostname}/.well-known/acme-challenge/${token}`\n        console.log(`[ssl/http01]    URL: ${challengeUrl}`)\n\n        let selfCheckPassed = false\n        const maxAttempts = 5\n        let attempt = 0\n\n        while (attempt < maxAttempts && !selfCheckPassed) {\n            const backoffMs = Math.min(1000 * Math.pow(2, attempt), 5000) // Exponential backoff: 1s, 2s, 4s, 5s, 5s\n\n            if (attempt > 0) {\n                console.log(`[ssl/http01] ⏳ Waiting ${backoffMs}ms before retry (attempt ${attempt + 1}/${maxAttempts})...`)\n                await new Promise(resolve => setTimeout(resolve, backoffMs))\n            }\n\n            try {\n                console.log(`[ssl/http01] 📡 Attempt ${attempt + 1}/${maxAttempts}: Fetching challenge endpoint...`)\n                const response = await fetch(challengeUrl, {\n                    headers: {'Host': hostname},\n                    signal: AbortSignal.timeout(5000),\n                })\n\n                console.log(`[ssl/http01]    Response status: ${response.status}`)\n\n                if (response.status === 200) {\n                    const body = await response.text()\n                    console.log(`[ssl/http01]    Response body length: ${body.length} bytes`)\n                    console.log(`[ssl/http01]    Expected: ${expectedKeyAuth.substring(0, 20)}...`)\n                    console.log(`[ssl/http01]    Received: ${body.substring(0, 20)}...`)\n\n                    if (body === expectedKeyAuth) {\n                        console.log(`[ssl/http01] ✅ Self-check PASSED - challenge is accessible and correct!`)\n                        selfCheckPassed = true\n                    } else {\n                        console.log(`[ssl/http01] ❌ Self-check FAILED - key authorization mismatch`)\n                        attempt++\n                    }\n                } else {\n                    console.log(`[ssl/http01] ❌ Self-check FAILED - HTTP ${response.status}`)\n                    attempt++\n                }\n            } catch (error) {\n                console.error(`[ssl/http01] ❌ Self-check error:`, error instanceof Error ? error.message : error)\n                attempt++\n            }\n        }\n\n        if (!selfCheckPassed) {\n            throw new Error(`Challenge self-check failed after ${maxAttempts} attempts - cannot verify challenge is accessible`)\n        }\n\n        console.log(`[ssl/http01] 🚀 Telling Let's Encrypt to verify the challenge...`)\n        await client.completeChallenge(challenge)\n\n        console.log(`[ssl/http01] ⏳ Waiting for Let's Encrypt to validate...`)\n        await client.waitForValidStatus(challenge)\n        console.log(`[ssl/http01] ✅ Challenge validated successfully!`)\n\n        const updatedCert = await db.domainCertificate.findUnique({where: {id: certId}})\n        if (!updatedCert?.acmeOrderUrl) {\n            console.log(`[ssl/http01] ❌ Order URL missing from database`)\n            throw new Error('Order URL missing from DB')\n        }\n\n        console.log(`[ssl/http01] 📋 Finalizing order with Certificate Signing Request...`)\n        const currentOrder = await getOrderByUrl(client, updatedCert.acmeOrderUrl)\n\n        if (currentOrder.status !== 'valid') {\n            await client.finalizeOrder(currentOrder, csr)\n        } else {\n            console.log('[ssl/http01] ℹ️ Order already valid; skipping finalizeOrder')\n        }\n\n        const finalizedOrder = await getOrderByUrl(client, currentOrder.url)\n\n        console.log(`[ssl/http01] 📜 Downloading certificate from Let's Encrypt...`)\n        const certificate = await client.getCertificate(finalizedOrder)\n        const info = acme.crypto.readCertificateInfo(certificate)\n\n        // ACME getCertificate returns the full chain (leaf + intermediates)\n        // Split to get just the leaf cert\n        const certs = certificate.split(/(?=-----BEGIN CERTIFICATE-----)/g).filter(Boolean)\n        const leafCert = certs[0] || certificate\n        const fullChain = certificate\n\n        console.log(`[ssl/http01] ✅ Certificate issued successfully!`)\n        console.log(`[ssl/http01]    Leaf cert: ${leafCert.length} bytes`)\n        console.log(`[ssl/http01]    Full chain: ${fullChain.length} bytes`)\n        console.log(`[ssl/http01]    Expires: ${info.notAfter.toISOString()}`)\n\n        await db.domainCertificate.update({\n            where: {id: certId},\n            data: {\n                status: 'ISSUED',\n                certificatePem: leafCert,\n                fullChainPem: fullChain,\n                issuedAt: new Date(),\n                expiresAt: info.notAfter,\n                acmeOrderUrl: null,\n                challengeToken: null,\n                challengeKeyAuth: null,\n            },\n        })\n\n        const domain = await db.customDomain.update({\n            where: {id: updatedCert.domainId},\n            data: {sslMode: 'LETS_ENCRYPT'},\n        })\n\n        console.log(`[ssl/http01] 📤 Notifying nginx-agent about new certificate...`)\n        await notifyAgent({\n            event: 'cert.issued',\n            domain: domain.domain,\n            certId: certId,\n        })\n        console.log(`[ssl/http01] 🎉 HTTP-01 certificate issuance complete!`)\n    } catch (error) {\n        console.error(`[ssl/http01] ❌ Certificate issuance failed:`, error)\n        if (error instanceof Error) {\n            console.error(`[ssl/http01]    Error: ${error.message}`)\n            if (error.stack) {\n                console.error(`[ssl/http01]    Stack: ${error.stack}`)\n            }\n        }\n        throw error\n    }\n}\n\n// ─── DNS-01 ───────────────────────────────────────────────────────────────────\n\nexport interface Dns01ChallengeInfo {\n    certId: string\n    txtName: string   // _acme-challenge.{hostname}\n    txtValue: string  // base64url(SHA-256(keyAuth)) — the TXT record value\n}\n\n// Returns the TXT record the user must create.\n// After they add it, call completeDns01Certificate(certId).\nexport async function initiateDns01Certificate(\n    domainId: string,\n    hostname: string,\n): Promise<Dns01ChallengeInfo> {\n    console.log(`[ssl/dns01] 🔵 Starting DNS-01 certificate issuance for ${hostname}`)\n\n    await assertNotInternal(hostname)\n    checkRateLimit(hostname)\n\n    console.log(`[ssl/dns01] 📡 Requesting certificate order from Let's Encrypt...`)\n    const client = await getAcmeClient()\n\n    const {order, dnsTxtValue} = await createDns01Order(client, hostname)\n    console.log(`[ssl/dns01] ✅ Order created: ${order.url}`)\n    console.log(`[ssl/dns01] 📝 TXT record required:`)\n    console.log(`[ssl/dns01]    Name: _acme-challenge.${hostname}`)\n    console.log(`[ssl/dns01]    Value: ${dnsTxtValue}`)\n\n    const [certKeyBuffer, csrBuffer] = await acme.crypto.createCsr({\n        altNames: [hostname],\n    })\n\n    const cert = await db.domainCertificate.create({\n        data: {\n            domainId,\n            status: 'PENDING_DNS01',\n            challengeType: 'DNS01',\n            privateKeyPem: encrypt(certKeyBuffer.toString()),\n            csrPem: csrBuffer.toString(),\n            acmeOrderUrl: order.url,\n            dnsTxtValue,\n        },\n    })\n    console.log(`[ssl/dns01] 💾 Certificate record saved to database (ID: ${cert.id})`)\n    console.log(`[ssl/dns01] ⏸️  Waiting for user to add DNS TXT record...`)\n\n    return {\n        certId: cert.id,\n        txtName: `_acme-challenge.${hostname}`,\n        txtValue: dnsTxtValue,\n    }\n}\n\n// Called after the user has added the DNS TXT record.\nexport async function completeDns01Certificate(certId: string): Promise<void> {\n    try {\n        console.log(`[ssl/dns01] 🔄 Attempting to complete DNS-01 verification for cert ${certId}`)\n\n        const cert = await db.domainCertificate.findUnique({\n            where: {id: certId},\n            include: {domain: true},\n        })\n\n        if (!cert) {\n            console.log(`[ssl/dns01] ❌ Certificate record not found in database`)\n            throw new Error('Certificate record not found')\n        }\n        if (cert.status !== 'PENDING_DNS01') {\n            console.log(`[ssl/dns01] ❌ Certificate in unexpected state: ${cert.status} (expected PENDING_DNS01)`)\n            throw new Error(`Certificate is in unexpected state: ${cert.status}`)\n        }\n        if (!cert.acmeOrderUrl) {\n            console.log(`[ssl/dns01] ❌ Missing ACME order URL`)\n            throw new Error('Missing ACME order URL - certificate may need to be re-issued')\n        }\n\n        const client = await getAcmeClient()\n        const hostname = cert.domain.domain\n\n        console.log(`[ssl/dns01] 🔍 Restoring existing ACME order from: ${cert.acmeOrderUrl}`)\n\n        let order\n        try {\n            order = await getOrderByUrl(client, cert.acmeOrderUrl)\n            console.log(`[ssl/dns01] ✅ Order retrieved successfully, status: ${order.status}`)\n        } catch (error) {\n            console.error(`[ssl/dns01] ❌ Failed to retrieve ACME order:`, error)\n            console.log(`[ssl/dns01]    Order URL: ${cert.acmeOrderUrl}`)\n\n            if (!shouldReplaceOrderAfterLookupFailure(error)) {\n                throw new Error('Unable to retrieve ACME order due to a transient upstream error. Please wait a moment and verify again.')\n            }\n\n            console.log(`[ssl/dns01]    Attempting to create a fresh DNS-01 order for seamless retry...`)\n\n            const {\n                order: replacementOrder,\n                dnsTxtValue: replacementDnsTxtValue\n            } = await createDns01Order(client, hostname)\n\n            await db.domainCertificate.update({\n                where: {id: certId},\n                data: {\n                    acmeOrderUrl: replacementOrder.url,\n                    dnsTxtValue: replacementDnsTxtValue,\n                    lastError: 'Previous ACME order expired or became invalid. Please update the TXT record to the new value and verify again.',\n                },\n            })\n\n            throw new Error('ACME order expired or became invalid. A new order has been created with a new TXT value. Update your DNS TXT record from the latest instructions and verify again.')\n        }\n\n        if (isUnusableDnsOrder(order)) {\n            console.log(`[ssl/dns01] ⚠️ Existing order is unusable (status=${order.status}); creating replacement order...`)\n            const {\n                order: replacementOrder,\n                dnsTxtValue: replacementDnsTxtValue\n            } = await createDns01Order(client, hostname)\n\n            await db.domainCertificate.update({\n                where: {id: certId},\n                data: {\n                    acmeOrderUrl: replacementOrder.url,\n                    dnsTxtValue: replacementDnsTxtValue,\n                    lastError: 'ACME order was no longer usable. Please update the TXT record to the new value and verify again.',\n                },\n            })\n\n            throw new Error('ACME order is no longer usable. A new order has been created with a new TXT value. Update your DNS TXT record from the latest instructions and verify again.')\n        }\n\n        const authorizations = await client.getAuthorizations(order)\n        const authz = authorizations[0]\n        if (!authz) {\n            throw new Error('No ACME authorization found while completing DNS-01 order')\n        }\n\n        const challenge = authz.challenges.find(c => c.type === 'dns-01')\n\n        if (!challenge) {\n            console.log(`[ssl/dns01] ❌ DNS-01 challenge not found in order`)\n            throw new Error('DNS-01 challenge not found')\n        }\n\n        // Self-check: verify the DNS TXT record exists before telling Let's Encrypt to check\n        console.log(`[ssl/dns01] 🔍 Self-check: verifying DNS TXT record is propagated...`)\n        console.log(`[ssl/dns01]    Looking for: _acme-challenge.${hostname} = ${cert.dnsTxtValue}`)\n\n        if (!cert.dnsTxtValue) {\n            throw new Error('DNS TXT challenge value is missing from certificate record')\n        }\n\n        try {\n            await verifyDnsTxtPropagation(hostname, cert.dnsTxtValue)\n        } catch (dnsError) {\n            const message = dnsError instanceof Error ? dnsError.message : String(dnsError)\n            console.error(`[ssl/dns01] ❌ DNS self-check failed: ${message}`)\n            throw new Error(message)\n        }\n\n        const challengeStatus = (challenge as { status?: string }).status\n        if (challengeStatus === 'invalid') {\n            console.log('[ssl/dns01] ❌ Existing DNS challenge is invalid; creating replacement order...')\n            const {\n                order: replacementOrder,\n                dnsTxtValue: replacementDnsTxtValue\n            } = await createDns01Order(client, hostname)\n\n            await db.domainCertificate.update({\n                where: {id: certId},\n                data: {\n                    acmeOrderUrl: replacementOrder.url,\n                    dnsTxtValue: replacementDnsTxtValue,\n                    lastError: 'ACME DNS challenge became invalid. Please update the TXT record to the new value and verify again.',\n                },\n            })\n\n            throw new Error('ACME DNS challenge is invalid. A new order has been created with a new TXT value. Update your DNS TXT record from the latest instructions and verify again.')\n        }\n\n        if (challengeStatus === 'valid') {\n            console.log('[ssl/dns01] ✅ DNS challenge already valid; skipping challenge completion step')\n        } else {\n            console.log(`[ssl/dns01] 🚀 Telling Let's Encrypt to verify DNS TXT record...`)\n            await client.completeChallenge(challenge)\n\n            console.log(`[ssl/dns01] ⏳ Waiting for Let's Encrypt to validate DNS record...`)\n            try {\n                await client.waitForValidStatus(challenge)\n                console.log(`[ssl/dns01] ✅ DNS challenge validated successfully!`)\n            } catch (validationError) {\n                console.error(`[ssl/dns01] ❌ DNS validation failed:`, validationError)\n                if (validationError instanceof Error) {\n                    console.error(`[ssl/dns01]    Error message: ${validationError.message}`)\n                    console.error(`[ssl/dns01]    Error name: ${validationError.name}`)\n                }\n                // Re-throw with more context about DNS validation\n                throw new Error(`DNS validation failed: ${validationError instanceof Error ? validationError.message : 'Unknown error'}`)\n            }\n        }\n\n        const csr = Buffer.from(cert.csrPem)\n        console.log(`[ssl/dns01] 📋 Finalizing order with Certificate Signing Request...`)\n        if (order.status !== 'valid') {\n            await client.finalizeOrder(order, csr)\n        } else {\n            console.log('[ssl/dns01] ℹ️ Order already valid; skipping finalizeOrder')\n        }\n\n        const finalizedOrder = await getOrderByUrl(client, order.url)\n\n        console.log(`[ssl/dns01] 📜 Downloading certificate from Let's Encrypt...`)\n        const certificate = await client.getCertificate(finalizedOrder)\n        const info = acme.crypto.readCertificateInfo(certificate)\n\n        // ACME getCertificate returns the full chain (leaf + intermediates)\n        // Split to get just the leaf cert\n        const certs = certificate.split(/(?=-----BEGIN CERTIFICATE-----)/g).filter(Boolean)\n        const leafCert = certs[0] || certificate\n        const fullChain = certificate\n\n        console.log(`[ssl/dns01] ✅ Certificate issued successfully!`)\n        console.log(`[ssl/dns01]    Leaf cert: ${leafCert.length} bytes`)\n        console.log(`[ssl/dns01]    Full chain: ${fullChain.length} bytes`)\n        console.log(`[ssl/dns01]    Expires: ${info.notAfter.toISOString()}`)\n\n        await db.domainCertificate.update({\n            where: {id: certId},\n            data: {\n                status: 'ISSUED',\n                certificatePem: leafCert,\n                fullChainPem: fullChain,\n                issuedAt: new Date(),\n                expiresAt: info.notAfter,\n                acmeOrderUrl: null,\n                dnsTxtValue: null,\n            },\n        })\n\n        const domain = await db.customDomain.update({\n            where: {id: cert.domainId},\n            data: {sslMode: 'LETS_ENCRYPT'},\n        })\n\n        console.log(`[ssl/dns01] 📤 Notifying nginx-agent about new certificate...`)\n        await notifyAgent({\n            event: 'cert.issued',\n            domain: domain.domain,\n            certId: certId,\n        })\n        console.log(`[ssl/dns01] 🎉 DNS-01 certificate issuance complete!`)\n    } catch (error) {\n        console.error(`[ssl/dns01] ❌ Certificate issuance failed:`, error)\n        if (error instanceof Error) {\n            console.error(`[ssl/dns01]    Error: ${error.message}`)\n            if (error.stack) {\n                console.error(`[ssl/dns01]    Stack: ${error.stack}`)\n            }\n        }\n        throw error\n    }\n}\n\n// ─── Cert bundle retrieval ────────────────────────────────────────────────────\n\nexport interface CertBundle {\n    privateKey: string  // decrypted PEM\n    certificate: string  // PEM\n    fullChain: string  // PEM\n    expiresAt: Date\n}\n\nexport async function getActiveCertBundle(\n    hostname: string,\n): Promise<CertBundle | null> {\n    const domain = await db.customDomain.findUnique({\n        where: {domain: hostname},\n    })\n\n    if (!domain || domain.sslMode !== 'LETS_ENCRYPT') return null\n\n    const cert = await db.domainCertificate.findFirst({\n        where: {\n            domainId: domain.id,\n            status: 'ISSUED',\n            certificatePem: {not: null},\n        },\n        orderBy: {issuedAt: 'desc'},\n    })\n\n    if (!cert?.certificatePem || !cert.fullChainPem || !cert.expiresAt) {\n        return null\n    }\n\n    return {\n        privateKey: decrypt(cert.privateKeyPem),\n        certificate: cert.certificatePem,\n        fullChain: cert.fullChainPem,\n        expiresAt: cert.expiresAt,\n    }\n}\n\n// ─── Renewal ──────────────────────────────────────────────────────────────────\n\nexport async function renewCertificate(cert: DomainCertificate & {\n    domain: { domain: string; id: string }\n}): Promise<void> {\n    const hostname = cert.domain.domain\n\n    if (cert.challengeType === 'HTTP01') {\n        const newCertId = await initiateHttp01Certificate(cert.domainId, hostname)\n\n        // Note: notifyAgent will be called when the new certificate is issued\n        // in the completeHttp01Challenge function\n    } else {\n        // DNS-01 can't renew automatically — notify the user instead\n        // TODO: send notification email to domain owner\n        await db.domainCertificate.update({\n            where: {id: cert.id},\n            data: {\n                lastError: 'DNS-01 certificate requires manual renewal via domain settings.',\n            },\n        })\n    }\n}\n\n// ─── Internal ─────────────────────────────────────────────────────────────────\n\nasync function markFailed(certId: string, error: unknown): Promise<void> {\n    const message = error instanceof Error ? error.message : String(error)\n    await db.domainCertificate.update({\n        where: {id: certId},\n        data: {\n            status: 'FAILED',\n            lastError: message,\n            renewalAttempts: {increment: 1},\n        },\n    }).catch(() => {\n    })\n}"
  },
  {
    "path": "lib/custom-domains/ssl/setup-renewal-job.ts",
    "content": "import { db } from '@/lib/db'\nimport { ScheduledJobType, JobStatus } from '@prisma/client'\n\n/**\n * Setup Daily SSL Renewal Job\n *\n * This creates a recurring scheduled job that runs daily at 3 AM to check\n * for expiring SSL certificates and renew them automatically.\n *\n * The job uses the existing ScheduledJobService infrastructure and will be\n * automatically executed by the JobRunnerService.\n */\nexport async function setupDailySslRenewal(): Promise<void> {\n    // Check if a renewal job already exists and is pending/running\n    const existingJob = await db.scheduledJob.findFirst({\n        where: {\n            type: ScheduledJobType.RENEW_SSL_CERTIFICATE,\n            status: {\n                in: [JobStatus.PENDING, JobStatus.RUNNING],\n            },\n        },\n    })\n\n    if (existingJob) {\n        console.log('[ssl-renewal-setup] Daily SSL renewal job already exists:', existingJob.id)\n        return\n    }\n\n    // Calculate next 3 AM\n    const now = new Date()\n    const next3AM = new Date(now)\n    next3AM.setHours(3, 0, 0, 0)\n\n    // If it's already past 3 AM today, schedule for tomorrow\n    if (next3AM <= now) {\n        next3AM.setDate(next3AM.getDate() + 1)\n    }\n\n    // Create the job\n    const job = await db.scheduledJob.create({\n        data: {\n            type: ScheduledJobType.RENEW_SSL_CERTIFICATE,\n            entityId: 'ssl-renewal', // Dummy entityId since this processes all certs\n            scheduledAt: next3AM,\n            maxRetries: 3,\n        },\n    })\n\n    console.log('[ssl-renewal-setup] Created daily SSL renewal job:', {\n        id: job.id,\n        scheduledAt: job.scheduledAt.toISOString(),\n    })\n}\n\n/**\n * Schedule Next SSL Renewal\n *\n * This should be called after each successful SSL renewal run to schedule\n * the next one for the following day at 3 AM.\n */\nexport async function scheduleNextSslRenewal(): Promise<void> {\n    const tomorrow3AM = new Date()\n    tomorrow3AM.setDate(tomorrow3AM.getDate() + 1)\n    tomorrow3AM.setHours(3, 0, 0, 0)\n\n    await db.scheduledJob.create({\n        data: {\n            type: ScheduledJobType.RENEW_SSL_CERTIFICATE,\n            entityId: 'ssl-renewal',\n            scheduledAt: tomorrow3AM,\n            maxRetries: 3,\n        },\n    })\n\n    console.log('[ssl-renewal-setup] Scheduled next SSL renewal for:', tomorrow3AM.toISOString())\n}\n"
  },
  {
    "path": "lib/custom-domains/ssl/ssrf-guard.ts",
    "content": "import dns from 'dns/promises'\n\nconst BLOCKED_PREFIXES = [\n    '127.',\n    '0.',\n    '10.',\n    '169.254.',  // link-local\n    '192.168.',\n    '::1',       // IPv6 loopback\n    'fc',        // IPv6 ULA\n    'fd',        // IPv6 ULA\n] as const\n\n// 172.16.0.0/12 requires a range check, not just a prefix\nfunction is172Private(ip: string): boolean {\n    const match = ip.match(/^172\\.(\\d+)\\./)\n    if (!match) return false\n    const octet = parseInt(match[1], 10)\n    return octet >= 16 && octet <= 31\n}\n\nfunction isPrivateIp(ip: string): boolean {\n    if (BLOCKED_PREFIXES.some(p => ip.startsWith(p))) return true\n    if (is172Private(ip)) return true\n    return false\n}\n\n// Throws if the hostname resolves to any private or loopback address.\n// Call before any outbound ACME request to prevent SSRF.\nexport async function assertNotInternal(hostname: string): Promise<void> {\n    let addresses: string[] = []\n\n    try {\n        const v4 = await dns.resolve4(hostname).catch(() => [])\n        const v6 = await dns.resolve6(hostname).catch(() => [])\n        addresses = [...v4, ...v6]\n    } catch {\n        // DNS failure — let ACME handle it\n        return\n    }\n\n    for (const ip of addresses) {\n        if (isPrivateIp(ip)) {\n            throw new Error(\n                `${hostname} resolves to private IP ${ip} — cannot issue certificate`,\n            )\n        }\n    }\n}"
  },
  {
    "path": "lib/custom-domains/ssl/webhook.ts",
    "content": "import crypto from 'crypto'\n\ntype AgentEvent =\n    | { event: 'cert.issued';          domain: string; certId: string; mode?: 'live' | 'sandbox' }\n    | { event: 'cert.renewed';         domain: string; certId: string; mode?: 'live' | 'sandbox' }\n    | { event: 'cert.revoked';         domain: string }\n    | { event: 'domain.added';         domain: string }\n    | { event: 'domain.removed';       domain: string }\n    | { event: 'ip_whitelist.updated'; enabled: boolean; whitelist: string[] }\n\nfunction sign(body: string, secret: string): string {\n    return 'sha256=' + crypto\n        .createHmac('sha256', secret)\n        .update(body)\n        .digest('hex')\n}\n\n// No-op if NGINX_AGENT_URL is not set. Never throws.\nexport async function notifyAgent(event: AgentEvent): Promise<void> {\n    const agentUrl = process.env.NGINX_AGENT_URL\n    const agentSecret = process.env.NGINX_AGENT_SECRET\n\n    if (!agentUrl || !agentSecret) return\n\n    // Add mode field for cert.issued and cert.renewed events\n    // Check both ACME_STAGING and ACME_SANDBOX_MODE (legacy) for staging mode\n    const isStaging = process.env.ACME_STAGING === 'true' || process.env.ACME_SANDBOX_MODE === 'true'\n    const enrichedEvent = (event.event === 'cert.issued' || event.event === 'cert.renewed')\n        ? { ...event, mode: isStaging ? 'sandbox' as const : 'live' as const }\n        : event\n\n    const body = JSON.stringify(enrichedEvent)\n\n    try {\n        const res = await fetch(`${agentUrl}/webhook`, {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/json',\n                'X-Chr-Signature': sign(body, agentSecret),\n            },\n            body,\n            signal: AbortSignal.timeout(8_000),\n        })\n\n        if (!res.ok) {\n            const text = await res.text().catch(() => '?')\n            console.warn(`[ssl/webhook] agent returned ${res.status}: ${text}`)\n        }\n    } catch (err) {\n        const message = err instanceof Error ? err.message : String(err)\n        console.warn(`[ssl/webhook] ${message}`)\n    }\n}"
  },
  {
    "path": "lib/custom-domains/utils.ts",
    "content": "/**\n * Extracts the hostname from NEXT_PUBLIC_APP_URL\n * Handles both localhost and production URLs\n */\nexport function getAppDomain(): string {\n    const appUrl = process.env.NEXT_PUBLIC_APP_URL\n\n    // Fallback to environment variable override\n    const appDomainOverride = process.env.NEXT_PUBLIC_APP_DOMAIN\n    if (appDomainOverride) {\n        return appDomainOverride\n    }\n\n    if (!appUrl) {\n        throw new Error('NEXT_PUBLIC_APP_URL environment variable is not set')\n    }\n\n    try {\n        const url = new URL(appUrl)\n        return url.host // includes port if present (e.g., localhost:3000)\n    } catch {\n        throw new Error(`Invalid NEXT_PUBLIC_APP_URL: ${appUrl}`)\n    }\n}\n\n/**\n * Checks if we're in development mode (localhost)\n */\nexport function isDevelopment(): boolean {\n    const appUrl = process.env.NEXT_PUBLIC_APP_URL || ''\n    return appUrl.includes('localhost') || appUrl.includes('127.0.0.1')\n}\n\n/**\n * Gets the full app URL for redirects and canonical URLs\n */\nexport function getAppUrl(): string {\n    const appUrl = process.env.NEXT_PUBLIC_APP_URL\n    if (!appUrl) {\n        throw new Error('NEXT_PUBLIC_APP_URL environment variable is not set')\n    }\n    return appUrl.endsWith('/') ? appUrl.slice(0, -1) : appUrl\n}"
  },
  {
    "path": "lib/custom-domains/validation.ts",
    "content": "import {BLOCKED_DOMAINS, DOMAIN_ERRORS} from '@/lib/custom-domains/constants'\nimport {getAppDomain, isDevelopment} from '@/lib/custom-domains/utils'\n\nexport interface ValidationResult {\n    valid: boolean\n    error?: string\n}\n\nexport function validateDomain(domain: string): ValidationResult {\n    const cleanDomain = domain.toLowerCase().trim()\n\n    // Basic domain format validation\n    const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9](?:\\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])*$/\n\n    if (!domainRegex.test(cleanDomain)) {\n        return {valid: false, error: DOMAIN_ERRORS.INVALID_FORMAT}\n    }\n\n    // Check against blocked domains\n    if (BLOCKED_DOMAINS.includes(cleanDomain as typeof BLOCKED_DOMAINS[number])) {\n        return {valid: false, error: DOMAIN_ERRORS.BLOCKED_DOMAIN}\n    }\n\n    // Prevent using our app domain\n    try {\n        const appDomain = getAppDomain()\n        if (cleanDomain === appDomain || cleanDomain.endsWith(`.${appDomain}`)) {\n            return {valid: false, error: DOMAIN_ERRORS.APP_DOMAIN_CONFLICT}\n        }\n    } catch (error) {\n        // If we can't get app domain, skip this check\n        console.warn('Could not validate against app domain:', error)\n    }\n\n    // Additional checks for development/localhost patterns (unless we're in development)\n    if (!isDevelopment() && (cleanDomain.includes('localhost') || cleanDomain.includes('127.0.0.1'))) {\n        return {valid: false, error: DOMAIN_ERRORS.BLOCKED_DOMAIN}\n    }\n\n    return {valid: true}\n}\n\nexport function validateProjectId(projectId: string): boolean {\n    // Assuming CUID format: starts with 'c' followed by 24 alphanumeric characters (not entirely sure)\n    const cuidRegex = /^c[a-z0-9]{24}$/\n    return cuidRegex.test(projectId)\n}\n\nexport function generateVerificationToken(): string {\n    const timestamp = Date.now()\n    const randomPart = Math.random().toString(36).substring(2, 15)\n    return `changerawr-domain-verification-${randomPart}-${timestamp}`\n}"
  },
  {
    "path": "lib/db.ts",
    "content": "import {PrismaClient} from \"@prisma/client\";\n\n// Create a singleton function for Prisma Client\nconst prismaClientSingleton = () => {\n    return new PrismaClient();\n};\n\n// Declare global type to avoid TypeScript errors\ndeclare const globalThis: {\n    prismaGlobal: ReturnType<typeof prismaClientSingleton>;\n} & typeof global;\n\n// Use globalThis instead of global for better compatibility\nconst prisma = globalThis.prismaGlobal ?? prismaClientSingleton();\n\nexport {prisma as db};\n\n// Only cache in development to prevent multiple instances during hot reload\nif (process.env.NODE_ENV !== \"production\") {\n    globalThis.prismaGlobal = prisma;\n}"
  },
  {
    "path": "lib/middleware/analytics.ts",
    "content": "import { db } from '@/lib/db';\nimport { extractIPFromRequest, getGeolocationFromIP, getCountryFromCloudflare } from '@/lib/services/analytics/geolocation';\n\nexport interface AnalyticsTrackingOptions {\n    projectId: string;\n    changelogEntryId?: string;\n    userAgent?: string;\n    referrer?: string;\n}\n\n/**\n * Track a public changelog view\n * This is GDPR compliant and cookieless\n */\nexport async function trackChangelogView(\n    request: Request,\n    options: AnalyticsTrackingOptions\n): Promise<void> {\n    try {\n        // Check if analytics are enabled\n        const systemConfig = await db.systemConfig.findFirst();\n        if (!systemConfig?.enableAnalytics) {\n            return; // Analytics disabled, skip tracking\n        }\n\n        // Extract IP and get geolocation data\n        const ip = extractIPFromRequest(request);\n        const userAgent = options.userAgent || request.headers.get('user-agent') || undefined;\n        const referrer = options.referrer || request.headers.get('referer') || undefined;\n\n        // Try to get country from Cloudflare headers first (most efficient)\n        let country = getCountryFromCloudflare(request);\n\n        let geolocationData;\n        if (!country) {\n            // Fallback to IP geolocation service\n            geolocationData = await getGeolocationFromIP(ip, userAgent);\n            country = geolocationData.country;\n        } else {\n            // Still need to generate hashes even if we have country from Cloudflare\n            geolocationData = await getGeolocationFromIP(ip, userAgent);\n        }\n\n        // Store the analytics data\n        await db.publicChangelogAnalytics.create({\n            data: {\n                projectId: options.projectId,\n                changelogEntryId: options.changelogEntryId,\n                ipHash: geolocationData.ipHash,\n                country: country || undefined,\n                userAgent: userAgent || undefined,\n                referrer: referrer || undefined,\n                sessionHash: geolocationData.sessionHash,\n                viewedAt: new Date()\n            }\n        });\n    } catch (error) {\n        // Don't let analytics tracking break the main request\n        console.error('Failed to track changelog view:', error);\n    }\n}\n\n/**\n * Track multiple views in batch (for performance)\n */\nexport async function trackChangelogViewsBatch(\n    request: Request,\n    views: AnalyticsTrackingOptions[]\n): Promise<void> {\n    try {\n        // Check if analytics are enabled\n        const systemConfig = await db.systemConfig.findFirst();\n        if (!systemConfig?.enableAnalytics) {\n            return; // Analytics disabled, skip tracking\n        }\n\n        // Extract IP and get geolocation data once\n        const ip = extractIPFromRequest(request);\n        const userAgent = request.headers.get('user-agent') || undefined;\n        const referrer = request.headers.get('referer') || undefined;\n\n        // Try to get country from Cloudflare headers first\n        let country = getCountryFromCloudflare(request);\n\n        let geolocationData;\n        if (!country) {\n            geolocationData = await getGeolocationFromIP(ip, userAgent);\n            country = geolocationData.country;\n        } else {\n            geolocationData = await getGeolocationFromIP(ip, userAgent);\n        }\n\n        // Create analytics records for all views\n        const analyticsData = views.map(view => ({\n            projectId: view.projectId,\n            changelogEntryId: view.changelogEntryId,\n            ipHash: geolocationData.ipHash,\n            country: country || undefined,\n            userAgent: view.userAgent || userAgent || undefined,\n            referrer: view.referrer || referrer || undefined,\n            sessionHash: geolocationData.sessionHash,\n            viewedAt: new Date()\n        }));\n\n        await db.publicChangelogAnalytics.createMany({\n            data: analyticsData\n        });\n    } catch (error) {\n        console.error('Failed to track changelog views batch:', error);\n    }\n}"
  },
  {
    "path": "lib/services/analytics/geolocation.ts",
    "content": "import {createHash} from 'crypto';\n\nexport interface GeolocationResult {\n    country?: string;\n    ipHash: string;\n    sessionHash: string;\n}\n\n/**\n * Get country from IP address using a free geolocation service\n * This is GDPR compliant as we hash the IP and don't store it\n */\nexport async function getGeolocationFromIP(\n    ip: string,\n    userAgent?: string\n): Promise<GeolocationResult> {\n    // Create hashed IP for privacy (GDPR compliant)\n    const ipHash = createHash('sha256').update(ip + process.env.ANALYTICS_SALT || 'changerawr-salt').digest('hex');\n\n    // Create session hash (IP + UserAgent for unique session tracking)\n    const sessionHash = createHash('sha256')\n        .update(ip + (userAgent || '') + process.env.ANALYTICS_SALT || 'changerawr-salt')\n        .digest('hex');\n\n    let country: string | undefined;\n\n    try {\n        // Use a free geolocation service (cloudflare headers, ipapi, etc.)\n        // First try Cloudflare CF-IPCountry header if available\n        // This is commonly available in production environments\n\n        // For localhost/development, skip geolocation\n        if (ip === '127.0.0.1' || ip === '::1' || ip.startsWith('192.168.') || ip.startsWith('10.') || ip.startsWith('172.')) {\n            country = 'Local';\n        } else {\n            // Try ipapi.co (free tier: 1000 requests/day)\n            const response = await fetch(`https://ipapi.co/${ip}/country_name/`, {\n                headers: {\n                    'User-Agent': 'Changerawr-Analytics/1.0'\n                },\n                // Add timeout to prevent blocking\n                signal: AbortSignal.timeout(3000)\n            });\n\n            if (response.ok) {\n                const countryName = await response.text();\n                if (countryName && countryName.trim() && !countryName.includes('error')) {\n                    country = countryName.trim();\n                }\n            }\n        }\n    } catch (error) {\n        // Geolocation failed, but we still return the hashes\n        console.warn('Geolocation failed:', error);\n    }\n\n    return {\n        country,\n        ipHash,\n        sessionHash\n    };\n}\n\n/**\n * Extract real IP from request headers\n * Handles various proxy configurations\n */\nexport function extractIPFromRequest(request: Request): string {\n    const forwardedFor = request.headers.get('x-forwarded-for');\n    const realIP = request.headers.get('x-real-ip');\n    const cfConnectingIP = request.headers.get('cf-connecting-ip');\n\n    // Priority: Cloudflare > X-Real-IP > X-Forwarded-For > fallback\n    if (cfConnectingIP) {\n        return cfConnectingIP;\n    }\n\n    if (realIP) {\n        return realIP;\n    }\n\n    if (forwardedFor) {\n        // X-Forwarded-For can contain multiple IPs, take the first one\n        return forwardedFor.split(',')[0].trim();\n    }\n\n    // Fallback for development\n    return '127.0.0.1';\n}\n\n/**\n * Get country from Cloudflare headers (if available)\n * This is the most efficient method when using Cloudflare\n */\nexport function getCountryFromCloudflare(request: Request): string | undefined {\n    return request.headers.get('cf-ipcountry') || undefined;\n}"
  },
  {
    "path": "lib/services/auth/password-breach.ts",
    "content": "import crypto from 'crypto';\n\ninterface PasswordBreachResult {\n    isBreached: boolean;\n    breachCount: number;\n}\n\n/**\n * Check if a password has been compromised in known data breaches\n * Uses HaveIBeenPwned's Pwned Passwords API v3 (k-anonymity model)\n * @param password The password to check\n * @returns Promise with breach status and count\n */\nexport async function checkPasswordBreach(password: string): Promise<PasswordBreachResult> {\n    try {\n        // Hash the password with SHA-1\n        const sha1Hash = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();\n\n        // Take the first 5 characters for k-anonymity\n        const hashPrefix = sha1Hash.substring(0, 5);\n        const hashSuffix = sha1Hash.substring(5);\n\n        // Query HaveIBeenPwned API\n        const response = await fetch(`https://api.pwnedpasswords.com/range/${hashPrefix}`, {\n            method: 'GET',\n            headers: {\n                'User-Agent': 'Changerawr-App',\n            },\n        });\n\n        if (!response.ok) {\n            // If their API is down, don't block login but log the error\n            console.error('Failed to check password breach:', response.status);\n            return {isBreached: false, breachCount: 0};\n        }\n\n        const responseText = await response.text();\n\n        // Parse response to find our hash suffix\n        const lines = responseText.split('\\n');\n        for (const line of lines) {\n            const [suffix, count] = line.trim().split(':');\n            if (suffix === hashSuffix) {\n                return {\n                    isBreached: true,\n                    breachCount: parseInt(count, 10)\n                };\n            }\n        }\n\n        // Hash was not found in breaches\n        return {isBreached: false, breachCount: 0};\n\n    } catch (error) {\n        // If anything fails, don't block login but log the error\n        console.error('Error checking password breach:', error);\n        return {isBreached: false, breachCount: 0};\n    }\n}"
  },
  {
    "path": "lib/services/auth/password-reset.ts",
    "content": "import {nanoid} from 'nanoid';\nimport {render} from '@react-email/render';\nimport {createTransport, SendMailOptions} from 'nodemailer';\nimport {db} from '@/lib/db';\nimport {PasswordResetEmail} from '@/emails/password-reset';\nimport SMTPTransport from 'nodemailer/lib/smtp-transport';\nimport React from \"react\";\n\nexport interface PasswordResetOptions {\n    email: string;\n    resetBaseUrl?: string;\n}\n\nexport interface SendPasswordResetEmailResult {\n    success: boolean;\n    userId?: string;\n    message: string;\n}\n\n/**\n * Creates a password reset token and sends a reset email\n */\nexport async function createPasswordResetAndSendEmail(options: PasswordResetOptions): Promise<SendPasswordResetEmailResult> {\n    try {\n        const {email, resetBaseUrl = `${process.env.NEXT_PUBLIC_APP_URL}/reset-password`} = options;\n\n        // Check if password reset is enabled in system settings\n        const systemConfig = await db.systemConfig.findFirst({\n            where: {id: 1}\n        });\n\n        if (!systemConfig || !systemConfig.enablePasswordReset) {\n            return {\n                success: false,\n                message: 'Password reset functionality is not enabled on this system'\n            };\n        }\n\n        // Check if system has SMTP configured\n        if (!systemConfig.smtpHost || !systemConfig.smtpPort || !systemConfig.systemEmail) {\n            return {\n                success: false,\n                message: 'System email configuration is incomplete'\n            };\n        }\n\n        // Find user by email\n        const user = await db.user.findUnique({\n            where: {email: email.toLowerCase()}\n        });\n\n        if (email.toLowerCase().endsWith('@changerawr.sys')) {\n            return {\n                success: false,\n                message: 'System accounts cannot receive password reset emails.'\n            } satisfies SendPasswordResetEmailResult\n        }\n\n\n        if (!user) {\n            // Don't reveal that the user doesn't exist for security\n            return {\n                success: true,\n                message: 'If a user with this email exists, a password reset email has been sent'\n            };\n        }\n\n        // Invalidate any existing reset tokens for this user\n        await db.passwordReset.updateMany({\n            where: {\n                userId: user.id,\n                usedAt: null\n            },\n            data: {\n                usedAt: new Date()\n            }\n        });\n\n        // Create a new reset token (expires in 60 minutes)\n        const token = nanoid(32);\n        const expiresAt = new Date();\n        expiresAt.setMinutes(expiresAt.getMinutes() + 60);\n\n        await db.passwordReset.create({\n            data: {\n                token,\n                userId: user.id,\n                email: user.email,\n                expiresAt\n            }\n        });\n\n        // Create reset link\n        const resetLink = `${resetBaseUrl}/${token}`;\n\n        // Set up SMTP transport\n        const transporterOptions: SMTPTransport.Options = {\n            host: systemConfig.smtpHost,\n            port: systemConfig.smtpPort!,\n            secure: systemConfig.smtpSecure || false,\n            auth: systemConfig.smtpUser && systemConfig.smtpPassword\n                ? {\n                    user: systemConfig.smtpUser,\n                    pass: systemConfig.smtpPassword,\n                }\n                : undefined,\n            tls: {\n                rejectUnauthorized: systemConfig.smtpSecure || false,\n            },\n        };\n\n        const transporter = createTransport(transporterOptions);\n\n        // Generate email\n        const emailComponent = PasswordResetEmail({\n            resetLink,\n            recipientName: user.name || undefined,\n            recipientEmail: user.email,\n            expiresInMinutes: 60\n        });\n\n        const htmlPromise = render(React.isValidElement(emailComponent) ? emailComponent : React.createElement(\"div\"), {pretty: true});\n        const textPromise = render(React.isValidElement(emailComponent) ? emailComponent : React.createElement(\"div\"), {plainText: true});\n\n        const html = await htmlPromise;\n        const text = await textPromise;\n\n        const mailOptions: SendMailOptions = {\n            from: `\"Changerawr\" <${systemConfig.systemEmail}>`,\n            to: user.email,\n            subject: 'Reset Your Password',\n            text,\n            html,\n        };\n\n        await transporter.sendMail(mailOptions);\n\n        return {\n            success: true,\n            userId: user.id,\n            message: 'Password reset email sent successfully'\n        };\n    } catch (error) {\n        console.error('Failed to send password reset email:', error);\n        return {\n            success: false,\n            message: 'Failed to send password reset email'\n        };\n    }\n}\n\n/**\n * Validates a password reset token\n */\nexport async function validatePasswordResetToken(token: string): Promise<{\n    valid: boolean;\n    userId?: string;\n    email?: string;\n    message?: string;\n}> {\n    try {\n        const passwordReset = await db.passwordReset.findUnique({\n            where: {token}\n        });\n\n        if (!passwordReset) {\n            return {\n                valid: false,\n                message: 'Invalid or expired reset token'\n            };\n        }\n\n        if (passwordReset.usedAt) {\n            return {\n                valid: false,\n                message: 'This reset token has already been used'\n            };\n        }\n\n        if (passwordReset.expiresAt < new Date()) {\n            return {\n                valid: false,\n                message: 'This reset token has expired'\n            };\n        }\n\n        return {\n            valid: true,\n            userId: passwordReset.userId,\n            email: passwordReset.email\n        };\n    } catch (error) {\n        console.error('Error validating password reset token:', error);\n        return {\n            valid: false,\n            message: 'Error validating password reset token'\n        };\n    }\n}\n\n/**\n * Resets a user's password using a valid token\n */\nexport async function resetPassword(token: string, newPassword: string): Promise<{\n    success: boolean;\n    message: string;\n}> {\n    try {\n        // Validate the token\n        const validation = await validatePasswordResetToken(token);\n\n        if (!validation.valid || !validation.userId) {\n            return {\n                success: false,\n                message: validation.message || 'Invalid reset token'\n            };\n        }\n\n        // Hash the new password\n        const {hashPassword} = await import('@/lib/auth/password');\n        const hashedPassword = await hashPassword(newPassword);\n\n        // Update the user's password\n        await db.user.update({\n            where: {id: validation.userId},\n            data: {password: hashedPassword}\n        });\n\n        // Mark the token as used\n        await db.passwordReset.update({\n            where: {token},\n            data: {usedAt: new Date()}\n        });\n\n        return {\n            success: true,\n            message: 'Password reset successful'\n        };\n    } catch (error) {\n        console.error('Error resetting password:', error);\n        return {\n            success: false,\n            message: 'Error resetting password'\n        };\n    }\n}"
  },
  {
    "path": "lib/services/bookmarks/bookmark.service.ts",
    "content": "// /lib/services/bookmarks/bookmark.service.ts\n\ninterface BookmarkedItem {\n    id: string;\n    title: string;\n    projectId: string;\n    bookmarkedAt: string;\n}\n\ninterface BookmarkStorage {\n    items: BookmarkedItem[];\n    lastUpdated: string;\n}\n\nexport class BookmarkService {\n    private static readonly STORAGE_PREFIX = 'bookmarked-';\n    private static readonly GLOBAL_BOOKMARKS_KEY = 'changerawr-global-bookmarks';\n\n    /**\n     * Get all bookmarks for a specific project\n     */\n    static getProjectBookmarks(projectId: string): BookmarkedItem[] {\n        try {\n            if (typeof window === 'undefined') return [];\n\n            const stored = localStorage.getItem(`${this.STORAGE_PREFIX}${projectId}`);\n            if (!stored) return [];\n\n            const parsed = JSON.parse(stored) as BookmarkedItem[];\n            return Array.isArray(parsed) ? parsed : [];\n        } catch (error) {\n            console.error('Failed to load project bookmarks:', error);\n            return [];\n        }\n    }\n\n    /**\n     * Get all bookmarks across all projects\n     */\n    static getAllBookmarks(): Record<string, BookmarkedItem[]> {\n        try {\n            if (typeof window === 'undefined') return {};\n\n            const stored = localStorage.getItem(this.GLOBAL_BOOKMARKS_KEY);\n            if (!stored) {\n                // Migrate from old storage format if needed\n                return this.migrateOldBookmarks();\n            }\n\n            const parsed = JSON.parse(stored) as BookmarkStorage;\n\n            // Group by project ID\n            const grouped: Record<string, BookmarkedItem[]> = {};\n            parsed.items.forEach(item => {\n                if (!grouped[item.projectId]) {\n                    grouped[item.projectId] = [];\n                }\n                grouped[item.projectId].push(item);\n            });\n\n            return grouped;\n        } catch (error) {\n            console.error('Failed to load all bookmarks:', error);\n            return {};\n        }\n    }\n\n    /**\n     * Check if a changelog entry is bookmarked\n     */\n    static isBookmarked(entryId: string, projectId: string): boolean {\n        const bookmarks = this.getProjectBookmarks(projectId);\n        return bookmarks.some(bookmark => bookmark.id === entryId);\n    }\n\n    /**\n     * Add a bookmark\n     */\n    static addBookmark(entryId: string, title: string, projectId: string): boolean {\n        try {\n            if (this.isBookmarked(entryId, projectId)) {\n                return false; // Already bookmarked\n            }\n\n            const newBookmark: BookmarkedItem = {\n                id: entryId,\n                title,\n                projectId,\n                bookmarkedAt: new Date().toISOString()\n            };\n\n            // Update project-specific storage\n            const projectBookmarks = this.getProjectBookmarks(projectId);\n            projectBookmarks.push(newBookmark);\n            localStorage.setItem(\n                `${this.STORAGE_PREFIX}${projectId}`,\n                JSON.stringify(projectBookmarks)\n            );\n\n            // Update global storage\n            this.updateGlobalBookmarks();\n\n            return true;\n        } catch (error) {\n            console.error('Failed to add bookmark:', error);\n            return false;\n        }\n    }\n\n    /**\n     * Remove a bookmark\n     */\n    static removeBookmark(entryId: string, projectId: string): boolean {\n        try {\n            const projectBookmarks = this.getProjectBookmarks(projectId);\n            const filtered = projectBookmarks.filter(bookmark => bookmark.id !== entryId);\n\n            if (filtered.length === projectBookmarks.length) {\n                return false; // Bookmark didn't exist\n            }\n\n            localStorage.setItem(\n                `${this.STORAGE_PREFIX}${projectId}`,\n                JSON.stringify(filtered)\n            );\n\n            // Update global storage\n            this.updateGlobalBookmarks();\n\n            return true;\n        } catch (error) {\n            console.error('Failed to remove bookmark:', error);\n            return false;\n        }\n    }\n\n    /**\n     * Toggle a bookmark (add if not exists, remove if exists)\n     */\n    static toggleBookmark(entryId: string, title: string, projectId: string): boolean {\n        const isCurrentlyBookmarked = this.isBookmarked(entryId, projectId);\n\n        if (isCurrentlyBookmarked) {\n            return this.removeBookmark(entryId, projectId);\n        } else {\n            return this.addBookmark(entryId, title, projectId);\n        }\n    }\n\n    /**\n     * Update bookmark title (in case the entry title changes)\n     */\n    static updateBookmarkTitle(entryId: string, newTitle: string, projectId: string): boolean {\n        try {\n            const projectBookmarks = this.getProjectBookmarks(projectId);\n            const bookmarkIndex = projectBookmarks.findIndex(bookmark => bookmark.id === entryId);\n\n            if (bookmarkIndex === -1) {\n                return false; // Bookmark doesn't exist\n            }\n\n            projectBookmarks[bookmarkIndex].title = newTitle;\n            localStorage.setItem(\n                `${this.STORAGE_PREFIX}${projectId}`,\n                JSON.stringify(projectBookmarks)\n            );\n\n            // Update global storage\n            this.updateGlobalBookmarks();\n\n            return true;\n        } catch (error) {\n            console.error('Failed to update bookmark title:', error);\n            return false;\n        }\n    }\n\n    /**\n     * Clear all bookmarks for a project\n     */\n    static clearProjectBookmarks(projectId: string): boolean {\n        try {\n            localStorage.removeItem(`${this.STORAGE_PREFIX}${projectId}`);\n            this.updateGlobalBookmarks();\n            return true;\n        } catch (error) {\n            console.error('Failed to clear project bookmarks:', error);\n            return false;\n        }\n    }\n\n    /**\n     * Get bookmark count for a project\n     */\n    static getProjectBookmarkCount(projectId: string): number {\n        return this.getProjectBookmarks(projectId).length;\n    }\n\n    /**\n     * Get recently bookmarked items across all projects\n     */\n    static getRecentBookmarks(limit: number = 10): BookmarkedItem[] {\n        const allBookmarks = this.getAllBookmarks();\n        const allItems: BookmarkedItem[] = [];\n\n        Object.values(allBookmarks).forEach(projectBookmarks => {\n            allItems.push(...projectBookmarks);\n        });\n\n        return allItems\n            .sort((a, b) => new Date(b.bookmarkedAt).getTime() - new Date(a.bookmarkedAt).getTime())\n            .slice(0, limit);\n    }\n\n    /**\n     * Search bookmarks by title\n     */\n    static searchBookmarks(query: string, projectId?: string): BookmarkedItem[] {\n        const searchTerm = query.toLowerCase().trim();\n        if (!searchTerm) return [];\n\n        let bookmarks: BookmarkedItem[] = [];\n\n        if (projectId) {\n            bookmarks = this.getProjectBookmarks(projectId);\n        } else {\n            const allBookmarks = this.getAllBookmarks();\n            Object.values(allBookmarks).forEach(projectBookmarks => {\n                bookmarks.push(...projectBookmarks);\n            });\n        }\n\n        return bookmarks.filter(bookmark =>\n            bookmark.title.toLowerCase().includes(searchTerm)\n        );\n    }\n\n    /**\n     * Export bookmarks as JSON\n     */\n    static exportBookmarks(): string {\n        const allBookmarks = this.getAllBookmarks();\n        return JSON.stringify({\n            exportedAt: new Date().toISOString(),\n            bookmarks: allBookmarks\n        }, null, 2);\n    }\n\n    /**\n     * Import bookmarks from JSON\n     */\n    static importBookmarks(jsonData: string): boolean {\n        try {\n            const parsed = JSON.parse(jsonData);\n            if (!parsed.bookmarks || typeof parsed.bookmarks !== 'object') {\n                throw new Error('Invalid bookmark data format');\n            }\n\n            Object.entries(parsed.bookmarks).forEach(([projectId, bookmarks]) => {\n                if (Array.isArray(bookmarks)) {\n                    localStorage.setItem(\n                        `${this.STORAGE_PREFIX}${projectId}`,\n                        JSON.stringify(bookmarks)\n                    );\n                }\n            });\n\n            this.updateGlobalBookmarks();\n            return true;\n        } catch (error) {\n            console.error('Failed to import bookmarks:', error);\n            return false;\n        }\n    }\n\n    /**\n     * Update the global bookmarks storage from all project-specific storages\n     */\n    private static updateGlobalBookmarks(): void {\n        try {\n            const allItems: BookmarkedItem[] = [];\n\n            // Collect all bookmarks from project-specific storage\n            for (let i = 0; i < localStorage.length; i++) {\n                const key = localStorage.key(i);\n                if (key?.startsWith(this.STORAGE_PREFIX) && key !== this.GLOBAL_BOOKMARKS_KEY) {\n                    const projectBookmarks = JSON.parse(localStorage.getItem(key) || '[]');\n                    if (Array.isArray(projectBookmarks)) {\n                        allItems.push(...projectBookmarks);\n                    }\n                }\n            }\n\n            const globalStorage: BookmarkStorage = {\n                items: allItems,\n                lastUpdated: new Date().toISOString()\n            };\n\n            localStorage.setItem(this.GLOBAL_BOOKMARKS_KEY, JSON.stringify(globalStorage));\n        } catch (error) {\n            console.error('Failed to update global bookmarks:', error);\n        }\n    }\n\n    /**\n     * Migrate from old storage format to new global format\n     */\n    private static migrateOldBookmarks(): Record<string, BookmarkedItem[]> {\n        const grouped: Record<string, BookmarkedItem[]> = {};\n\n        try {\n            for (let i = 0; i < localStorage.length; i++) {\n                const key = localStorage.key(i);\n                if (key?.startsWith(this.STORAGE_PREFIX)) {\n                    const projectId = key.replace(this.STORAGE_PREFIX, '');\n                    const bookmarks = JSON.parse(localStorage.getItem(key) || '[]');\n\n                    if (Array.isArray(bookmarks)) {\n                        // Ensure bookmarks have the bookmarkedAt field\n                        const migratedBookmarks = bookmarks.map(bookmark => ({\n                            ...bookmark,\n                            bookmarkedAt: bookmark.bookmarkedAt || new Date().toISOString()\n                        }));\n\n                        grouped[projectId] = migratedBookmarks;\n\n                        // Update the project storage with migrated data\n                        localStorage.setItem(key, JSON.stringify(migratedBookmarks));\n                    }\n                }\n            }\n\n            // Create global storage\n            this.updateGlobalBookmarks();\n        } catch (error) {\n            console.error('Failed to migrate bookmarks:', error);\n        }\n\n        return grouped;\n    }\n}\n\nexport type { BookmarkedItem };"
  },
  {
    "path": "lib/services/changelog/rss.ts",
    "content": "import { ChangelogEntry } from '@prisma/client'\n\ninterface RSSFeedOptions {\n    title: string\n    description: string\n    link: string\n    language?: string\n    copyright?: string\n    ttl?: number\n}\n\nexport function generateRSSFeed(entries: ChangelogEntry[], options: RSSFeedOptions): string {\n    const {\n        title,\n        description,\n        link,\n        language = 'en-US',\n        copyright = `Copyright ${new Date().getFullYear()}`,\n        ttl = 60\n    } = options\n\n    const items = entries\n        .map(entry => {\n            const pubDate = entry.publishedAt\n                ? new Date(entry.publishedAt).toUTCString()\n                : new Date(entry.createdAt).toUTCString()\n\n            return `\n        <item>\n          <title><![CDATA[${entry.title}]]></title>\n          <description><![CDATA[${entry.content}]]></description>\n          <link>${link}/${entry.id}</link>\n          <guid isPermaLink=\"false\">${entry.id}</guid>\n          <pubDate>${pubDate}</pubDate>\n          ${entry.version ? `<version>${entry.version}</version>` : ''}\n        </item>\n      `.trim()\n        })\n        .join('\\n')\n\n    return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\">\n  <channel>\n    <title><![CDATA[${title}]]></title>\n    <description><![CDATA[${description}]]></description>\n    <link>${link}</link>\n    <language>${language}</language>\n    <ttl>${ttl}</ttl>\n    <copyright>${copyright}</copyright>\n    <atom:link href=\"${link}/rss.xml\" rel=\"self\" type=\"application/rss+xml\"/>\n    <lastBuildDate>${new Date().toUTCString()}</lastBuildDate>\n    ${items}\n  </channel>\n</rss>`.trim()\n}"
  },
  {
    "path": "lib/services/core/markdown/EXTENSIONS.md",
    "content": "# Custom Markdown Extensions\n\nThis directory contains custom extensions for the `@changerawr/markdown` package that add extra markdown syntax support.\n\n## Available Extensions\n\n### 1. Table Extension\n\nSupports GFM-style markdown tables with alignment control.\n\n**Location:** `lib/services/core/markdown/extensions/table/table.ts`\n\n**Syntax:**\n```markdown\n| Header 1 | Header 2 | Header 3 |\n|----------|----------|----------|\n| Cell 1   | Cell 2   | Cell 3   |\n| Cell 4   | Cell 5   | Cell 6   |\n```\n\n**With Alignment:**\n```markdown\n| Left | Center | Right |\n|:-----|:------:|------:|\n| L1   | C1     | R1    |\n| L2   | C2     | R2    |\n```\n\n**Alignment Markers:**\n- `:-----` = Left aligned\n- `:-----:` = Center aligned\n- `-----:` = Right aligned\n- `-----` = Default (left)\n\n**Features:**\n- Parses headers and column alignment\n- Renders with Tailwind CSS styling\n- Alternating row colors for readability\n- Dark mode support\n- Responsive with overflow handling\n\n---\n\n### 2. Subtext Extension\n\nSupports Discord-style subtext for smaller, muted secondary information.\n\n**Location:** `lib/services/core/markdown/extensions/subtext/subtext.ts`\n\n**Syntax:**\n```markdown\n-# This is smaller muted text for supporting details\n```\n\n**Example Usage:**\n```markdown\n## Main Content\n\nRegular paragraph with important information.\n\n-# This is supporting detail text that appears smaller and more muted\n```\n\n**Features:**\n- Simple `-# text` syntax (Discord-style)\n- Renders as small gray text\n- Dark mode support\n- Block-level element with proper spacing\n\n---\n\n## Using the Extensions\n\n### Automatic (Recommended)\n\nAll markdown rendering throughout the app automatically includes the custom extensions. Just import from our extension module:\n\n```typescript\nimport { renderMarkdown, parseMarkdown } from '@/lib/services/core/markdown/useCustomExtensions';\n\n// Render markdown with all extensions enabled\nconst html = renderMarkdown(`\n| Name | Age |\n|------|-----|\n| John | 25  |\n| Jane | 30  |\n\n-# Last updated today\n`);\n\n// Parse markdown into tokens with all extensions enabled\nconst tokens = parseMarkdown(markdownContent);\n```\n\n**How it works:**\n- Singleton engine instance created on first use\n- All custom extensions registered automatically\n- Reused for subsequent calls (efficient)\n\n### Manual Engine Creation\n\n```typescript\nimport { createEngineWithExtensions } from '@/lib/services/core/markdown/useCustomExtensions';\n\nconst engine = createEngineWithExtensions();\nconst html = engine.toHtml(markdownContent);\nconst tokens = engine.parse(markdownContent);\n```\n\n### Reset for Testing\n\n```typescript\nimport { resetEngineInstance } from '@/lib/services/core/markdown/useCustomExtensions';\n\n// Clear singleton instance (useful in tests)\nresetEngineInstance();\n```\n\n---\n\n## Adding More Extensions\n\nTo add a new extension:\n\n1. Create a new directory: `lib/services/core/markdown/extensions/[extensionName]/`\n2. Create the extension file: `[extensionName].ts`\n3. Export the extension object following the `Extension` interface\n4. Add to `lib/services/core/markdown/extensions/index.ts`\n\n### Extension Template\n\n```typescript\nimport { Extension, MarkdownToken } from '@changerawr/markdown';\n\nconst myExtension: Extension = {\n  name: 'my-feature',\n  parseRules: [\n    {\n      name: 'my-feature',\n      pattern: /pattern-to-match/,\n      render: (match): MarkdownToken => ({\n        type: 'my-token-type',\n        content: match[1],\n        raw: match[0],\n        attributes: { /* optional */ }\n      })\n    }\n  ],\n  renderRules: [\n    {\n      type: 'my-token-type',\n      render: (token): string => {\n        return `<custom-element>${token.content}</custom-element>`;\n      }\n    }\n  ]\n};\n\nexport { myExtension };\n```\n\n---\n\n## Implementation Notes\n\n- **Priority:** Extensions are registered before default markdown rules, giving them higher priority\n- **Patterns:** Use regex patterns with capturing groups `(...)` to extract content\n- **HTML Rendering:** Always use Tailwind CSS classes for consistent styling\n- **Dark Mode:** Include `dark:` prefixed classes for dark mode support\n- **Type Safety:** All extensions are TypeScript-first for IDE support\n\n---\n\n## Testing Extensions\n\nTo test extensions during development:\n\n```typescript\nimport { renderMarkdownWithExtensions, parseMarkdownWithExtensions } from '@/lib/services/core/markdown/useCustomExtensions';\n\n// Test rendering\nconst markdown = `\n| Header |\n|--------|\n| Cell   |\n\n-# Subtext\n`;\n\nconst html = renderMarkdownWithExtensions(markdown);\nconsole.log(html);\n\n// Test parsing\nconst tokens = parseMarkdownWithExtensions(markdown);\nconsole.log(tokens);\n```"
  },
  {
    "path": "lib/services/core/markdown/EXTENSIONS_SETUP.md",
    "content": "# Custom Markdown Extensions - Setup Complete ✅\n\n## Summary\n\nSuccessfully created and integrated two custom markdown extensions for the Changerawr markdown engine:\n\n1. **Table Extension** - GFM-style markdown tables with alignment support\n2. **Subtext Extension** - Discord-style subtext using `-# text` syntax\n\n## Architecture\n\n### File Structure\n\n```\nlib/services/core/markdown/\n├── extensions/\n│   ├── index.ts                    # Central registry of all extensions\n│   ├── table/\n│   │   └── table.ts               # Table extension definition\n│   └── subtext/\n│       └── subtext.ts             # Subtext extension definition\n├── useCustomExtensions.ts          # Singleton engine with extensions\n├── EXTENSIONS.md                   # User-facing documentation\n└── EXTENSIONS_SETUP.md            # This file\n```\n\n### Extension System Flow\n\n```\nrenderMarkdown(markdown)\n    ↓\ngetMarkdownEngine() (singleton)\n    ↓\nChangerawrMarkdown instance\n    ├─ registerExtension(tableExtension)\n    ├─ registerExtension(subtextExtension)\n    └─ (extensions cached and reused)\n    ↓\ntoHtml(markdown) with all extensions enabled\n```\n\n## Implementation Details\n\n### Singleton Engine Instance\n\nThe `useCustomExtensions.ts` module creates a single reusable engine instance with all custom extensions pre-registered:\n\n```typescript\n// First call: Creates engine and registers extensions\nrenderMarkdown(content1);\n\n// Subsequent calls: Reuse existing instance\nrenderMarkdown(content2);\nrenderMarkdown(content3);\n```\n\n**Benefits:**\n- Efficient: Extension registration happens once\n- Consistent: Same engine instance used throughout app\n- Testable: Can reset with `resetEngineInstance()`\n\n### Extension Priority\n\nCustom extensions are registered with feature extension priority, meaning they're matched before default markdown rules. This ensures:\n\n- Tables are recognized before falling back to paragraph parsing\n- Subtext `-# text` patterns are caught before being treated as regular text/lists\n\n### Integration Points\n\nAll markdown rendering components now use the custom extension system:\n\n1. **RenderMarkdown.tsx** - Display component\n2. **MarkdownPreview.tsx** - Preview component\n3. **MarkdownEditor.tsx** - Full editor with preview\n4. **CUMButtonModal.tsx** - Button preview modal\n\n## Extension Specifications\n\n### Table Extension\n\n**Name:** `table`\n\n**Pattern:** Matches GFM-style tables\n```\n| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1   | Cell 2   |\n```\n\n**Features:**\n- Column alignment (left, center, right)\n- Dark mode support\n- Alternating row colors\n- Responsive overflow scrolling\n- Tailwind CSS styling\n\n**Render Output:** HTML `<table>` with semantic markup\n\n---\n\n### Subtext Extension\n\n**Name:** `subtext`\n\n**Pattern:** Matches `-# text` format\n```\n-# This is smaller, muted text for secondary information\n```\n\n**Features:**\n- Discord-style syntax\n- Small gray text (text-sm)\n- Dark mode support (gray-400 in dark mode)\n- Block-level rendering with proper spacing\n\n**Render Output:** HTML `<p>` with small text styling\n\n---\n\n## Usage Examples\n\n### Basic Usage\n\n```typescript\nimport { renderMarkdown } from '@/lib/services/core/markdown/useCustomExtensions';\n\nconst markdown = `\n# Features\n\n| Feature | Status |\n|---------|--------|\n| Tables | ✅ |\n| Subtext | ✅ |\n\n-# Last updated November 2, 2025\n`;\n\nconst html = renderMarkdown(markdown);\n// Returns HTML with table and subtext rendered\n```\n\n### In React Components\n\n```typescript\nimport { renderMarkdown } from '@/lib/services/core/markdown/useCustomExtensions';\n\nexport function ChangelogEntry({ content }: { content: string }) {\n  const html = renderMarkdown(content);\n\n  return (\n    <div\n      className=\"prose\"\n      dangerouslySetInnerHTML={{ __html: html }}\n    />\n  );\n}\n```\n\n### Parsing Only\n\n```typescript\nimport { parseMarkdown } from '@/lib/services/core/markdown/useCustomExtensions';\n\nconst tokens = parseMarkdown(markdown);\n// Returns MarkdownToken[] with table and subtext tokens\n```\n\n## Technical Details\n\n### Extension Interface\n\nBoth extensions implement the `Extension` interface:\n\n```typescript\ninterface Extension {\n  name: string;                    // Unique identifier\n  parseRules: ParseRule[];         // How to recognize patterns\n  renderRules: RenderRule[];       // How to render tokens\n}\n```\n\n### Pattern Matching\n\n- **Table:** Regex matches header row + separator row + data rows\n- **Subtext:** Regex matches `-#` followed by text on same line\n\n### Rendering\n\n- **Table:** Generates semantic HTML table with Tailwind classes\n- **Subtext:** Generates `<p>` element with gray text classes\n\n## Testing the Extensions\n\n### Manual Testing\n\nTry these in the markdown editor:\n\n```markdown\n# Test Tables\n\n| Column 1 | Column 2 | Column 3 |\n|:---------|:--------:|----------:|\n| Left | Center | Right |\n| A | B | C |\n\n-# This is subtext supporting information\n\nRegular paragraph continues here.\n```\n\n### Unit Testing\n\n```typescript\nimport { renderMarkdown, resetEngineInstance } from '@/lib/services/core/markdown/useCustomExtensions';\n\ndescribe('Extensions', () => {\n  beforeEach(() => {\n    resetEngineInstance(); // Fresh engine for each test\n  });\n\n  it('renders tables', () => {\n    const html = renderMarkdown('| A | B |\\n|-|-|\\n| 1 | 2 |');\n    expect(html).toContain('<table>');\n  });\n\n  it('renders subtext', () => {\n    const html = renderMarkdown('-# Small text');\n    expect(html).toContain('text-sm');\n  });\n});\n```\n\n## Performance Considerations\n\n### Memory\n\n- Single singleton instance reduces memory overhead\n- Extensions registered once on first use\n- Subsequent calls reuse same instance\n\n### Speed\n\n- Pattern matching is O(n) for markdown length\n- Regex patterns optimized for early termination\n- No unnecessary re-parsing\n\n### Scalability\n\n- Easy to add more extensions to registry\n- Each extension isolated and independent\n- Extension priority system prevents conflicts\n\n## Troubleshooting\n\n### Extensions Not Rendering\n\n**Problem:** Tables or subtext appear as plain text\n\n**Solution:** Ensure you're importing from the correct module:\n```typescript\n// ✅ Correct\nimport { renderMarkdown } from '@/lib/services/core/markdown/useCustomExtensions';\n\n// ❌ Wrong\nimport { renderMarkdown } from '@changerawr/markdown';\n```\n\n### Pattern Not Matching\n\n**Problem:** Table/subtext syntax doesn't parse\n\n**Solution:** Check exact format:\n- Tables: Require pipe (`|`) delimiters and separator row\n- Subtext: Must start line with `-#` followed by space\n\n### HTML Injection Concerns\n\n**Status:** Safe - DOMPurify sanitization is enabled by default\n\n## Future Enhancements\n\nPossible extensions to add:\n- Footnotes\n- Task lists with custom styling\n- Math/LaTeX support\n- Mermaid diagrams\n- Callouts (styled alert blocks)\n- Strikethrough with custom styling\n- Superscript/subscript\n- Abbreviations\n\n## Files Modified\n\n1. Created: `lib/services/core/markdown/extensions/table/table.ts`\n2. Created: `lib/services/core/markdown/extensions/subtext/subtext.ts`\n3. Created: `lib/services/core/markdown/extensions/index.ts`\n4. Created: `lib/services/core/markdown/useCustomExtensions.ts`\n5. Updated: `components/markdown-editor/RenderMarkdown.tsx`\n6. Updated: `components/markdown-editor/MarkdownPreview.tsx`\n7. Updated: `components/markdown-editor/MarkdownEditor.tsx`\n8. Updated: `components/markdown-editor/modals/CUMButtonModal.tsx`\n9. Created: `lib/services/core/markdown/EXTENSIONS.md`\n10. Created: `lib/services/core/markdown/EXTENSIONS_SETUP.md` (this file)\n\n## Build Status\n\n✅ **Build Successful** - All changes compile without errors\n✅ **Type Safety** - Full TypeScript support\n✅ **Integration** - All components using custom extensions\n✅ **Documentation** - Complete setup and usage guides\n\n## Next Steps\n\n1. Test extensions in development environment\n2. Adjust styling as needed\n3. Consider additional extensions\n4. Add unit tests for extension patterns\n5. Monitor performance in production\n\n---\n\n**Date:** November 2, 2025\n**Status:** Complete and Ready for Use\n"
  },
  {
    "path": "lib/services/core/markdown/extensions/index.ts",
    "content": "/**\n * Custom Markdown Extensions for Changerawr\n *\n * This module exports all custom extensions that enhance the base markdown engine.\n */\n\nimport { Extension } from '@changerawr/markdown';\nimport { tableExtension } from './table/table';\nimport { subtextExtension } from './subtext/subtext';\n\n/**\n * Array of all custom extensions\n * Extensions are registered in order, with feature extensions taking priority\n */\nexport const customExtensions: Extension[] = [\n  tableExtension,\n  subtextExtension\n];\n\n/**\n * Get a specific extension by name\n */\nexport function getExtension(name: string): Extension | undefined {\n  return customExtensions.find(ext => ext.name === name);\n}\n\n/**\n * Get all extension names\n */\nexport function getExtensionNames(): string[] {\n  return customExtensions.map(ext => ext.name);\n}\n\n/**\n * Export individual extensions for selective use\n */\nexport { tableExtension } from './table/table';\nexport { subtextExtension } from './subtext/subtext';"
  },
  {
    "path": "lib/services/core/markdown/extensions/subtext/subtext.ts",
    "content": "import { Extension, MarkdownToken } from '@changerawr/markdown';\n\n/**\n * Subtext Extension for Changerawr Markdown\n *\n * Supports Discord-style subtext for smaller, muted secondary information.\n *\n * Syntax: -# text\n *\n * Examples:\n * -# This is smaller muted text\n * Regular paragraph above\n * -# Supporting details below\n */\n\nconst subtextExtension: Extension = {\n  name: 'subtext',\n  parseRules: [\n    {\n      name: 'subtext-line',\n      // Matches -# text (Discord subtext format) - must match at line start\n      pattern: /-#\\s+([^\\n]+)/,\n      render: (match): MarkdownToken => {\n        return {\n          type: 'subtext',\n          content: match[1],\n          raw: match[0],\n          attributes: {\n            variant: 'inline'\n          }\n        };\n      }\n    }\n  ],\n  renderRules: [\n    {\n      type: 'subtext',\n      render: (token): string => {\n        return `<p class=\"text-sm text-gray-600 dark:text-gray-400 my-2\">${token.content}</p>`;\n      }\n    }\n  ]\n};\n\nexport { subtextExtension };"
  },
  {
    "path": "lib/services/core/markdown/extensions/table/table.ts",
    "content": "import {Extension, MarkdownToken} from '@changerawr/markdown';\n\n/**\n * Table Extension for Changerawr Markdown\n *\n * Supports GFM-style tables:\n *\n * | Header 1 | Header 2 |\n * |----------|----------|\n * | Cell 1   | Cell 2   |\n * | Cell 3   | Cell 4   |\n *\n * Also supports alignment:\n * | Left | Center | Right |\n * |:-----|:------:|------:|\n * | L    | C      | R     |\n */\n\nconst tableExtension: Extension = {\n    name: 'table',\n    parseRules: [\n        {\n            name: 'table',\n            // Matches GFM-style tables: header row, separator row, then data rows\n            // Pattern must capture the full table including newlines\n            pattern: /\\|[^\\n]+\\n\\|[\\s\\-:|]+\\n(\\|[^\\n]+\\n?)*/,\n            render: (match): MarkdownToken => {\n                const fullTable = match[0];\n                const lines = fullTable.trim().split('\\n');\n\n                // First line is headers\n                const headerRow = lines[0];\n                // Lines 2+ are data rows\n                const bodyRowsText = lines.slice(2).join('\\n');\n\n                // Parse header row\n                const headers = headerRow\n                    .split('|')\n                    .map(h => h.trim())\n                    .filter(h => h.length > 0);\n\n                // Parse alignment from separator row\n                const separatorLine = fullTable.split('\\n')[1];\n                const alignments = separatorLine\n                    .split('|')\n                    .map(sep => sep.trim())\n                    .filter(sep => sep.length > 0)\n                    .map(sep => {\n                        if (sep.startsWith(':') && sep.endsWith(':')) return 'center';\n                        if (sep.endsWith(':')) return 'right';\n                        if (sep.startsWith(':')) return 'left';\n                        return 'left';\n                    });\n\n                // Parse body rows\n                const rows = bodyRowsText\n                    .split('\\n')\n                    .filter(row => row.trim().length > 0)\n                    .map(row =>\n                        row\n                            .split('|')\n                            .map(cell => cell.trim())\n                            .filter(cell => cell.length > 0)\n                    );\n\n                return {\n                    type: 'table',\n                    content: fullTable,\n                    raw: fullTable,\n                    attributes: {\n                        headers: headers.join('||'),\n                        alignments: alignments.join('||'),\n                        rowCount: rows.length,\n                        columnCount: headers.length,\n                        bodyRows: JSON.stringify(rows)\n                    }\n                };\n            }\n        }\n    ],\n    renderRules: [\n        {\n            type: 'table',\n            render: (token): string => {\n                const headers = (token.attributes?.headers as string)?.split('||') || [];\n                const alignments = (token.attributes?.alignments as string)?.split('||') || [];\n                const bodyRowsJson = token.attributes?.bodyRows as string;\n                const bodyRows = bodyRowsJson ? JSON.parse(bodyRowsJson) : [];\n\n                // Build table HTML with Tailwind styling\n                let html = '<div class=\"overflow-x-auto my-4\"><table class=\"border-collapse border border-gray-300 dark:border-gray-700\">';\n\n                // Header row\n                html += '<thead class=\"bg-gray-100 dark:bg-gray-800\">';\n                html += '<tr>';\n                headers.forEach((header, idx) => {\n                    const align = alignments[idx] || 'left';\n                    const textAlign =\n                        align === 'center' ? 'text-center' :\n                            align === 'right' ? 'text-right' :\n                                'text-left';\n                    html += `<th class=\"border border-gray-300 dark:border-gray-700 px-4 py-2 font-semibold ${textAlign}\">${header}</th>`;\n                });\n                html += '</tr>';\n                html += '</thead>';\n\n                // Body rows\n                html += '<tbody>';\n                bodyRows.forEach((row: string[], rowIdx: number) => {\n                    html += '<tr class=\"' + (rowIdx % 2 === 0 ? 'bg-white dark:bg-gray-900' : 'bg-gray-50 dark:bg-gray-800/50') + '\">';\n                    row.forEach((cell, cellIdx) => {\n                        const align = alignments[cellIdx] || 'left';\n                        const textAlign =\n                            align === 'center' ? 'text-center' :\n                                align === 'right' ? 'text-right' :\n                                    'text-left';\n                        html += `<td class=\"border border-gray-300 dark:border-gray-700 px-4 py-2 ${textAlign}\">${cell}</td>`;\n                    });\n                    html += '</tr>';\n                });\n                html += '</tbody>';\n\n                html += '</table></div>';\n                return html;\n            }\n        }\n    ]\n};\n\nexport {tableExtension};"
  },
  {
    "path": "lib/services/core/markdown/useCustomExtensions.ts",
    "content": "/**\n * Hook/utility to register custom extensions with the markdown engine\n *\n * Usage:\n * import { renderMarkdown, parseMarkdown } from '@/lib/services/core/markdown/useCustomExtensions';\n *\n * const html = renderMarkdown(markdownContent);\n * const tokens = parseMarkdown(markdownContent);\n */\n\nimport { ChangerawrMarkdown, EngineConfig } from '@changerawr/markdown';\nimport { customExtensions } from './extensions';\n\n// Singleton instance with all custom extensions registered\nlet engineInstance: ChangerawrMarkdown | null = null;\n\n/**\n * Get or create the singleton markdown engine with all custom extensions\n */\nfunction getMarkdownEngine(): ChangerawrMarkdown {\n  if (!engineInstance) {\n    engineInstance = new ChangerawrMarkdown();\n\n    // Register all custom extensions (feature extensions have priority)\n    customExtensions.forEach(extension => {\n      const result = engineInstance!.registerExtension(extension);\n      if (!result.success) {\n        console.warn(`Failed to register extension '${extension.name}':`, result.error);\n      }\n    });\n  }\n\n  return engineInstance;\n}\n\n/**\n * Render markdown with all custom extensions\n * This is the main function to use for rendering markdown throughout the app\n *\n * Note: The engine automatically caches rendered HTML internally using LRU caching.\n * No need for manual memoization - the engine handles it!\n */\nexport function renderMarkdown(markdown: string): string {\n  const engine = getMarkdownEngine();\n  return engine.toHtml(markdown);\n}\n\n/**\n * Render markdown with performance metrics\n * Returns both HTML and detailed performance data\n */\nexport function renderMarkdownWithMetrics(markdown: string): { html: string; metrics: { parseTime: number; renderTime: number; totalTime: number; cacheHit: boolean } } {\n  const engine = getMarkdownEngine();\n  return engine.toHtmlWithMetrics(markdown);\n}\n\n/**\n * Render markdown with streaming for large documents\n * Provides progress callbacks for UI updates\n */\nexport async function renderMarkdownStreamed(\n  markdown: string,\n  options?: {\n    chunkSize?: number;\n    onProgress?: (progress: { html: string; progress: number }) => void;\n  }\n): Promise<string> {\n  const engine = getMarkdownEngine();\n  return engine.toHtmlStreamed(markdown, {\n    chunkSize: options?.chunkSize || 50,\n    onChunk: options?.onProgress\n  });\n}\n\n/**\n * Get cache statistics from the engine\n */\nexport function getCacheStats() {\n  const engine = getMarkdownEngine();\n  return engine.getCacheStats();\n}\n\n/**\n * Clear all caches in the engine\n */\nexport function clearCaches() {\n  const engine = getMarkdownEngine();\n  engine.clearCaches();\n}\n\n/**\n * Parse markdown with all custom extensions into tokens\n */\nexport function parseMarkdown(markdown: string) {\n  const engine = getMarkdownEngine();\n  return engine.parse(markdown);\n}\n\n/**\n * Create a fresh engine instance (useful for testing)\n */\nexport function createEngineWithExtensions(config?: EngineConfig): ChangerawrMarkdown {\n  const engine = new ChangerawrMarkdown(config);\n\n  // Register all custom extensions\n  customExtensions.forEach(extension => {\n    const result = engine.registerExtension(extension);\n    if (!result.success) {\n      console.warn(`Failed to register extension '${extension.name}':`, result.error);\n    }\n  });\n\n  return engine;\n}\n\n/**\n * Reset the singleton instance (useful for testing)\n */\nexport function resetEngineInstance(): void {\n  engineInstance = null;\n}"
  },
  {
    "path": "lib/services/core/system-user/README.md",
    "content": "# System User Service\n\n## Overview\n\nThe System User service manages a special internal user account (`system@changerawr.sys`) that is used for system-level operations that require user attribution in audit logs and other tracking mechanisms.\n\n## Purpose\n\nMany operations in Changerawr require a user ID for foreign key constraints and audit trails:\n- Scheduled job execution (telemetry, email digests, etc.)\n- Background tasks\n- System-initiated actions\n- Automated processes\n\nInstead of creating nullable foreign keys or special handling for these cases, we use a dedicated system user that exists in the database like any other user but has special characteristics.\n\n## System User Characteristics\n\n- **ID**: `system` (constant, never changes)\n- **Email**: `system@changerawr.sys`\n- **Password**: Empty string (cannot login via normal auth)\n- **Name**: \"System\"\n- **Role**: `ADMIN` (has necessary permissions but filtered from UI)\n- **Not deletable**: Frontend should filter this user from management interfaces\n- **Not visible**: Should not appear in user lists or selection dropdowns\n\n## Usage\n\n### Initialize at Startup\n\nThe system user is automatically created during application startup if it doesn't exist:\n\n```typescript\nimport {ensureSystemUser} from '@/lib/services/core/system-user/service';\n\n// In app startup file\nawait ensureSystemUser();\n```\n\n### Use in Audit Logs\n\nWhen creating audit logs for system operations, use the `SYSTEM_USER_ID` constant:\n\n```typescript\nimport {SYSTEM_USER_ID} from '@/lib/services/core/system-user/service';\nimport {createAuditLog} from '@/lib/utils/auditLog';\n\nawait createAuditLog(\n    'SCHEDULED_JOB_EXECUTED',\n    SYSTEM_USER_ID,  // performedById\n    null,            // targetUserId\n    {\n        jobId: job.id,\n        jobType: job.type,\n    }\n);\n```\n\n### Check if User is System\n\n```typescript\nimport {isSystemUser} from '@/lib/services/core/system-user/service';\n\nif (isSystemUser(userId)) {\n    // Filter from UI, prevent deletion, etc.\n}\n```\n\n## Frontend Considerations\n\nWhen building user management interfaces:\n\n1. **Filter from lists**: Use `isSystemUser()` or filter by email `system@changerawr.sys`\n2. **Prevent deletion**: Block delete operations on the system user\n3. **Hide from selection**: Don't show in user dropdowns or assignment pickers\n4. **Read-only**: Don't allow editing of system user properties\n\n## Database Schema\n\nThe system user follows the standard `User` model:\n\n```prisma\nmodel User {\n  id       String @id @default(cuid())\n  email    String @unique\n  password String\n  name     String?\n  role     Role   @default(STAFF)\n  // ... other fields\n}\n```\n\n## Migration Notes\n\nIf you have existing installations, the system user will be created automatically on next startup. No manual migration is required.\n\n## Security\n\n- The system user has an empty password and cannot authenticate via normal login flows\n- API key authentication should also reject system user credentials\n- OAuth connections cannot be linked to the system user\n- 2FA cannot be enabled for the system user"
  },
  {
    "path": "lib/services/core/system-user/service.ts",
    "content": "import {db} from '@/lib/db';\nimport {Role} from '@prisma/client';\n\n/**\n * System user ID - a well-known constant\n */\nexport const SYSTEM_USER_ID = 'system';\n\n/**\n * System user configuration\n */\nconst SYSTEM_USER = {\n    id: SYSTEM_USER_ID,\n    email: 'system@changerawr.sys',\n    password: '', // No password - cannot login\n    name: 'System',\n    role: Role.ADMIN,\n} as const;\n\n/**\n * Ensure the system user exists in the database.\n * This user is used for audit logs and other system-level operations.\n *\n * @returns The system user ID\n */\nexport async function ensureSystemUser(): Promise<string> {\n    try {\n        // Try to find existing system user\n        const existingUser = await db.user.findUnique({\n            where: {id: SYSTEM_USER_ID}\n        });\n\n        if (existingUser) {\n            return SYSTEM_USER_ID;\n        }\n\n        // Create system user if it doesn't exist\n        await db.user.create({\n            data: SYSTEM_USER\n        });\n\n        console.log('✓ Created system user');\n        return SYSTEM_USER_ID;\n\n    } catch (error) {\n        // If user already exists (race condition during parallel execution), that's fine\n        if (error && typeof error === 'object' && 'code' in error && error.code === 'P2002') {\n            return SYSTEM_USER_ID;\n        }\n        console.error('Failed to ensure system user exists:', error);\n        throw error;\n    }\n}\n\n/**\n * Check if a user ID is the system user\n */\nexport function isSystemUser(userId: string): boolean {\n    return userId === SYSTEM_USER_ID;\n}"
  },
  {
    "path": "lib/services/easypanel/index.ts",
    "content": "// lib/services/easypanel.ts (Updated)\n\nimport { EasypanelConfig, EasypanelUpdateImagePayload, EasypanelDeployPayload, EasypanelApiResponse } from '@/lib/types/easypanel';\nimport { generateDockerImage, validateDockerImage } from '@/lib/utils/docker';\n\nexport class EasypanelService {\n    private config: EasypanelConfig;\n\n    constructor(config: EasypanelConfig) {\n        this.config = config;\n    }\n\n    /**\n     * Check if Easypanel is properly configured\n     */\n    static isConfigured(): boolean {\n        return !!(\n            process.env.EASYPANEL_PROJECT_ID &&\n            process.env.EASYPANEL_SERVICE_ID &&\n            process.env.EASYPANEL_PANEL_URL &&\n            process.env.EASYPANEL_API_KEY\n        );\n    }\n\n    /**\n     * Create an instance from environment variables\n     */\n    static fromEnv(): EasypanelService | null {\n        if (!this.isConfigured()) {\n            return null;\n        }\n\n        return new EasypanelService({\n            projectId: process.env.EASYPANEL_PROJECT_ID!,\n            serviceId: process.env.EASYPANEL_SERVICE_ID!,\n            panelUrl: process.env.EASYPANEL_PANEL_URL!,\n            apiKey: process.env.EASYPANEL_API_KEY!,\n        });\n    }\n\n    /**\n     * Make a request to the Easypanel API\n     */\n    private async makeRequest<T>(\n        endpoint: string,\n        method: 'GET' | 'POST' = 'POST',\n        body?: unknown\n    ): Promise<T> {\n        const url = `${this.config.panelUrl}/api/trpc/${endpoint}`;\n\n        const response = await fetch(url, {\n            method,\n            headers: {\n                'Content-Type': 'application/json',\n                'Authorization': `Bearer ${this.config.apiKey}`,\n            },\n            body: body ? JSON.stringify(body) : undefined,\n        });\n\n        if (!response.ok) {\n            throw new Error(`Easypanel API request failed: ${response.status} ${response.statusText}`);\n        }\n\n        const data: EasypanelApiResponse<T> = await response.json();\n\n        if (data.error) {\n            throw new Error(`Easypanel API error: ${data.error.message}`);\n        }\n\n        return data.result?.data as T;\n    }\n\n    /**\n     * Update the Docker image for the service\n     */\n    async updateImage(newImage: string): Promise<void> {\n        // Validate the Docker image format\n        const validation = validateDockerImage(newImage);\n        if (!validation.valid) {\n            throw new Error(`Invalid Docker image format: ${validation.error}`);\n        }\n\n        const payload: EasypanelUpdateImagePayload = {\n            json: {\n                projectName: this.config.projectId,\n                serviceName: this.config.serviceId,\n                image: newImage,\n            },\n        };\n\n        console.log(`Updating image to: ${newImage}`);\n        await this.makeRequest('services.app.updateSourceImage', 'POST', payload);\n    }\n\n    /**\n     * Deploy the service with the new image\n     */\n    async deployService(): Promise<void> {\n        const payload: EasypanelDeployPayload = {\n            json: {\n                projectName: this.config.projectId,\n                serviceName: this.config.serviceId,\n                forceRebuild: true,\n            },\n        };\n\n        console.log('Deploying service with force rebuild...');\n        await this.makeRequest('services.app.deployService', 'POST', payload);\n    }\n\n    /**\n     * Perform a complete update: change image and deploy\n     */\n    async performUpdate(version: string, customImage?: string): Promise<void> {\n        console.log(`Starting Easypanel update to version: ${version}`);\n\n        // Generate or use custom Docker image\n        const dockerImage = customImage || generateDockerImage(version);\n\n        console.log(`Using Docker image: ${dockerImage}`);\n\n        // Step 1: Update the image\n        console.log('Step 1: Updating Docker image...');\n        await this.updateImage(dockerImage);\n\n        // Small delay to ensure the image update is processed\n        await new Promise(resolve => setTimeout(resolve, 2000));\n\n        // Step 2: Deploy the service\n        console.log('Step 2: Deploying service...');\n        await this.deployService();\n\n        console.log('Easypanel update completed successfully');\n    }\n\n    /**\n     * Test the connection to Easypanel\n     */\n    async testConnection(): Promise<boolean> {\n        try {\n            // Try to make a simple request to verify connectivity\n            await this.makeRequest('users.listUsers', 'GET');\n            return true;\n        } catch (error) {\n            console.error('Easypanel connection test failed:', error);\n            return false;\n        }\n    }\n\n    /**\n     * Get service information\n     */\n    async getServiceInfo(): Promise<unknown> {\n        return this.makeRequest('services.app.get', 'POST', {\n            json: {\n                projectName: this.config.projectId,\n                serviceName: this.config.serviceId,\n            }\n        });\n    }\n\n    /**\n     * Get the configuration for debugging\n     */\n    getConfig(): { projectId: string; serviceId: string; panelUrl: string; apiKey: string } {\n        return {\n            projectId: this.config.projectId,\n            serviceId: this.config.serviceId,\n            panelUrl: this.config.panelUrl,\n            apiKey: '[REDACTED]',\n        };\n    }\n\n    /**\n     * Generate recommended Docker image for a version\n     */\n    static generateRecommendedImage(version: string): string {\n        return generateDockerImage(version);\n    }\n}"
  },
  {
    "path": "lib/services/email/notification.ts",
    "content": "import { isValidElement } from 'react';\nimport { render } from '@react-email/render';\nimport { createTransport, SendMailOptions } from 'nodemailer';\nimport { db } from '@/lib/db';\nimport { ChangelogEmail } from '@/emails/changelog';\nimport SMTPTransport from \"nodemailer/lib/smtp-transport\";\nimport { nanoid } from 'nanoid';\nimport ApprovalNotificationEmail from \"@/emails/approval-notification\";\nimport RejectionNotificationEmail from \"@/emails/rejection-notification\";\n\nexport interface SendEmailParams {\n    projectId: string;\n    subject?: string;\n    changelogEntryId?: string;\n    recipients?: string[];\n    isDigest?: boolean;\n    subscriberIds?: string[];\n    customDomain?: string;  // custom domain (optional)\n}\n\nexport interface EmailResult {\n    success: boolean;\n    messageId: string;\n    recipientCount: number;\n}\n\ninterface NotificationRequestInfo {\n    type: string;\n    projectName: string;\n    entryTitle?: string;\n    adminName?: string;\n}\n\ninterface SendNotificationParams {\n    userId: string;\n    status: 'APPROVED' | 'REJECTED' | 'PENDING';\n    request: NotificationRequestInfo;\n    dashboardUrl: string;\n}\n\ninterface SubscriberWithSubscriptions {\n    id: string;\n    email: string;\n    name: string | null;\n    unsubscribeToken: string;\n    subscriptions: Array<{\n        customDomain: string | null;\n    }>;\n}\n\ninterface ManageSubscriptionParams {\n    email: string;\n    projectId: string;\n    subscriptionType?: 'ALL_UPDATES' | 'MAJOR_ONLY' | 'DIGEST_ONLY';\n    unsubscribe?: boolean;\n}\n\ninterface SubscriptionResult {\n    success: boolean;\n    message: string;\n}\n\n/**\n * Sends changelog emails to specified recipients or subscribers\n */\nexport async function sendChangelogEmail(params: SendEmailParams): Promise<EmailResult> {\n    try {\n        const { projectId, subject, changelogEntryId, recipients, isDigest, subscriberIds, customDomain } = params;\n\n        // Get project and email config\n        const project = await db.project.findUnique({\n            where: { id: projectId },\n            include: {\n                emailConfig: true,\n                changelog: {\n                    include: {\n                        entries: changelogEntryId\n                            ? {\n                                where: { id: changelogEntryId },\n                                include: { tags: true }\n                            }\n                            : {\n                                orderBy: { createdAt: 'desc' },\n                                take: isDigest ? 5 : 1,\n                                include: { tags: true }\n                            }\n                    }\n                }\n            }\n        });\n\n        if (!project?.emailConfig?.enabled) {\n            throw new Error('Email notifications are not enabled for this project');\n        }\n\n        const { emailConfig } = project;\n\n        // Make sure there are entries to send\n        if (!project.changelog || !project.changelog.entries.length) {\n            throw new Error('No changelog entries found to send');\n        }\n\n        // Create transporter\n        const transporterOptions: SMTPTransport.Options = {\n            host: emailConfig.smtpHost,\n            port: emailConfig.smtpPort,\n            secure: emailConfig.smtpSecure,\n            auth: emailConfig.smtpUser && emailConfig.smtpPassword\n                ? {\n                    user: emailConfig.smtpUser,\n                    pass: emailConfig.smtpPassword,\n                }\n                : undefined,\n            tls: {\n                rejectUnauthorized: emailConfig.smtpSecure,\n            },\n        };\n\n        const transporter = createTransport(transporterOptions);\n\n        const emailSubject = subject || emailConfig.defaultSubject || 'New Changelog Updates';\n\n        // Determine recipients\n        let emailRecipients: string[] = [];\n        const finalSubscriberIds: string[] = [];\n\n        // Process direct recipients if provided\n        if (recipients && recipients.length > 0) {\n            emailRecipients = [...recipients];\n\n            // Check if any of these recipients are already subscribers\n            const existingSubscribers = await db.emailSubscriber.findMany({\n                where: {\n                    email: { in: recipients },\n                },\n                select: {\n                    id: true,\n                    email: true\n                }\n            });\n\n            const existingEmails = new Set(existingSubscribers.map(s => s.email));\n            const newEmails = recipients.filter(email => !existingEmails.has(email));\n\n            // Create subscribers for new emails\n            if (newEmails.length > 0) {\n                const newSubscribers = await Promise.all(newEmails.map(async (email) => {\n                    const subscriber = await db.emailSubscriber.create({\n                        data: {\n                            email,\n                            unsubscribeToken: nanoid(32),\n                            subscriptions: {\n                                create: {\n                                    projectId,\n                                    subscriptionType: 'ALL_UPDATES'\n                                }\n                            }\n                        },\n                        select: {\n                            id: true\n                        }\n                    });\n                    return subscriber;\n                }));\n\n                // Track new subscriber IDs\n                const newSubscriberIds = newSubscribers.map(s => s.id);\n                finalSubscriberIds.push(...newSubscriberIds);\n            }\n\n            // Add existing subscriber IDs\n            finalSubscriberIds.push(...existingSubscribers.map(s => s.id));\n        }\n\n        // Add explicitly specified subscribers\n        if (subscriberIds && subscriberIds.length > 0) {\n            const existingSubscriberIds = new Set(finalSubscriberIds);\n            // Add only new subscriber IDs (avoid duplicates)\n            subscriberIds.forEach(id => {\n                if (!existingSubscriberIds.has(id)) {\n                    finalSubscriberIds.push(id);\n                }\n            });\n        }\n\n        // If sending to subscribers, get their emails\n        if (finalSubscriberIds.length > 0) {\n            const subscribers = await db.emailSubscriber.findMany({\n                where: {\n                    id: { in: finalSubscriberIds },\n                    isActive: true\n                },\n                select: {\n                    id: true,\n                    email: true,\n                    unsubscribeToken: true\n                }\n            });\n\n            // Add subscriber emails to recipients\n            subscribers.forEach(subscriber => {\n                if (!emailRecipients.includes(subscriber.email)) {\n                    emailRecipients.push(subscriber.email);\n                }\n            });\n        }\n\n        // Ensure we have recipients to send to\n        if (emailRecipients.length === 0) {\n            throw new Error('No recipients to send to');\n        }\n\n        // Use the existing entries from the query\n        const entries = project.changelog ? project.changelog.entries : [];\n\n        // Get subscriber details with subscriptions - FIXED: using include only\n        const subscriberMap: SubscriberWithSubscriptions[] = await db.emailSubscriber.findMany({\n            where: {\n                id: { in: finalSubscriberIds }\n            },\n            include: {\n                subscriptions: {\n                    where: { projectId },\n                    select: { customDomain: true }\n                }\n            }\n        });\n\n        // Get the app URL from environment\n        const baseUrl = customDomain\n            ? `https://${customDomain}`\n            : process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';\n\n        // Process each email individually with custom unsubscribe link\n        const emailPromises = emailRecipients.map(async (email) => {\n            const subscriber = subscriberMap.find(s => s.email === email);\n\n            // Determine which domain to use for unsubscribe\n            let unsubscribeBaseUrl = baseUrl;\n            if (!customDomain && subscriber?.subscriptions?.[0]?.customDomain) {\n                unsubscribeBaseUrl = `https://${subscriber.subscriptions[0].customDomain}`;\n            }\n\n            const unsubscribeUrl = subscriber\n                ? `${unsubscribeBaseUrl}/api/changelog/unsubscribe/${subscriber.unsubscribeToken}?projectId=${projectId}`\n                : `${unsubscribeBaseUrl}/unsubscribe?email=${encodeURIComponent(email)}&projectId=${projectId}`;\n\n            // Generate the changelog URL based on domain\n            const changelogUrl = customDomain\n                ? `https://${customDomain}`\n                : subscriber?.subscriptions?.[0]?.customDomain\n                    ? `https://${subscriber.subscriptions[0].customDomain}`\n                    : `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/projects/${projectId}`;\n\n            // Pass custom domain and changelog URL to email template\n            const emailComponent = ChangelogEmail({\n                projectName: project.name,\n                entries: entries,\n                isDigest: isDigest || false,\n                unsubscribeUrl,\n                recipientName: subscriber?.name || undefined,\n                recipientEmail: email,\n                customDomain: customDomain || subscriber?.subscriptions?.[0]?.customDomain || undefined,\n                changelogUrl\n            });\n\n            const html = emailComponent && isValidElement(emailComponent) ? await render(emailComponent, {\n                pretty: true\n            }) : '';\n\n            const text = emailComponent && isValidElement(emailComponent) ? await render(emailComponent, {\n                plainText: true\n            }) : '';\n\n            const mailOptions: SendMailOptions = {\n                from: `\"${emailConfig.fromName || project.name}\" <${emailConfig.fromEmail}>`,\n                to: email,\n                subject: emailSubject,\n                text,\n                html,\n                replyTo: emailConfig.replyToEmail || undefined,\n                headers: {\n                    'List-Unsubscribe': `<${unsubscribeUrl}>`,\n                    'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click'\n                }\n            };\n\n            return transporter.sendMail(mailOptions);\n        });\n\n        // Send all emails\n        const results = await Promise.all(emailPromises);\n\n        // Update last sent time for subscribers\n        if (finalSubscriberIds.length > 0) {\n            await db.emailSubscriber.updateMany({\n                where: { id: { in: finalSubscriberIds } },\n                data: { lastEmailSentAt: new Date() }\n            });\n        }\n\n        // Create email log\n        await db.emailLog.create({\n            data: {\n                projectId,\n                recipients: emailRecipients,\n                subject: emailSubject,\n                messageId: results[0]?.messageId || '',\n                type: isDigest ? 'DIGEST' : 'SINGLE_UPDATE',\n                entryIds: entries.map(e => e.id),\n            }\n        });\n\n        return {\n            success: true,\n            messageId: results[0]?.messageId || '',\n            recipientCount: emailRecipients.length\n        };\n    } catch (error) {\n        console.error('Failed to send changelog email:', error);\n        throw error;\n    }\n}\n\n/**\n * Sends a notification email to a user about request status\n */\nexport async function sendNotificationEmail({\n                                                userId,\n                                                status,\n                                                request,\n                                                dashboardUrl\n                                            }: SendNotificationParams): Promise<boolean> {\n    try {\n        // Get user details\n        const user = await db.user.findUnique({\n            where: { id: userId },\n            include: { settings: true }\n        });\n\n        if (!user || (user.settings?.enableNotifications === false)) {\n            return false; // User doesn't want notifications\n        }\n\n        // Get system email settings\n        const systemConfig = await db.systemConfig.findFirst({\n            where: { id: 1 }\n        });\n\n        if (!systemConfig || !systemConfig.systemEmail || !systemConfig.smtpHost) {\n            console.error('System email not configured for notifications');\n            return false;\n        }\n\n        // Set up transport\n        const transporterOptions: SMTPTransport.Options = {\n            host: systemConfig.smtpHost,\n            port: systemConfig.smtpPort || 587,\n            secure: !!systemConfig.smtpSecure,\n            auth: systemConfig.smtpUser && systemConfig.smtpPassword\n                ? {\n                    user: systemConfig.smtpUser,\n                    pass: systemConfig.smtpPassword,\n                }\n                : undefined,\n            tls: {\n                rejectUnauthorized: !!systemConfig.smtpSecure,\n            },\n        };\n\n        const transporter = createTransport(transporterOptions);\n\n        // Choose the right email template\n        const emailProps = {\n            recipientName: user.name || undefined,\n            projectName: request.projectName,\n            requestType: request.type,\n            entryTitle: request.entryTitle,\n            adminName: request.adminName || 'an administrator',\n            dashboardUrl\n        };\n\n        // Use the appropriate email template based on status\n        const emailComponent = status === 'APPROVED'\n            ? ApprovalNotificationEmail(emailProps)\n            : RejectionNotificationEmail(emailProps);\n\n        // Render the email\n        const html = isValidElement(emailComponent)\n            ? await render(emailComponent, { pretty: true })\n            : '';\n\n        const text = isValidElement(emailComponent)\n            ? await render(emailComponent, { plainText: true })\n            : '';\n\n        // Set the subject based on status\n        const subject = status === 'APPROVED'\n            ? `Request Approved for ${request.projectName}`\n            : `Request Not Approved for ${request.projectName}`;\n\n        // Send the email\n        const mailOptions: SendMailOptions = {\n            from: `\"Changerawr Notifications\" <${systemConfig.systemEmail}>`,\n            to: user.email,\n            subject,\n            html,\n            text,\n        };\n\n        const result = await transporter.sendMail(mailOptions);\n\n        // Log the notification\n        await db.auditLog.create({\n            data: {\n                action: 'NOTIFICATION_SENT',\n                userId,\n                details: {\n                    notificationType: 'email',\n                    subject,\n                    status,\n                    requestType: request.type,\n                    projectName: request.projectName,\n                    messageId: result.messageId,\n                    timestamp: new Date().toISOString()\n                }\n            }\n        });\n\n        return true;\n    } catch (error) {\n        console.error('Failed to send notification email:', error);\n        return false;\n    }\n}\n\n/**\n * Manages subscriptions for users\n */\nexport async function manageSubscription({\n                                             email,\n                                             projectId,\n                                             subscriptionType,\n                                             unsubscribe = false\n                                         }: ManageSubscriptionParams): Promise<SubscriptionResult> {\n    // Check if project exists\n    const project = await db.project.findUnique({\n        where: { id: projectId }\n    });\n\n    if (!project) {\n        throw new Error('Project not found');\n    }\n\n    // Find or create subscriber\n    let subscriber = await db.emailSubscriber.findUnique({\n        where: { email },\n        include: {\n            subscriptions: {\n                where: { projectId }\n            }\n        }\n    });\n\n    if (!subscriber) {\n        subscriber = await db.emailSubscriber.create({\n            data: {\n                email,\n                unsubscribeToken: nanoid(32),\n            },\n            include: {\n                subscriptions: {\n                    where: { projectId }\n                }\n            }\n        });\n    }\n\n    // If unsubscribing, remove the subscription\n    if (unsubscribe) {\n        if (subscriber.subscriptions.length > 0) {\n            await db.projectSubscription.delete({\n                where: {\n                    id: subscriber.subscriptions[0].id\n                }\n            });\n        }\n        return { success: true, message: 'Unsubscribed successfully' };\n    }\n\n    // If subscribing or updating subscription\n    if (subscriptionType) {\n        if (subscriber.subscriptions.length > 0) {\n            // Update existing subscription\n            await db.projectSubscription.update({\n                where: {\n                    id: subscriber.subscriptions[0].id\n                },\n                data: {\n                    subscriptionType\n                }\n            });\n        } else {\n            // Create new subscription\n            await db.projectSubscription.create({\n                data: {\n                    projectId,\n                    subscriberId: subscriber.id,\n                    subscriptionType\n                }\n            });\n        }\n    }\n\n    return { success: true, message: 'Subscription updated successfully' };\n}"
  },
  {
    "path": "lib/services/email/schedule-notification.ts",
    "content": "import {isValidElement} from 'react';\nimport {render} from '@react-email/render';\nimport {createTransport, SendMailOptions} from 'nodemailer';\nimport {db} from '@/lib/db';\nimport SchedulePublishedEmail from '@/emails/schedule-published';\nimport SMTPTransport from \"nodemailer/lib/smtp-transport\";\n\ninterface SendScheduleNotificationParams {\n    userId: string;\n    entryId: string;\n    projectId: string;\n}\n\n// Type for audit log details that contain staffUserId\ninterface AuditLogDetailsWithStaffUser {\n    staffUserId?: string;\n    entryId?: string;\n    [key: string]: unknown;\n}\n\n/**\n * Sends a notification email to the user who scheduled an entry when it gets automatically published\n */\nexport async function sendSchedulePublishedNotification({\n                                                            userId,\n                                                            entryId,\n                                                            projectId\n                                                        }: SendScheduleNotificationParams): Promise<boolean> {\n    try {\n        // Get user details with settings\n        const user = await db.user.findUnique({\n            where: {id: userId},\n            include: {settings: true}\n        });\n\n        if (!user) {\n            console.log(`User ${userId} not found for schedule notification`);\n            return false;\n        }\n\n        // Check if user wants notifications (default to true if no preference set)\n        if (user.settings?.enableNotifications === false) {\n            console.log(`User ${userId} has notifications disabled`);\n            return false;\n        }\n\n        // Get the published entry with project details\n        const entry = await db.changelogEntry.findUnique({\n            where: {id: entryId},\n            include: {\n                changelog: {\n                    include: {\n                        project: true\n                    }\n                }\n            }\n        });\n\n        if (!entry) {\n            console.error(`Entry ${entryId} not found for schedule notification`);\n            return false;\n        }\n\n        // Get system email configuration\n        const systemConfig = await db.systemConfig.findFirst({\n            where: {id: 1}\n        });\n\n        if (!systemConfig || !systemConfig.systemEmail || !systemConfig.smtpHost) {\n            console.error('System email not configured for schedule notifications');\n            return false;\n        }\n\n        // Set up email transporter\n        const transporterOptions: SMTPTransport.Options = {\n            host: systemConfig.smtpHost,\n            port: systemConfig.smtpPort || 587,\n            secure: !!systemConfig.smtpSecure,\n            auth: systemConfig.smtpUser && systemConfig.smtpPassword\n                ? {\n                    user: systemConfig.smtpUser,\n                    pass: systemConfig.smtpPassword,\n                }\n                : undefined,\n            tls: {\n                rejectUnauthorized: !!systemConfig.smtpSecure,\n            },\n        };\n\n        const transporter = createTransport(transporterOptions);\n\n        // Prepare email content\n        const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';\n        const publicChangelogUrl = entry.changelog.project.isPublic\n            ? `${appUrl}/changelog/${projectId}`\n            : undefined;\n\n        // Resolve timezone: user preference > system default > UTC\n        const effectiveTimezone = (user.settings?.timezone) || systemConfig.timezone || 'UTC';\n\n        const emailProps = {\n            recipientName: user.name || undefined,\n            projectName: entry.changelog.project.name,\n            entryTitle: entry.title,\n            entryVersion: entry.version || undefined,\n            publishedAt: entry.publishedAt || new Date(),\n            dashboardUrl: `${appUrl}/dashboard`, // Added missing dashboardUrl\n            viewEntryUrl: publicChangelogUrl,\n            timezone: effectiveTimezone,\n        };\n\n        // Render the email template\n        const emailComponent = SchedulePublishedEmail(emailProps);\n\n        const html = isValidElement(emailComponent)\n            ? await render(emailComponent, {pretty: true})\n            : '';\n\n        const text = isValidElement(emailComponent)\n            ? await render(emailComponent, {plainText: true})\n            : '';\n\n        if (!html) {\n            console.error('Failed to render schedule notification email template');\n            return false;\n        }\n\n        // Send the email\n        const subject = `🎉 Your scheduled entry \"${entry.title}\" is now live!`;\n\n        const mailOptions: SendMailOptions = {\n            from: `\"Changerawr\" <${systemConfig.systemEmail}>`,\n            to: user.email,\n            subject,\n            html,\n            text,\n        };\n\n        const result = await transporter.sendMail(mailOptions);\n\n        // Log the notification in audit log\n        await db.auditLog.create({\n            data: {\n                action: 'SCHEDULE_NOTIFICATION_SENT',\n                userId: userId,\n                targetUserId: userId,\n                details: {\n                    notificationType: 'schedule_published',\n                    entryId: entry.id,\n                    entryTitle: entry.title,\n                    projectId: projectId,\n                    projectName: entry.changelog.project.name,\n                    messageId: result.messageId,\n                    recipientEmail: user.email,\n                    publishedAt: entry.publishedAt?.toISOString(),\n                    timestamp: new Date().toISOString()\n                }\n            }\n        });\n\n        console.log(`Schedule notification sent to ${user.email} for entry ${entry.title}`);\n        return true;\n\n    } catch (error) {\n        console.error('Failed to send schedule published notification:', error);\n\n        // Log the error but don't throw - we don't want to fail the job execution\n        try {\n            await db.auditLog.create({\n                data: {\n                    action: 'SCHEDULE_NOTIFICATION_ERROR',\n                    userId: userId,\n                    targetUserId: userId,\n                    details: {\n                        error: error instanceof Error ? error.message : 'Unknown error',\n                        entryId,\n                        projectId,\n                        timestamp: new Date().toISOString()\n                    }\n                }\n            });\n        } catch (auditError) {\n            console.error('Failed to log schedule notification error:', auditError);\n        }\n\n        return false;\n    }\n}\n\n/**\n * Helper function to get the user ID who scheduled an entry\n * This looks at audit logs to find who created the schedule\n */\nexport async function getScheduleCreatorUserId(entryId: string): Promise<string | null> {\n    try {\n        // Look for the most recent schedule creation audit log for this entry\n        const auditLog = await db.auditLog.findFirst({\n            where: {\n                action: {\n                    in: ['CHANGELOG_ENTRY_SCHEDULED', 'CHANGELOG_SCHEDULE_APPROVED']\n                },\n                details: {\n                    path: ['entryId'],\n                    equals: entryId\n                }\n            },\n            orderBy: {\n                createdAt: 'desc'\n            }\n        });\n\n        if (auditLog) {\n            // For approved schedules, get the original requester (staffUserId)\n            if (auditLog.action === 'CHANGELOG_SCHEDULE_APPROVED') {\n                const details = auditLog.details as AuditLogDetailsWithStaffUser;\n                const staffUserId = details?.staffUserId;\n                if (staffUserId && typeof staffUserId === 'string') {\n                    return staffUserId;\n                }\n            }\n\n            // For direct schedules, use the userId who performed the action\n            return auditLog.userId;\n        }\n\n        // Fallback: look for schedule requests\n        const scheduleRequest = await db.changelogRequest.findFirst({\n            where: {\n                type: 'ALLOW_SCHEDULE',\n                changelogEntryId: entryId,\n                status: 'APPROVED'\n            },\n            orderBy: {\n                createdAt: 'desc'\n            }\n        });\n\n        if (scheduleRequest?.staffId) {\n            return scheduleRequest.staffId;\n        }\n\n        return null;\n    } catch (error) {\n        console.error('Failed to get schedule creator user ID:', error);\n        return null;\n    }\n}"
  },
  {
    "path": "lib/services/github/changelog-generator.ts",
    "content": "import {\n    GitHubClient,\n    GitHubCommit,\n    parseConventionalCommit,\n    shouldIncludeCommit\n} from './client';\nimport { createSectonClient } from '@/lib/utils/ai/secton';\n\nexport interface FileChange {\n    filename: string;\n    status: 'added' | 'modified' | 'removed' | 'renamed';\n    additions: number;\n    deletions: number;\n    patch?: string;\n}\n\nexport interface CommitData {\n    sha: string;\n    message: string;\n    author: string;\n    date: string;\n    files: FileChange[];\n    url: string;\n}\n\nexport interface ChangelogEntry {\n    category: 'Features' | 'Bug Fixes' | 'Breaking Changes' | 'Documentation' | 'Refactoring' | 'Performance' | 'Other';\n    description: string;\n    files: string[];\n    commit: string;\n    impact?: string;\n    technicalDetails?: string;\n}\n\nexport interface ChangelogGenerationOptions {\n    includeBreakingChanges: boolean;\n    includeFixes: boolean;\n    includeFeatures: boolean;\n    includeChores: boolean;\n    customCommitTypes: string[];\n    useAI: boolean;\n    aiApiKey?: string;\n    aiModel?: string;\n    groupByType: boolean;\n    includeCommitLinks: boolean;\n    repositoryUrl: string;\n    includeCodeAnalysis: boolean;\n    maxCommitsToAnalyze?: number;\n}\n\nexport interface GeneratedChangelog {\n    content: string;\n    version?: string;\n    commits: CommitData[];\n    entries: ChangelogEntry[];\n    metadata: {\n        totalCommits: number;\n        analyzedCommits: number;\n        aiGenerated: boolean;\n        hasCodeAnalysis: boolean;\n        model?: string;\n        generatedAt: string;\n    };\n}\n\n/**\n * Safe text cleaning that avoids JSON parsing issues\n */\nfunction cleanText(text: string | undefined | null): string {\n    if (!text) return '';\n\n    return text\n        // Remove control characters except newlines and tabs\n        .replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]/g, '')\n        // Replace problematic quotes with safe alternatives\n        .replace(/\"/g, \"'\")\n        .replace(/\"/g, \"'\")\n        .replace(/\"/g, \"'\")\n        // Remove other problematic Unicode\n        .replace(/[\\u2028\\u2029]/g, ' ')\n        // Normalize whitespace\n        .replace(/\\s+/g, ' ')\n        // Trim\n        .trim();\n}\n\n/**\n * Safe truncation with word boundaries\n */\nfunction safeTruncate(text: string, maxLength: number): string {\n    if (text.length <= maxLength) return text;\n\n    const truncated = text.substring(0, maxLength - 3);\n    const lastSpace = truncated.lastIndexOf(' ');\n\n    if (lastSpace > maxLength * 0.8) {\n        return truncated.substring(0, lastSpace) + '...';\n    }\n\n    return truncated + '...';\n}\n\n/**\n * Enhanced service for generating changelog content from GitHub commits with AI code analysis\n */\nexport class GitHubChangelogGenerator {\n    private client: GitHubClient;\n\n    constructor(client: GitHubClient) {\n        this.client = client;\n    }\n\n    /**\n     * Generate changelog from commits between two references with code analysis\n     */\n    async generateChangelogBetweenRefs(\n        repositoryUrl: string,\n        fromRef: string,\n        toRef: string,\n        options: ChangelogGenerationOptions\n    ): Promise<GeneratedChangelog> {\n        console.log(`Fetching commits between ${fromRef} and ${toRef}`);\n\n        const commits = await this.client.getCommitsBetween(repositoryUrl, fromRef, toRef);\n        console.log(`Found ${commits.length} commits between refs`);\n\n        return this.generateChangelogFromCommits(commits, repositoryUrl, options);\n    }\n\n    /**\n     * Generate changelog from recent commits with code analysis\n     */\n    async generateChangelogFromRecent(\n        repositoryUrl: string,\n        daysBack: number,\n        options: ChangelogGenerationOptions\n    ): Promise<GeneratedChangelog> {\n        const since = new Date();\n        since.setDate(since.getDate() - daysBack);\n\n        console.log(`Fetching commits since ${since.toISOString()}`);\n\n        const commits = await this.client.getCommits(repositoryUrl, {\n            since: since.toISOString(),\n            per_page: Math.min(options.maxCommitsToAnalyze ?? 50, 100)\n        });\n\n        console.log(`Found ${commits.length} commits in the last ${daysBack} days`);\n\n        return this.generateChangelogFromCommits(commits, repositoryUrl, options);\n    }\n\n    /**\n     * Generate changelog from a list of commits with optional AI code analysis\n     */\n    async generateChangelogFromCommits(\n        commits: GitHubCommit[],\n        repositoryUrl: string,\n        options: ChangelogGenerationOptions\n    ): Promise<GeneratedChangelog> {\n        console.log(`Processing ${commits.length} commits`);\n\n        // Filter commits based on settings\n        const filteredCommits = commits.filter(commit =>\n            shouldIncludeCommit(commit, options)\n        );\n\n        console.log(`${filteredCommits.length} commits passed filtering`);\n\n        if (filteredCommits.length === 0) {\n            return {\n                content: 'No significant changes found in the selected commits.',\n                commits: [],\n                entries: [],\n                metadata: {\n                    totalCommits: commits.length,\n                    analyzedCommits: 0,\n                    aiGenerated: false,\n                    hasCodeAnalysis: false,\n                    generatedAt: new Date().toISOString()\n                }\n            };\n        }\n\n        // Convert GitHub commits to our format with safe text cleaning\n        const commitData: CommitData[] = filteredCommits.map(commit => ({\n            sha: commit.sha,\n            message: cleanText(commit.message),\n            author: cleanText(commit.author?.name) || 'Unknown',\n            date: commit.author?.date ?? new Date().toISOString(),\n            files: [], // Will be populated later if code analysis is enabled\n            url: commit.url ?? `${repositoryUrl}/commit/${commit.sha}`\n        }));\n\n        // Get file changes if code analysis is enabled\n        if (options.includeCodeAnalysis && options.useAI && options.aiApiKey) {\n            console.log('Fetching file changes for code analysis...');\n            await this.enrichCommitsWithFileData(commitData, repositoryUrl);\n        }\n\n        // Generate changelog entries\n        let entries: ChangelogEntry[];\n        if (options.useAI && options.aiApiKey) {\n            console.log('Generating changelog entries with AI analysis...');\n            entries = await this.generateEntriesWithAI(commitData, options);\n        } else {\n            console.log('Generating basic changelog entries...');\n            entries = this.generateBasicEntries(commitData);\n        }\n\n        // Generate markdown content\n        const content = this.generateMarkdownContent(entries, options);\n\n        // Try to infer version\n        const version = await this.inferVersion(commitData, repositoryUrl);\n\n        return {\n            content,\n            version,\n            commits: commitData,\n            entries,\n            metadata: {\n                totalCommits: commits.length,\n                analyzedCommits: commitData.length,\n                aiGenerated: options.useAI && !!options.aiApiKey,\n                hasCodeAnalysis: options.includeCodeAnalysis && !!options.aiApiKey,\n                model: options.aiModel,\n                generatedAt: new Date().toISOString()\n            }\n        };\n    }\n\n    /**\n     * Enrich commit data with file changes\n     */\n    private async enrichCommitsWithFileData(\n        commitData: CommitData[],\n        repositoryUrl: string\n    ): Promise<void> {\n        const maxCommitsToAnalyze = Math.min(commitData.length, 10); // Limit to prevent API abuse\n\n        for (let i = 0; i < maxCommitsToAnalyze; i++) {\n            const commit = commitData[i];\n\n            try {\n                console.log(`Fetching file data for commit ${commit.sha.substring(0, 7)}...`);\n\n                const detailedCommit = await this.client.getCommit(repositoryUrl, commit.sha);\n\n                if (detailedCommit.files) {\n                    commit.files = detailedCommit.files.map(file => ({\n                        filename: cleanText(file.filename),\n                        status: file.status,\n                        additions: file.additions || 0,\n                        deletions: file.deletions || 0,\n                        patch: file.patch ? safeTruncate(cleanText(file.patch), 500) : undefined\n                    }));\n                }\n\n                // Rate limiting\n                await this.delay(300);\n\n            } catch (error) {\n                console.warn(`Failed to fetch file data for commit ${commit.sha}:`, error);\n                // Continue with empty files array\n            }\n        }\n    }\n\n    /**\n     * Generate changelog entries using AI analysis with safer approach\n     */\n    private async generateEntriesWithAI(\n        commits: CommitData[],\n        options: ChangelogGenerationOptions\n    ): Promise<ChangelogEntry[]> {\n        if (!options.aiApiKey) {\n            throw new Error('AI API key is required for AI analysis');\n        }\n\n        const aiClient = createSectonClient({\n            apiKey: options.aiApiKey,\n            defaultModel: options.aiModel ?? 'copilot-zero'\n        });\n\n        const entries: ChangelogEntry[] = [];\n\n        // Process commits one by one to avoid large payloads and parsing issues\n        for (const commit of commits) {\n            try {\n                console.log(`Analyzing commit ${commit.sha.substring(0, 7)} with AI...`);\n\n                const entry = await this.analyzeCommitWithAI(commit, aiClient);\n                if (entry) {\n                    entries.push(entry);\n                }\n\n                // Rate limiting\n                await this.delay(1000);\n\n            } catch (error) {\n                console.error(`Error analyzing commit ${commit.sha} with AI:`, error);\n\n                // Fallback to basic analysis for this commit\n                const fallbackEntry = this.generateBasicEntry(commit);\n                entries.push(fallbackEntry);\n            }\n        }\n\n        return entries;\n    }\n\n    /**\n     * Analyze a single commit with AI using a safer structured approach\n     */\n    private async analyzeCommitWithAI(\n        commit: CommitData,\n        aiClient: ReturnType<typeof createSectonClient>\n    ): Promise<ChangelogEntry | null> {\n        // Build a simple, structured prompt without JSON\n        const prompt = this.buildStructuredPrompt(commit);\n\n        try {\n            const response = await aiClient.generateText(prompt, {\n                temperature: 0.3,\n                max_tokens: 500\n            });\n\n            // Parse the structured response instead of JSON\n            return this.parseStructuredResponse(response, commit);\n\n        } catch (error) {\n            console.error('AI analysis failed for commit:', error);\n            return this.generateBasicEntry(commit);\n        }\n    }\n\n    /**\n     * Build a structured prompt that avoids JSON parsing issues\n     */\n    private buildStructuredPrompt(commit: CommitData): string {\n        let prompt = `Analyze this git commit and provide changelog information:\n\nCOMMIT: ${commit.sha}\nMESSAGE: ${safeTruncate(commit.message, 200)}\nAUTHOR: ${commit.author}\nFILES: ${commit.files.length} files changed`;\n\n        if (commit.files.length > 0) {\n            prompt += '\\n\\nFILE CHANGES:';\n            commit.files.slice(0, 3).forEach(file => {\n                prompt += `\\n- ${file.filename} (${file.status}): +${file.additions} -${file.deletions}`;\n            });\n        }\n\n        prompt += `\n\nPlease respond in this exact format:\nCATEGORY: [Features|Bug Fixes|Breaking Changes|Documentation|Refactoring|Performance|Other]\nDESCRIPTION: [One line description of the change]\nIMPACT: [Optional: Why this matters to users]\nTECHNICAL: [Optional: Technical implementation notes]\n\nFocus on user-facing impact and be concise.`;\n\n        return prompt;\n    }\n\n    /**\n     * Parse structured AI response instead of JSON\n     */\n    private parseStructuredResponse(response: string, commit: CommitData): ChangelogEntry {\n        const lines = response.split('\\n').map(line => line.trim()).filter(Boolean);\n\n        let category = 'Other';\n        let description = safeTruncate(commit.message.split('\\n')[0], 100) || 'Code changes';\n        let impact: string | undefined;\n        let technicalDetails: string | undefined;\n\n        for (const line of lines) {\n            if (line.startsWith('CATEGORY:')) {\n                const cat = line.replace('CATEGORY:', '').trim();\n                if (['Features', 'Bug Fixes', 'Breaking Changes', 'Documentation', 'Refactoring', 'Performance', 'Other'].includes(cat)) {\n                    category = cat;\n                }\n            } else if (line.startsWith('DESCRIPTION:')) {\n                const desc = line.replace('DESCRIPTION:', '').trim();\n                if (desc) {\n                    description = safeTruncate(desc, 150);\n                }\n            } else if (line.startsWith('IMPACT:')) {\n                const imp = line.replace('IMPACT:', '').trim();\n                if (imp) {\n                    impact = safeTruncate(imp, 200);\n                }\n            } else if (line.startsWith('TECHNICAL:')) {\n                const tech = line.replace('TECHNICAL:', '').trim();\n                if (tech) {\n                    technicalDetails = safeTruncate(tech, 200);\n                }\n            }\n        }\n\n        return {\n            category: category as ChangelogEntry['category'],\n            description,\n            files: commit.files.map(f => f.filename),\n            commit: commit.sha,\n            impact,\n            technicalDetails\n        };\n    }\n\n    /**\n     * Generate basic changelog entries without AI\n     */\n    private generateBasicEntries(commits: CommitData[]): ChangelogEntry[] {\n        return commits.map(commit => this.generateBasicEntry(commit));\n    }\n\n    /**\n     * Generate a basic entry for a single commit\n     */\n    private generateBasicEntry(commit: CommitData): ChangelogEntry {\n        return {\n            category: this.categorizeCommitMessage(commit.message) as ChangelogEntry['category'],\n            description: safeTruncate(commit.message.split('\\n')[0], 100) || 'Code changes',\n            files: commit.files.map(f => f.filename),\n            commit: commit.sha\n        };\n    }\n\n    /**\n     * Categorize commit message using basic rules\n     */\n    private categorizeCommitMessage(message: string): string {\n        const msg = message.toLowerCase();\n\n        if (msg.includes('feat') || msg.includes('add') || msg.includes('implement')) {\n            return 'Features';\n        }\n        if (msg.includes('fix') || msg.includes('bug') || msg.includes('resolve')) {\n            return 'Bug Fixes';\n        }\n        if (msg.includes('break') || msg.includes('!:')) {\n            return 'Breaking Changes';\n        }\n        if (msg.includes('doc') || msg.includes('readme')) {\n            return 'Documentation';\n        }\n        if (msg.includes('refactor') || msg.includes('clean') || msg.includes('reorganiz')) {\n            return 'Refactoring';\n        }\n        if (msg.includes('perf') || msg.includes('optimize') || msg.includes('speed')) {\n            return 'Performance';\n        }\n\n        return 'Other';\n    }\n\n    /**\n     * Generate markdown content from entries\n     */\n    private generateMarkdownContent(entries: ChangelogEntry[], options: ChangelogGenerationOptions): string {\n        const now = new Date().toISOString().split('T')[0];\n        let content = `# Changelog (${now})\\n\\n`;\n\n        if (entries.length === 0) {\n            return content + 'No significant changes found.\\n';\n        }\n\n        // Group entries by category\n        const groupedEntries = entries.reduce((acc, entry) => {\n            if (!acc[entry.category]) {\n                acc[entry.category] = [];\n            }\n            acc[entry.category].push(entry);\n            return acc;\n        }, {} as Record<string, ChangelogEntry[]>);\n\n        // Order categories by importance\n        const categoryOrder = [\n            'Breaking Changes',\n            'Features',\n            'Bug Fixes',\n            'Performance',\n            'Refactoring',\n            'Documentation',\n            'Other'\n        ];\n\n        categoryOrder.forEach(category => {\n            const categoryEntries = groupedEntries[category];\n            if (categoryEntries && categoryEntries.length > 0) {\n                const emoji = this.getCategoryEmoji(category);\n                content += `## ${emoji} ${category}\\n\\n`;\n\n                categoryEntries.forEach(entry => {\n                    content += `- ${entry.description}\\n`;\n\n                    if (entry.impact) {\n                        content += `  - **Impact**: ${entry.impact}\\n`;\n                    }\n\n                    if (entry.technicalDetails) {\n                        content += `  - **Technical**: ${entry.technicalDetails}\\n`;\n                    }\n\n                    if (options.includeCommitLinks) {\n                        const shortSha = entry.commit.substring(0, 7);\n                        const commitUrl = `${options.repositoryUrl}/commit/${entry.commit}`;\n                        content += `  - **Commit**: [${shortSha}](${commitUrl})\\n`;\n                    }\n\n                    content += '\\n';\n                });\n            }\n        });\n\n        return content;\n    }\n\n    /**\n     * Get emoji for category\n     */\n    private getCategoryEmoji(category: string): string {\n        const emojiMap: Record<string, string> = {\n            'Breaking Changes': '💥',\n            'Features': '🚀',\n            'Bug Fixes': '🐛',\n            'Performance': '⚡',\n            'Refactoring': '♻️',\n            'Documentation': '📚',\n            'Other': '📝'\n        };\n        return emojiMap[category] || '📝';\n    }\n\n    /**\n     * Try to infer version from commits or recent tags\n     */\n    private async inferVersion(commits: CommitData[], repositoryUrl: string): Promise<string | undefined> {\n        try {\n            // Check if any commit messages contain version info\n            for (const commit of commits) {\n                const versionMatch = commit.message.match(/v?(\\d+\\.\\d+\\.\\d+)/);\n                if (versionMatch) {\n                    return versionMatch[1];\n                }\n            }\n\n            // Try to get the latest tag and increment\n            const tags = await this.client.getTags(repositoryUrl, { per_page: 10 });\n\n            if (tags.length > 0) {\n                // Find the latest semantic version tag\n                const versionTags = tags\n                    .map(tag => ({\n                        name: tag.name,\n                        version: tag.name.replace(/^v/, ''),\n                        parts: tag.name.replace(/^v/, '').split('.').map(Number)\n                    }))\n                    .filter(tag =>\n                        tag.parts.length === 3 &&\n                        tag.parts.every(part => !isNaN(part))\n                    )\n                    .sort((a, b) => {\n                        for (let i = 0; i < 3; i++) {\n                            if (a.parts[i] !== b.parts[i]) {\n                                return b.parts[i] - a.parts[i];\n                            }\n                        }\n                        return 0;\n                    });\n\n                if (versionTags.length > 0) {\n                    const latestVersion = versionTags[0];\n                    const [major, minor, patch] = latestVersion.parts;\n\n                    // Analyze commits to determine version increment\n                    const hasBreaking = commits.some(c =>\n                        parseConventionalCommit(c.message)?.breaking ||\n                        c.message?.includes('BREAKING')\n                    );\n                    const hasFeatures = commits.some(c => {\n                        const parsed = parseConventionalCommit(c.message);\n                        return parsed?.type === 'feat' || parsed?.type === 'feature';\n                    });\n\n                    if (hasBreaking) {\n                        return `${major + 1}.0.0`;\n                    } else if (hasFeatures) {\n                        return `${major}.${minor + 1}.0`;\n                    } else {\n                        return `${major}.${minor}.${patch + 1}`;\n                    }\n                }\n            }\n\n            return undefined;\n        } catch (error) {\n            console.error('Version inference failed:', error);\n            return undefined;\n        }\n    }\n\n    /**\n     * Get available tags for version selection\n     */\n    async getAvailableTags(repositoryUrl: string): Promise<Array<{ name: string; sha: string }>> {\n        try {\n            console.log('Fetching tags from repository:', repositoryUrl);\n            const tags = await this.client.getTags(repositoryUrl, { per_page: 50 });\n            console.log(`Found ${tags.length} tags`);\n\n            return tags.map(tag => ({\n                name: tag.name,\n                sha: tag.commit.sha\n            }));\n        } catch (error) {\n            console.error('Failed to fetch tags:', error);\n            throw error;\n        }\n    }\n\n    /**\n     * Get available releases for version selection\n     */\n    async getAvailableReleases(repositoryUrl: string): Promise<Array<{ name: string; tagName: string }>> {\n        try {\n            console.log('Fetching releases from repository:', repositoryUrl);\n            const releases = await this.client.getReleases(repositoryUrl, { per_page: 50 });\n            console.log(`Found ${releases.length} releases`);\n\n            return releases.map(release => ({\n                name: release.name || release.tag_name,\n                tagName: release.tag_name\n            }));\n        } catch (error) {\n            console.error('Failed to fetch releases:', error);\n            throw error;\n        }\n    }\n\n    /**\n     * Utility delay function\n     */\n    private delay(ms: number): Promise<void> {\n        return new Promise(resolve => setTimeout(resolve, ms));\n    }\n}\n\n/**\n * Factory function to create changelog generator\n */\nexport function createGitHubChangelogGenerator(client: GitHubClient): GitHubChangelogGenerator {\n    return new GitHubChangelogGenerator(client);\n}"
  },
  {
    "path": "lib/services/github/client.ts",
    "content": "// lib/services/github/client.ts\n\nexport interface GitHubConfig {\n    accessToken: string;\n    repositoryUrl: string;\n    defaultBranch?: string;\n}\n\nexport interface GitHubCommitAuthor {\n    name: string;\n    email: string;\n    date: string;\n}\n\nexport interface GitHubCommitData {\n    message: string;\n    author: GitHubCommitAuthor;\n    committer: GitHubCommitAuthor;\n}\n\nexport interface GitHubCommitStats {\n    additions: number;\n    deletions: number;\n    total: number;\n}\n\nexport interface GitHubFile {\n    filename: string;\n    status: 'added' | 'modified' | 'removed' | 'renamed';\n    additions: number;\n    deletions: number;\n    changes: number;\n    patch?: string;\n    blob_url: string;\n    raw_url: string;\n    contents_url: string;\n    previous_filename?: string;\n}\n\nexport interface GitHubCommitResponse {\n    sha: string;\n    commit: GitHubCommitData;\n    html_url: string;\n    stats?: GitHubCommitStats;\n    files?: GitHubFile[];\n    repository?: {\n        html_url: string;\n    };\n}\n\nexport interface GitHubCommit {\n    sha: string;\n    commit: GitHubCommitData;\n    html_url: string;\n    stats?: GitHubCommitStats;\n    files?: GitHubFile[];\n    // Flattened properties for easier access\n    message: string;\n    author: GitHubCommitAuthor;\n    url: string;\n}\n\nexport interface GitHubTag {\n    name: string;\n    commit: {\n        sha: string;\n        url: string;\n    };\n    zipball_url: string;\n    tarball_url: string;\n}\n\nexport interface GitHubReleaseAuthor {\n    login: string;\n    avatar_url: string;\n}\n\nexport interface GitHubRelease {\n    id: number;\n    tag_name: string;\n    name: string;\n    body: string;\n    draft: boolean;\n    prerelease: boolean;\n    created_at: string;\n    published_at: string;\n    author: GitHubReleaseAuthor;\n}\n\nexport interface GitHubRepository {\n    id: number;\n    name: string;\n    full_name: string;\n    description: string;\n    private: boolean;\n    default_branch: string;\n    language: string;\n    stargazers_count: number;\n    forks_count: number;\n    open_issues_count: number;\n    created_at: string;\n    updated_at: string;\n    pushed_at: string;\n}\n\nexport interface GitHubComparisonResponse {\n    commits: GitHubCommitResponse[];\n    total_commits: number;\n}\n\nexport interface GitHubUser {\n    login: string;\n    id: number;\n    avatar_url: string;\n}\n\nexport interface GitHubErrorResponse {\n    message?: string;\n    documentation_url?: string;\n    errors?: Array<{\n        resource: string;\n        field: string;\n        code: string;\n    }>;\n}\n\nexport class GitHubError extends Error {\n    statusCode: number;\n    response?: GitHubErrorResponse;\n\n    constructor(message: string, statusCode: number, response?: GitHubErrorResponse) {\n        super(message);\n        this.name = 'GitHubError';\n        this.statusCode = statusCode;\n        this.response = response;\n    }\n}\n\nexport class GitHubClient {\n    private accessToken: string;\n    private baseUrl = 'https://api.github.com';\n\n    constructor(config: GitHubConfig) {\n        this.accessToken = config.accessToken;\n    }\n\n    private async makeRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n        const url = `${this.baseUrl}${endpoint}`;\n\n        // Use Bearer auth for all tokens since it works universally\n        const authHeader = `Bearer ${this.accessToken}`;\n\n        console.log('Making GitHub API request:', {\n            url,\n            authType: authHeader.split(' ')[0],\n            tokenPrefix: this.accessToken.substring(0, 8) + '...'\n        });\n\n        const response = await fetch(url, {\n            ...options,\n            headers: {\n                'Authorization': authHeader,\n                'Accept': 'application/vnd.github.v3+json',\n                'User-Agent': 'Changerawr/1.0',\n                'X-GitHub-Api-Version': '2022-11-28',\n                ...options.headers,\n            },\n        });\n\n        if (!response.ok) {\n            let errorMessage = `GitHub API error: ${response.status} ${response.statusText}`;\n            let errorBody: GitHubErrorResponse | undefined;\n\n            try {\n                const errorText = await response.text();\n                errorBody = JSON.parse(errorText) as GitHubErrorResponse;\n                errorMessage = errorBody.message ?? errorMessage;\n            } catch {\n                // If we can't parse JSON, use the default message\n                errorMessage = errorMessage;\n            }\n\n            throw new GitHubError(errorMessage, response.status, errorBody);\n        }\n\n        return response.json() as Promise<T>;\n    }\n\n    /**\n     * Extract owner and repo from repository URL\n     */\n    private parseRepositoryUrl(repositoryUrl: string): { owner: string; repo: string } {\n        // Handle various GitHub URL formats\n        const patterns = [\n            /^https:\\/\\/github\\.com\\/([^\\/]+)\\/([^\\/]+?)(?:\\.git)?(?:\\/)?$/,\n            /^git@github\\.com:([^\\/]+)\\/([^\\/]+?)(?:\\.git)?$/,\n            /^([^\\/]+)\\/([^\\/]+)$/, // Just owner/repo format\n        ];\n\n        for (const pattern of patterns) {\n            const match = repositoryUrl.match(pattern);\n            if (match) {\n                return { owner: match[1], repo: match[2].replace(/\\.git$/, '') };\n            }\n        }\n\n        throw new Error(`Invalid GitHub repository URL: ${repositoryUrl}`);\n    }\n\n    /**\n     * Normalize GitHub commit to our format\n     */\n    private normalizeCommit(commit: GitHubCommitResponse): GitHubCommit {\n        const { owner, repo } = this.parseRepositoryUrl(\n            commit.repository?.html_url ?? 'unknown/unknown'\n        );\n\n        return {\n            ...commit,\n            message: commit.commit?.message ?? '',\n            author: commit.commit?.author ?? {\n                name: 'Unknown',\n                email: '',\n                date: new Date().toISOString()\n            },\n            url: commit.html_url ?? `https://github.com/${owner}/${repo}/commit/${commit.sha}`\n        };\n    }\n\n    /**\n     * Test the connection and validate the repository access\n     */\n    async testConnection(repositoryUrl: string): Promise<GitHubRepository> {\n        const { owner, repo } = this.parseRepositoryUrl(repositoryUrl);\n\n        try {\n            const repository = await this.makeRequest<GitHubRepository>(`/repos/${owner}/${repo}`);\n            return repository;\n        } catch (error) {\n            if (error instanceof GitHubError) {\n                if (error.statusCode === 404) {\n                    throw new GitHubError('Repository not found or access denied', 404);\n                } else if (error.statusCode === 401) {\n                    throw new GitHubError('Invalid access token', 401);\n                } else if (error.statusCode === 403) {\n                    throw new GitHubError('Access forbidden - check token permissions', 403);\n                }\n            }\n            throw error;\n        }\n    }\n\n    /**\n     * Get commits from the repository\n     */\n    async getCommits(\n        repositoryUrl: string,\n        options: {\n            since?: string;\n            until?: string;\n            sha?: string;\n            path?: string;\n            per_page?: number;\n            page?: number;\n        } = {}\n    ): Promise<GitHubCommit[]> {\n        const { owner, repo } = this.parseRepositoryUrl(repositoryUrl);\n\n        const queryParams = new URLSearchParams();\n        if (options.since) queryParams.append('since', options.since);\n        if (options.until) queryParams.append('until', options.until);\n        if (options.sha) queryParams.append('sha', options.sha);\n        if (options.path) queryParams.append('path', options.path);\n        queryParams.append('per_page', (options.per_page ?? 30).toString());\n        if (options.page) queryParams.append('page', options.page.toString());\n\n        const endpoint = `/repos/${owner}/${repo}/commits${queryParams.toString() ? '?' + queryParams.toString() : ''}`;\n\n        const commits = await this.makeRequest<GitHubCommitResponse[]>(endpoint);\n\n        // Normalize the commits to ensure consistent structure\n        return commits.map(commit => this.normalizeCommit(commit));\n    }\n\n    /**\n     * Get commits with detailed file information\n     */\n    async getCommitsWithFiles(\n        repositoryUrl: string,\n        options: {\n            since?: string;\n            until?: string;\n            sha?: string;\n            path?: string;\n            per_page?: number;\n            page?: number;\n        } = {}\n    ): Promise<GitHubCommit[]> {\n        // First get the basic commits\n        const commits = await this.getCommits(repositoryUrl, options);\n\n        // Then fetch detailed info for each commit (with files)\n        const detailedCommits: GitHubCommit[] = [];\n\n        for (const commit of commits) {\n            try {\n                const detailedCommit = await this.getCommit(repositoryUrl, commit.sha);\n                detailedCommits.push(detailedCommit);\n\n                // Rate limiting - be nice to GitHub API\n                await this.delay(100);\n            } catch (error) {\n                console.warn(`Failed to get detailed info for commit ${commit.sha}:`, error);\n                // Use the basic commit info as fallback\n                detailedCommits.push(commit);\n            }\n        }\n\n        return detailedCommits;\n    }\n\n    /**\n     * Get commits between two references (tags, commits, branches)\n     */\n    async getCommitsBetween(\n        repositoryUrl: string,\n        base: string,\n        head: string\n    ): Promise<GitHubCommit[]> {\n        const { owner, repo } = this.parseRepositoryUrl(repositoryUrl);\n\n        const comparison = await this.makeRequest<GitHubComparisonResponse>(\n            `/repos/${owner}/${repo}/compare/${base}...${head}`\n        );\n\n        // Normalize the commits\n        return comparison.commits.map(commit => this.normalizeCommit(commit));\n    }\n\n    /**\n     * Get commits between two references with detailed file information\n     */\n    async getCommitsBetweenWithFiles(\n        repositoryUrl: string,\n        base: string,\n        head: string\n    ): Promise<GitHubCommit[]> {\n        // First get the basic commits\n        const commits = await this.getCommitsBetween(repositoryUrl, base, head);\n\n        // Then fetch detailed info for each commit (with files)\n        const detailedCommits: GitHubCommit[] = [];\n\n        for (const commit of commits) {\n            try {\n                const detailedCommit = await this.getCommit(repositoryUrl, commit.sha);\n                detailedCommits.push(detailedCommit);\n\n                // Rate limiting - be nice to GitHub API\n                await this.delay(100);\n            } catch (error) {\n                console.warn(`Failed to get detailed info for commit ${commit.sha}:`, error);\n                // Use the basic commit info as fallback\n                detailedCommits.push(commit);\n            }\n        }\n\n        return detailedCommits;\n    }\n\n    /**\n     * Get repository tags\n     */\n    async getTags(\n        repositoryUrl: string,\n        options: { per_page?: number; page?: number } = {}\n    ): Promise<GitHubTag[]> {\n        const { owner, repo } = this.parseRepositoryUrl(repositoryUrl);\n\n        const queryParams = new URLSearchParams();\n        if (options.per_page) queryParams.append('per_page', options.per_page.toString());\n        if (options.page) queryParams.append('page', options.page.toString());\n\n        const endpoint = `/repos/${owner}/${repo}/tags${queryParams.toString() ? '?' + queryParams.toString() : ''}`;\n\n        return this.makeRequest<GitHubTag[]>(endpoint);\n    }\n\n    /**\n     * Get repository releases\n     */\n    async getReleases(\n        repositoryUrl: string,\n        options: { per_page?: number; page?: number } = {}\n    ): Promise<GitHubRelease[]> {\n        const { owner, repo } = this.parseRepositoryUrl(repositoryUrl);\n\n        const queryParams = new URLSearchParams();\n        if (options.per_page) queryParams.append('per_page', options.per_page.toString());\n        if (options.page) queryParams.append('page', options.page.toString());\n\n        const endpoint = `/repos/${owner}/${repo}/releases${queryParams.toString() ? '?' + queryParams.toString() : ''}`;\n\n        return this.makeRequest<GitHubRelease[]>(endpoint);\n    }\n\n    /**\n     * Get the latest release\n     */\n    async getLatestRelease(repositoryUrl: string): Promise<GitHubRelease> {\n        const { owner, repo } = this.parseRepositoryUrl(repositoryUrl);\n        return this.makeRequest<GitHubRelease>(`/repos/${owner}/${repo}/releases/latest`);\n    }\n\n    /**\n     * Get detailed commit information including stats and file changes\n     */\n    async getCommit(repositoryUrl: string, sha: string): Promise<GitHubCommit> {\n        const { owner, repo } = this.parseRepositoryUrl(repositoryUrl);\n        const commit = await this.makeRequest<GitHubCommitResponse>(`/repos/${owner}/${repo}/commits/${sha}`);\n        return this.normalizeCommit(commit);\n    }\n\n    /**\n     * Get repository information\n     */\n    async getRepository(repositoryUrl: string): Promise<GitHubRepository> {\n        const { owner, repo } = this.parseRepositoryUrl(repositoryUrl);\n        return this.makeRequest<GitHubRepository>(`/repos/${owner}/${repo}`);\n    }\n\n    /**\n     * Get authenticated user information (for testing token validity)\n     */\n    async getUser(): Promise<GitHubUser> {\n        return this.makeRequest<GitHubUser>('/user');\n    }\n\n    /**\n     * Get file content from repository\n     */\n    async getFileContent(\n        repositoryUrl: string,\n        path: string,\n        ref?: string\n    ): Promise<string> {\n        const { owner, repo } = this.parseRepositoryUrl(repositoryUrl);\n\n        const queryParams = new URLSearchParams();\n        if (ref) queryParams.append('ref', ref);\n\n        const endpoint = `/repos/${owner}/${repo}/contents/${path}${queryParams.toString() ? '?' + queryParams.toString() : ''}`;\n\n        interface GitHubContent {\n            content: string;\n            encoding: string;\n            type: string;\n        }\n\n        const response = await this.makeRequest<GitHubContent>(endpoint);\n\n        if (response.type !== 'file') {\n            throw new Error(`Path ${path} is not a file`);\n        }\n\n        if (response.encoding === 'base64') {\n            return Buffer.from(response.content, 'base64').toString('utf-8');\n        }\n\n        return response.content;\n    }\n\n    /**\n     * Utility delay function\n     */\n    private delay(ms: number): Promise<void> {\n        return new Promise(resolve => setTimeout(resolve, ms));\n    }\n}\n\n/**\n * Factory function to create GitHub client from config\n */\nexport function createGitHubClient(config: GitHubConfig): GitHubClient {\n    return new GitHubClient(config);\n}\n\n/**\n * Analyze commit message using conventional commits\n */\nexport interface ConventionalCommit {\n    type: string;\n    scope?: string;\n    description: string;\n    body?: string;\n    footer?: string;\n    breaking: boolean;\n    raw: string;\n}\n\nexport function parseConventionalCommit(message: string | undefined): ConventionalCommit | undefined {\n    // Handle undefined/null messages\n    if (!message || typeof message !== 'string') {\n        return undefined;\n    }\n\n    // Try strict conventional commit format first\n    const conventionalCommitRegex = /^(\\w+)(\\(([^)]+)\\))?(!)?:\\s*(.+)$/;\n    const match = message.match(conventionalCommitRegex);\n\n    if (match) {\n        const [, type, , scope, breaking, description] = match;\n        const lines = message.split('\\n');\n        const body = lines.slice(1).join('\\n').trim();\n\n        return {\n            type: type.toLowerCase(),\n            scope,\n            description,\n            body: body || undefined,\n            breaking: !!breaking || message.includes('BREAKING CHANGE'),\n            raw: message,\n        };\n    }\n\n    // If no strict match, try to infer type from message content\n    const messageLower = message.toLowerCase();\n\n    // Look for common patterns\n    if (messageLower.startsWith('fix') || messageLower.includes('bug') || messageLower.includes('hotfix')) {\n        return {\n            type: 'fix',\n            description: message.split('\\n')[0],\n            breaking: message.includes('BREAKING CHANGE'),\n            raw: message,\n        };\n    }\n\n    if (messageLower.startsWith('feat') || messageLower.startsWith('add') || messageLower.startsWith('implement')) {\n        return {\n            type: 'feat',\n            description: message.split('\\n')[0],\n            breaking: message.includes('BREAKING CHANGE'),\n            raw: message,\n        };\n    }\n\n    if (messageLower.startsWith('chore') || messageLower.startsWith('update') || messageLower.startsWith('bump')) {\n        return {\n            type: 'chore',\n            description: message.split('\\n')[0],\n            breaking: message.includes('BREAKING CHANGE'),\n            raw: message,\n        };\n    }\n\n    if (messageLower.startsWith('docs') || messageLower.includes('documentation') || messageLower.includes('readme')) {\n        return {\n            type: 'docs',\n            description: message.split('\\n')[0],\n            breaking: false,\n            raw: message,\n        };\n    }\n\n    if (messageLower.startsWith('style') || messageLower.includes('formatting')) {\n        return {\n            type: 'style',\n            description: message.split('\\n')[0],\n            breaking: false,\n            raw: message,\n        };\n    }\n\n    if (messageLower.startsWith('refactor') || messageLower.includes('reorganize')) {\n        return {\n            type: 'refactor',\n            description: message.split('\\n')[0],\n            breaking: message.includes('BREAKING CHANGE'),\n            raw: message,\n        };\n    }\n\n    if (messageLower.startsWith('test') || messageLower.includes('testing')) {\n        return {\n            type: 'test',\n            description: message.split('\\n')[0],\n            breaking: false,\n            raw: message,\n        };\n    }\n\n    return undefined;\n}\n\n/**\n * Group commits by type for changelog generation\n */\nexport function groupCommitsByType(commits: GitHubCommit[]): Record<string, GitHubCommit[]> {\n    const groups: Record<string, GitHubCommit[]> = {};\n\n    commits.forEach(commit => {\n        // Handle missing commit message\n        if (!commit || !commit.message) {\n            return;\n        }\n\n        const parsed = parseConventionalCommit(commit.message);\n        const type = parsed?.type ?? 'other';\n\n        if (!groups[type]) {\n            groups[type] = [];\n        }\n        groups[type].push(commit);\n    });\n\n    return groups;\n}\n\nexport interface CommitFilterSettings {\n    includeBreakingChanges: boolean;\n    includeFixes: boolean;\n    includeFeatures: boolean;\n    includeChores: boolean;\n    customCommitTypes: string[];\n}\n\n/**\n * Check if commit should be included based on settings\n */\nexport function shouldIncludeCommit(\n    commit: GitHubCommit,\n    settings: CommitFilterSettings\n): boolean {\n    // Handle missing commit message\n    if (!commit || !commit.message) {\n        // console.log('Excluding commit: no message');\n        return false;\n    }\n\n    const parsed = parseConventionalCommit(commit.message);\n\n    // console.log('Checking commit:', {\n    //     message: commit.message.substring(0, 50) + '...',\n    //     parsed: parsed ? `${parsed.type}${parsed.scope ? `(${parsed.scope})` : ''}` : 'no-conventional-format',\n    //     breaking: parsed?.breaking ?? false\n    // });\n\n    // Always include breaking changes if enabled\n    if (parsed?.breaking && settings.includeBreakingChanges) {\n        // console.log('✓ Including: breaking change');\n        return true;\n    }\n\n    if (parsed) {\n        // Check specific conventional commit types\n        switch (parsed.type) {\n            case 'feat':\n            case 'feature':\n                if (settings.includeFeatures) {\n                    // console.log('✓ Including: feature');\n                    return true;\n                }\n                break;\n            case 'fix':\n                if (settings.includeFixes) {\n                    // console.log('✓ Including: fix');\n                    return true;\n                }\n                break;\n            case 'chore':\n                if (settings.includeChores) {\n                    // console.log('✓ Including: chore');\n                    return true;\n                }\n                break;\n            default:\n                // Check custom commit types\n                if (settings.customCommitTypes.includes(parsed.type)) {\n                    // console.log(`✓ Including: custom type (${parsed.type})`);\n                    return true;\n                }\n                break;\n        }\n\n        console.log(`✗ Excluding: ${parsed.type} not in enabled types`);\n        return false;\n    }\n\n    // No conventional commit format - be more lenient\n    // Include if ANY of the basic types are enabled OR if 'other' is in custom types\n    const hasBasicTypesEnabled = settings.includeFeatures || settings.includeFixes || settings.includeChores;\n    const includesOther = settings.customCommitTypes.includes('other');\n\n    if (hasBasicTypesEnabled || includesOther) {\n        // console.log('✓ Including: non-conventional commit (basic types enabled or \"other\" included)');\n        return true;\n    }\n\n    // console.log('✗ Excluding: non-conventional commit and no basic types enabled');\n    return false;\n}"
  },
  {
    "path": "lib/services/jobs/executors/changelog-publish.executor.ts",
    "content": "import {db} from '@/lib/db';\nimport {createAuditLog} from '@/lib/utils/auditLog';\nimport {sendSchedulePublishedNotification, getScheduleCreatorUserId} from '@/lib/services/email/schedule-notification';\n\nexport interface ScheduledJobExecutor {\n    execute(entityId: string): Promise<void>;\n}\n\nexport class ChangelogPublishExecutor implements ScheduledJobExecutor {\n    async execute(entityId: string): Promise<void> {\n        try {\n            // Get the entry with project information\n            const entry = await db.changelogEntry.findUnique({\n                where: {id: entityId},\n                include: {\n                    changelog: {\n                        include: {\n                            project: true,\n                        },\n                    },\n                },\n            });\n\n            if (!entry) {\n                throw new Error(`Changelog entry not found: ${entityId}`);\n            }\n\n            if (entry.publishedAt) {\n                console.log(`Changelog entry already published: ${entityId}`);\n                return; // Entry already published, nothing to do\n            }\n\n            // Get the user who scheduled this entry for notification\n            const scheduleCreatorId = await getScheduleCreatorUserId(entityId);\n\n            // Publish the entry\n            const updatedEntry = await db.changelogEntry.update({\n                where: {id: entityId},\n                data: {\n                    publishedAt: new Date(),\n                    scheduledAt: null, // Clear the schedule since it's now published\n                },\n            });\n\n            // Log the automatic publication\n            await createAuditLog(\n                'CHANGELOG_ENTRY_AUTO_PUBLISHED',\n                'system',\n                scheduleCreatorId || 'system', // Use schedule creator as target if available\n                {\n                    entryId: updatedEntry.id,\n                    entryTitle: updatedEntry.title,\n                    entryVersion: updatedEntry.version,\n                    projectId: entry.changelog.project.id,\n                    projectName: entry.changelog.project.name,\n                    scheduledBy: scheduleCreatorId || 'unknown',\n                    publishedAt: updatedEntry.publishedAt?.toISOString(),\n                    timestamp: new Date().toISOString(),\n                }\n            );\n\n            // Send notification email to the person who scheduled it\n            if (scheduleCreatorId) {\n                try {\n                    const notificationSent = await sendSchedulePublishedNotification({\n                        userId: scheduleCreatorId,\n                        entryId: entityId,\n                        projectId: entry.changelog.project.id\n                    });\n\n                    if (notificationSent) {\n                        console.log(`Schedule notification sent to user ${scheduleCreatorId} for entry ${updatedEntry.title}`);\n                    } else {\n                        console.log(`Schedule notification not sent (user preferences or configuration issue) for entry ${updatedEntry.title}`);\n                    }\n                } catch (emailError) {\n                    // Log email error but don't fail the job\n                    console.error('Failed to send schedule notification email:', emailError);\n\n                    await createAuditLog(\n                        'SCHEDULE_NOTIFICATION_FAILED',\n                        'system',\n                        scheduleCreatorId,\n                        {\n                            entryId: entityId,\n                            entryTitle: updatedEntry.title,\n                            projectId: entry.changelog.project.id,\n                            error: emailError instanceof Error ? emailError.message : 'Unknown error',\n                            timestamp: new Date().toISOString(),\n                        }\n                    );\n                }\n            } else {\n                console.log(`No schedule creator found for entry ${updatedEntry.title}, skipping notification`);\n            }\n\n            console.log(`Successfully published scheduled entry: ${updatedEntry.title} (${entityId})`);\n\n        } catch (error) {\n            console.error(`Failed to publish scheduled entry ${entityId}:`, error);\n\n            // Log the failure\n            try {\n                await createAuditLog(\n                    'CHANGELOG_ENTRY_AUTO_PUBLISH_FAILED',\n                    'system',\n                    'system',\n                    {\n                        entryId: entityId,\n                        error: error instanceof Error ? error.message : 'Unknown error',\n                        stack: error instanceof Error ? error.stack : undefined,\n                        timestamp: new Date().toISOString(),\n                    }\n                );\n            } catch (auditError) {\n                console.error('Failed to log auto-publish failure:', auditError);\n            }\n\n            throw error; // Re-throw to mark the job as failed\n        }\n    }\n}\n\n// Export a singleton instance\nexport const changelogPublishExecutor = new ChangelogPublishExecutor();"
  },
  {
    "path": "lib/services/jobs/executors/ssl-renewal.executor.ts",
    "content": "import { ScheduledJobExecutor } from '../scheduled-job.service'\nimport { runAutoRenewal } from '@/lib/custom-domains/ssl/auto-renewal'\nimport { scheduleNextSslRenewal } from '@/lib/custom-domains/ssl/setup-renewal-job'\n\n/**\n * SSL Certificate Auto-Renewal Executor\n *\n * This executor runs periodically to renew SSL certificates that are expiring soon.\n * It doesn't use entityId since it processes all expiring certificates in one run.\n * After successful execution, it automatically schedules the next renewal for tomorrow.\n */\nexport class SslRenewalExecutor implements ScheduledJobExecutor {\n    async execute(entityId: string): Promise<void> {\n        console.log('[ssl-renewal-executor] Starting SSL certificate renewal check')\n\n        try {\n            const result = await runAutoRenewal()\n\n            console.log('[ssl-renewal-executor] Renewal completed:', {\n                checked: result.checked,\n                renewed: result.renewed,\n                failed: result.failed,\n            })\n\n            if (result.errors.length > 0) {\n                console.error('[ssl-renewal-executor] Renewal errors:', result.errors)\n            }\n\n            // Schedule the next renewal for tomorrow at 3 AM\n            await scheduleNextSslRenewal()\n        } catch (error) {\n            console.error('[ssl-renewal-executor] Failed to run auto-renewal:', error)\n            throw error\n        }\n    }\n}\n"
  },
  {
    "path": "lib/services/jobs/executors/telemetry-send.executor.ts",
    "content": "import {ScheduledJobExecutor} from '@/lib/services/jobs/scheduled-job.service';\n\nexport class TelemetrySendExecutor implements ScheduledJobExecutor {\n    async execute(entityId: string): Promise<void> {\n        // entityId is the system config ID for telemetry jobs\n        try {\n            // Dynamically import to avoid circular dependencies\n            const { TelemetryService } = await import('@/lib/services/telemetry/service');\n            await TelemetryService.sendTelemetryNow();\n        } catch (error) {\n            // Let the job system handle retries\n            throw new Error(`Telemetry send failed: ${error instanceof Error ? error.message + entityId : 'Unknown error'}`);\n        }\n    }\n}"
  },
  {
    "path": "lib/services/jobs/job-runner.service.ts",
    "content": "import {ScheduledJobService} from \"@/lib/services/jobs/scheduled-job.service\";\nimport {createAuditLog} from \"@/lib/utils/auditLog\";\n\nexport class JobRunnerService {\n    private static isRunning = false;\n    private static intervalId: NodeJS.Timeout | null = null;\n\n    /**\n     * Start the job runner with specified interval\n     * @param intervalMs - How often to check for due jobs (default: 60 seconds)\n     */\n    static start(intervalMs: number = 60000): void {\n        if (this.isRunning) {\n            console.log('Job runner is already running');\n            return;\n        }\n\n        console.log(`Starting job runner with ${intervalMs}ms interval`);\n        this.isRunning = true;\n\n        // Run immediately on start\n        this.runJobs();\n\n        // Set up interval\n        this.intervalId = setInterval(() => {\n            this.runJobs();\n        }, intervalMs);\n    }\n\n    /**\n     * Stop the job runner\n     */\n    static stop(): void {\n        if (!this.isRunning) {\n            console.log('Job runner is not running');\n            return;\n        }\n\n        console.log('Stopping job runner');\n        this.isRunning = false;\n\n        if (this.intervalId) {\n            clearInterval(this.intervalId);\n            this.intervalId = null;\n        }\n    }\n\n    /**\n     * Run all due jobs\n     */\n    private static async runJobs(): Promise<void> {\n        try {\n            const dueJobs = await ScheduledJobService.getDueJobs();\n\n            if (dueJobs.length === 0) {\n                return;\n            }\n\n            console.log(`Found ${dueJobs.length} due jobs to execute`);\n\n            // Execute jobs in parallel but with concurrency limit\n            const concurrencyLimit = 5;\n            const jobBatches = this.chunkArray(dueJobs, concurrencyLimit);\n\n            for (const batch of jobBatches) {\n                const promises = batch.map(async (job) => {\n                    try {\n                        const success = await ScheduledJobService.executeJob(job.id);\n\n                        if (success) {\n                            console.log(`Successfully executed job ${job.id} (${job.type})`);\n\n                            await createAuditLog(\n                                'SCHEDULED_JOB_EXECUTED',\n                                'system',\n                                'system',\n                                {\n                                    jobId: job.id,\n                                    jobType: job.type,\n                                    entityId: job.entityId,\n                                    originalScheduledAt: job.scheduledAt.toISOString(),\n                                    executedAt: new Date().toISOString(),\n                                    retryCount: job.retryCount,\n                                }\n                            );\n                        } else {\n                            console.error(`Failed to execute job ${job.id} (${job.type})`);\n                        }\n                    } catch (error) {\n                        console.error(`Error executing job ${job.id}:`, error);\n                    }\n                });\n\n                await Promise.allSettled(promises);\n            }\n        } catch (error) {\n            console.error('Error in job runner:', error);\n        }\n    }\n\n    /**\n     * Utility to chunk array into smaller arrays\n     */\n    private static chunkArray<T>(array: T[], size: number): T[][] {\n        const chunks: T[][] = [];\n        for (let i = 0; i < array.length; i += size) {\n            chunks.push(array.slice(i, i + size));\n        }\n        return chunks;\n    }\n\n    /**\n     * Clean up old completed/failed jobs\n     */\n    static async cleanup(olderThanDays: number = 30): Promise<number> {\n        try {\n            const count = await ScheduledJobService.cleanupOldJobs(olderThanDays);\n\n            if (count > 0) {\n                console.log(`Cleaned up ${count} old scheduled jobs`);\n\n                await createAuditLog(\n                    'SCHEDULED_JOBS_CLEANUP',\n                    'system',\n                    'system',\n                    {\n                        deletedCount: count,\n                        olderThanDays,\n                        timestamp: new Date().toISOString(),\n                    }\n                );\n            }\n\n            return count;\n        } catch (error) {\n            console.error('Error cleaning up old jobs:', error);\n            return 0;\n        }\n    }\n\n    /**\n     * Get status of the job runner\n     */\n    static getStatus(): { isRunning: boolean; intervalId: NodeJS.Timeout | null } {\n        return {\n            isRunning: this.isRunning,\n            intervalId: this.intervalId,\n        };\n    }\n}"
  },
  {
    "path": "lib/services/jobs/scheduled-job.service.ts",
    "content": "import {db} from \"@/lib/db\";\nimport {ScheduledJobType, JobStatus} from \"@prisma/client\";\nimport {createAuditLog} from \"@/lib/utils/auditLog\";\nimport {ChangelogPublishExecutor} from \"@/lib/services/jobs/executors/changelog-publish.executor\"\nimport {TelemetrySendExecutor} from \"@/lib/services/jobs/executors/telemetry-send.executor\"\nimport {SslRenewalExecutor} from \"@/lib/services/jobs/executors/ssl-renewal.executor\"\n\nexport interface CreateScheduledJobParams {\n    type: ScheduledJobType;\n    entityId: string;\n    scheduledAt: Date;\n    maxRetries?: number;\n}\n\nexport interface ScheduledJobExecutor {\n    execute(entityId: string): Promise<void>;\n}\n\nexport class ScheduledJobService {\n    private static executors: Map<ScheduledJobType, ScheduledJobExecutor> = new Map();\n\n    /**\n     * Register an executor for a specific job type\n     */\n    static registerExecutor(type: ScheduledJobType, executor: ScheduledJobExecutor): void {\n        this.executors.set(type, executor);\n    }\n\n    /**\n     * Create a new scheduled job\n     */\n    static async createJob(params: CreateScheduledJobParams): Promise<string> {\n        const job = await db.scheduledJob.create({\n            data: {\n                type: params.type,\n                entityId: params.entityId,\n                scheduledAt: params.scheduledAt,\n                maxRetries: params.maxRetries ?? 3,\n            },\n        });\n\n        return job.id;\n    }\n\n    /**\n     * Cancel a scheduled job\n     */\n    static async cancelJob(jobId: string, userId: string): Promise<boolean> {\n        try {\n            const job = await db.scheduledJob.findUnique({\n                where: {id: jobId},\n            });\n\n            if (!job || job.status !== JobStatus.PENDING) {\n                return false;\n            }\n\n            await db.scheduledJob.update({\n                where: {id: jobId},\n                data: {status: JobStatus.CANCELLED},\n            });\n\n            // Log the cancellation\n            await createAuditLog(\n                'SCHEDULED_JOB_CANCELLED',\n                userId,\n                userId,\n                {\n                    jobId: job.id,\n                    jobType: job.type,\n                    entityId: job.entityId,\n                    originalScheduledAt: job.scheduledAt.toISOString(),\n                }\n            );\n\n            return true;\n        } catch (error) {\n            console.error('Failed to cancel scheduled job:', error);\n            return false;\n        }\n    }\n\n    /**\n     * Get due jobs that need to be executed\n     */\n    static async getDueJobs(): Promise<Array<{\n        id: string;\n        type: ScheduledJobType;\n        entityId: string;\n        scheduledAt: Date;\n        retryCount: number;\n    }>> {\n        const now = new Date();\n\n        return await db.scheduledJob.findMany({\n            where: {\n                status: JobStatus.PENDING,\n                scheduledAt: {\n                    lte: now,\n                },\n            },\n            orderBy: {\n                scheduledAt: 'asc',\n            },\n            select: {\n                id: true,\n                type: true,\n                entityId: true,\n                scheduledAt: true,\n                retryCount: true,\n            },\n        });\n    }\n\n    /**\n     * Execute a scheduled job\n     */\n    static async executeJob(jobId: string): Promise<boolean> {\n        const job = await db.scheduledJob.findUnique({\n            where: {id: jobId},\n        });\n\n        if (!job || job.status !== JobStatus.PENDING) {\n            return false;\n        }\n\n        // Mark as running\n        await db.scheduledJob.update({\n            where: {id: jobId},\n            data: {status: JobStatus.RUNNING},\n        });\n\n        try {\n            const executor = this.executors.get(job.type);\n            if (!executor) {\n                throw new Error(`No executor registered for job type: ${job.type}`);\n            }\n\n            await executor.execute(job.entityId);\n\n            // Mark as completed\n            await db.scheduledJob.update({\n                where: {id: jobId},\n                data: {\n                    status: JobStatus.COMPLETED,\n                    executedAt: new Date(),\n                },\n            });\n\n            return true;\n        } catch (error) {\n            console.error(`Failed to execute job ${jobId}:`, error);\n\n            const shouldRetry = job.retryCount < job.maxRetries;\n\n            await db.scheduledJob.update({\n                where: {id: jobId},\n                data: {\n                    status: shouldRetry ? JobStatus.PENDING : JobStatus.FAILED,\n                    retryCount: job.retryCount + 1,\n                    errorMessage: error instanceof Error ? error.message : 'Unknown error',\n                    // Schedule retry for 5 minutes later if retrying\n                    scheduledAt: shouldRetry ? new Date(Date.now() + 5 * 60 * 1000) : job.scheduledAt,\n                },\n            });\n\n            return false;\n        }\n    }\n\n    /**\n     * Get scheduled jobs for a specific entity\n     */\n    static async getJobsForEntity(entityId: string, type?: ScheduledJobType): Promise<Array<{\n        id: string;\n        type: ScheduledJobType;\n        scheduledAt: Date;\n        status: JobStatus;\n        errorMessage: string | null;\n    }>> {\n        return await db.scheduledJob.findMany({\n            where: {\n                entityId,\n                ...(type && {type}),\n            },\n            orderBy: {\n                scheduledAt: 'desc',\n            },\n            select: {\n                id: true,\n                type: true,\n                scheduledAt: true,\n                status: true,\n                errorMessage: true,\n            },\n        });\n    }\n\n    /**\n     * Clean up old completed/failed jobs\n     */\n    static async cleanupOldJobs(olderThanDays: number = 30): Promise<number> {\n        const cutoffDate = new Date();\n        cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);\n\n        const result = await db.scheduledJob.deleteMany({\n            where: {\n                status: {\n                    in: [JobStatus.COMPLETED, JobStatus.FAILED],\n                },\n                createdAt: {\n                    lt: cutoffDate,\n                },\n            },\n        });\n\n        return result.count;\n    }\n}\n\nScheduledJobService.registerExecutor(\n    ScheduledJobType.PUBLISH_CHANGELOG_ENTRY,\n    new ChangelogPublishExecutor()\n);\n\nScheduledJobService.registerExecutor(\n    ScheduledJobType.TELEMETRY_SEND,\n    new TelemetrySendExecutor()\n);\n\nScheduledJobService.registerExecutor(\n    ScheduledJobType.RENEW_SSL_CERTIFICATE,\n    new SslRenewalExecutor()\n);\n\n// Export types and main service\nexport {ScheduledJobType, JobStatus};\nexport default ScheduledJobService;"
  },
  {
    "path": "lib/services/projects/catch-up/catch-up.service.ts",
    "content": "import {db} from '@/lib/db';\nimport type {\n    CatchUpResponse,\n    CatchUpEntry,\n    CatchUpSummary\n} from '@/lib/types/projects/catch-up/types';\n\nexport class CatchUpService {\n    /**\n     * Get catch-up data for a project since a specified point\n     */\n    static async getCatchUpData(\n        projectId: string,\n        userId: string,\n        since: string = 'auto'\n    ): Promise<CatchUpResponse> {\n        console.log('getCatchUpData called with:', {projectId, userId, since});\n\n        // Verify project exists\n        const project = await db.project.findUnique({\n            where: {id: projectId},\n            include: {changelog: true},\n        });\n\n        if (!project) {\n            throw new Error('Project not found');\n        }\n\n        if (!project.changelog) {\n            console.log('No changelog found for project');\n            return {\n                fromDate: new Date().toISOString(),\n                fromVersion: null,\n                toVersion: null,\n                totalEntries: 0,\n                summary: {features: 0, fixes: 0, other: 0},\n                entries: [],\n            };\n        }\n\n        // Determine the \"since\" date\n        const fromDate = await this.determineSinceDate(userId, since, project.changelog.id);\n        console.log('Determined fromDate:', fromDate);\n\n        // Get entries since that date - Updated query logic\n        const entries = await db.changelogEntry.findMany({\n            where: {\n                changelogId: project.changelog.id,\n                OR: [\n                    {\n                        publishedAt: {\n                            gte: fromDate,\n                            lte: new Date(), // Don't include future scheduled entries\n                        },\n                    },\n                    {\n                        // Include unpublished entries created after the fromDate\n                        publishedAt: null,\n                        createdAt: {\n                            gte: fromDate,\n                        },\n                    },\n                ],\n            },\n            include: {\n                tags: {\n                    select: {\n                        id: true,\n                        name: true,\n                        color: true,\n                    },\n                },\n            },\n            orderBy: [\n                {publishedAt: {sort: 'desc', nulls: 'last'}},\n                {createdAt: 'desc'},\n            ],\n        });\n\n        console.log('Found entries:', entries.length);\n        console.log('Entry details:', entries.map(e => ({\n            id: e.id,\n            title: e.title,\n            version: e.version,\n            publishedAt: e.publishedAt,\n            createdAt: e.createdAt,\n        })));\n\n        // Find version range\n        const {fromVersion, toVersion} = await this.getVersionRange(entries, fromDate, project.changelog.id);\n        console.log('Version range:', {fromVersion, toVersion});\n\n        // Categorize entries\n        const summary = this.categorizeEntries(entries);\n        console.log('Summary:', summary);\n\n        return {\n            fromDate: fromDate.toISOString(),\n            fromVersion,\n            toVersion,\n            totalEntries: entries.length,\n            summary,\n            entries: entries.map(entry => ({\n                id: entry.id,\n                title: entry.title,\n                content: entry.content,\n                version: entry.version,\n                publishedAt: entry.publishedAt,\n                tags: entry.tags,\n            })),\n        };\n    }\n\n    /**\n     * Determine the starting date based on the \"since\" parameter\n     */\n    private static async determineSinceDate(\n        userId: string,\n        since: string,\n        changelogId: string\n    ): Promise<Date> {\n        console.log('determineSinceDate called with:', {userId, since, changelogId});\n\n        if (since === 'auto') {\n            // Try to use user's last login\n            const user = await db.user.findUnique({\n                where: {id: userId},\n                select: {lastLoginAt: true},\n            });\n\n            if (user?.lastLoginAt) {\n                console.log('Using user lastLoginAt:', user.lastLoginAt);\n                return user.lastLoginAt;\n            }\n\n            // Fallback: 7 days ago\n            const fallbackDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);\n            console.log('Using 7-day fallback:', fallbackDate);\n            return fallbackDate;\n        }\n\n        // Handle version format (e.g., \"v1.2.0\", \"1.3.1\")\n        if (since.match(/^v?\\d+\\.\\d+/)) {\n            // Try both with and without 'v' prefix\n            const versionVariants = [\n                since,\n                since.startsWith('v') ? since.substring(1) : `v${since}`,\n            ];\n\n            console.log('Looking for version variants:', versionVariants);\n\n            for (const versionVariant of versionVariants) {\n                const entry = await db.changelogEntry.findFirst({\n                    where: {\n                        changelogId,\n                        version: versionVariant,\n                    },\n                    select: {publishedAt: true, createdAt: true, version: true},\n                    orderBy: {publishedAt: 'asc'},\n                });\n\n                console.log('Version search result for', versionVariant, ':', entry);\n\n                if (entry) {\n                    // Use publishedAt if available, otherwise createdAt\n                    const dateToUse = entry.publishedAt || entry.createdAt;\n                    console.log('Using date from version entry:', dateToUse);\n                    return dateToUse;\n                }\n            }\n\n            // Debug: Let's see what versions exist\n            const allVersions = await db.changelogEntry.findMany({\n                where: {changelogId},\n                select: {version: true, publishedAt: true, createdAt: true},\n            });\n            console.log('All available versions in changelog:', allVersions);\n\n            // If version not found, fallback to 7 days ago\n            const fallbackDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);\n            console.log('Version not found, using 7-day fallback:', fallbackDate);\n            return fallbackDate;\n        }\n\n        // Handle relative dates (e.g., \"7d\", \"1w\", \"1m\")\n        const relativeMatch = since.match(/^(\\d+)([dwm])$/);\n        if (relativeMatch) {\n            const [, amount, unit] = relativeMatch;\n            const multipliers = {d: 1, w: 7, m: 30};\n            const days = parseInt(amount) * multipliers[unit as keyof typeof multipliers];\n            const relativeDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000);\n            console.log('Using relative date:', relativeDate);\n            return relativeDate;\n        }\n\n        // Handle ISO date format\n        try {\n            const parsedDate = new Date(since);\n            if (!isNaN(parsedDate.getTime())) {\n                console.log('Using parsed ISO date:', parsedDate);\n                return parsedDate;\n            }\n        } catch {\n            // Invalid date format\n        }\n\n        // Fallback: 7 days ago\n        const fallbackDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);\n        console.log('Using final fallback:', fallbackDate);\n        return fallbackDate;\n    }\n\n    /**\n     * Get the version range for the entries\n     */\n    private static async getVersionRange(\n        entries: CatchUpEntry[],\n        fromDate: Date,\n        changelogId: string\n    ): Promise<{ fromVersion: string | null; toVersion: string | null }> {\n        if (entries.length === 0) {\n            return {fromVersion: null, toVersion: null};\n        }\n\n        // Get the latest version\n        const latestEntry = entries.find(entry => entry.version);\n        const toVersion = latestEntry?.version || null;\n\n        // Find the version that was current at the fromDate\n        const fromVersionEntry = await db.changelogEntry.findFirst({\n            where: {\n                changelogId,\n                publishedAt: {lte: fromDate},\n                version: {not: null},\n            },\n            select: {version: true},\n            orderBy: {publishedAt: 'desc'},\n        });\n\n        return {\n            fromVersion: fromVersionEntry?.version || null,\n            toVersion,\n        };\n    }\n\n    /**\n     * Categorize entries by tag types\n     */\n    private static categorizeEntries(entries: CatchUpEntry[]): CatchUpSummary {\n        const summary: CatchUpSummary = {\n            features: 0,\n            fixes: 0,\n            other: 0,\n        };\n\n        for (const entry of entries) {\n            const tagNames = entry.tags.map(tag => tag.name.toLowerCase());\n\n            if (tagNames.some(name =>\n                name.includes('feature') ||\n                name.includes('enhancement') ||\n                name.includes('new') ||\n                name.includes('feat')\n            )) {\n                summary.features++;\n            } else if (tagNames.some(name =>\n                name.includes('fix') ||\n                name.includes('bug') ||\n                name.includes('patch') ||\n                name.includes('hotfix')\n            )) {\n                summary.fixes++;\n            } else {\n                summary.other++;\n            }\n        }\n\n        return summary;\n    }\n}"
  },
  {
    "path": "lib/services/projects/importing/index.ts",
    "content": "export {MarkdownParserService} from './markdown-parser.service';\nexport {ImportValidationService} from './validation.service';\nexport {ImportProcessorService} from './processor.service';\n\n// Re-export types for convenience\nexport type {\n    ParsedChangelogEntry,\n    ChangelogSection,\n    ParsedChangelog,\n    ImportPreview,\n    ImportOptions,\n    ImportResult,\n    ImportStats,\n    ImportFormat,\n    FormatDetectionResult,\n    ValidationError,\n    ValidatedEntry\n} from '@/lib/types/projects/importing';\n\n// Main service class that orchestrates the import process\nimport {MarkdownParserService} from './markdown-parser.service';\nimport {ImportValidationService} from './validation.service';\nimport {ImportProcessorService} from './processor.service';\nimport {\n    ParsedChangelog,\n    ImportPreview,\n    ValidatedEntry,\n    ImportOptions,\n    ImportResult\n} from '@/lib/types/projects/importing';\n\nexport class ChangelogImportService {\n    /**\n     * Complete import workflow - parse, validate, and process\n     */\n    static async performCompleteImport(\n        content: string,\n        projectId: string,\n        options: ImportOptions,\n        userId: string\n    ): Promise<{\n        parsed: ParsedChangelog;\n        preview: ImportPreview;\n        result: ImportResult;\n    }> {\n        // Step 1: Parse the content\n        const parsed = MarkdownParserService.parseChangelog(content);\n\n        if (parsed.entries.length === 0) {\n            throw new Error('No valid entries found in the provided content');\n        }\n\n        // Step 2: Validate entries\n        const {validatedEntries, preview} = ImportValidationService.validateEntries(\n            parsed.entries\n        );\n\n        // Step 3: Process the import\n        const result = await ImportProcessorService.processImport(\n            projectId,\n            validatedEntries,\n            options,\n            userId\n        );\n\n        return {parsed, preview, result};\n    }\n\n    /**\n     * Preview import without actually importing\n     */\n    static previewImport(content: string): {\n        parsed: ParsedChangelog;\n        preview: ImportPreview;\n        validatedEntries: ValidatedEntry[];\n    } {\n        const parsed = MarkdownParserService.parseChangelog(content);\n        const {validatedEntries, preview} = ImportValidationService.validateEntries(\n            parsed.entries\n        );\n\n        return {parsed, preview, validatedEntries};\n    }\n\n    /**\n     * Detect the format of changelog content\n     */\n    static detectFormat(content: string) {\n        return MarkdownParserService.detectFormat(content);\n    }\n\n    /**\n     * Get import recommendations based on content analysis\n     */\n    static getImportRecommendations(content: string): {\n        recommendedStrategy: 'merge' | 'replace' | 'append';\n        recommendedOptions: Partial<ImportOptions>;\n        warnings: string[];\n        suggestions: string[];\n    } {\n        // const detection = MarkdownParserService.detectFormat(content);\n        const parsed = MarkdownParserService.parseChangelog(content);\n\n        const warnings: string[] = [];\n        const suggestions: string[] = [];\n        let recommendedStrategy: 'merge' | 'replace' | 'append' = 'merge';\n\n        // Analyze content to make recommendations\n        const hasVersions = parsed.metadata.hasVersions;\n        const hasDates = parsed.metadata.hasDates;\n        const entryCount = parsed.entries.length;\n\n        // Strategy recommendations\n        if (entryCount > 50) {\n            recommendedStrategy = 'replace';\n            warnings.push('Large number of entries detected. Consider using replace strategy.');\n        } else if (entryCount > 10) {\n            recommendedStrategy = 'merge';\n            suggestions.push('Medium-sized import. Merge strategy recommended to preserve existing data.');\n        } else {\n            recommendedStrategy = 'append';\n            suggestions.push('Small import. Append strategy will add entries to existing ones.');\n        }\n\n        // Date handling recommendations\n        let dateHandling: 'preserve' | 'current' | 'sequence' = 'preserve';\n        if (!hasDates) {\n            dateHandling = 'current';\n            warnings.push('No dates found in entries. Consider using current date for all entries.');\n        }\n\n        // Version handling recommendations\n        let autoGenerateVersions = false;\n        if (!hasVersions && entryCount > 5) {\n            autoGenerateVersions = true;\n            suggestions.push('No versions detected. Auto-generation recommended for better organization.');\n        }\n\n        // Publishing recommendations\n        let publishImportedEntries = false;\n        if (entryCount <= 10 && hasVersions) {\n            publishImportedEntries = true;\n            suggestions.push('Small import with versions. Consider publishing entries immediately.');\n        }\n\n        const recommendedOptions: Partial<ImportOptions> = {\n            strategy: recommendedStrategy,\n            dateHandling,\n            autoGenerateVersions,\n            publishImportedEntries,\n            conflictResolution: 'skip',\n            preserveExistingEntries: true\n        };\n\n        return {\n            recommendedStrategy,\n            recommendedOptions,\n            warnings,\n            suggestions\n        };\n    }\n\n    /**\n     * Validate content before showing import UI\n     */\n    static validateContent(content: string): {\n        isValid: boolean;\n        errors: string[];\n        warnings: string[];\n        stats: {\n            characterCount: number;\n            lineCount: number;\n            estimatedEntries: number;\n            hasMarkdown: boolean;\n        };\n    } {\n        const errors: string[] = [];\n        const warnings: string[] = [];\n\n        // Basic validation\n        if (!content || typeof content !== 'string') {\n            errors.push('Content must be a non-empty string');\n        }\n\n        if (content.length < 10) {\n            errors.push('Content is too short to contain valid changelog entries');\n        }\n\n        if (content.length > 1000000) { // 1MB limit\n            errors.push('Content is too large (max 1MB)');\n        }\n\n        // Content analysis\n        const lines = content.split('\\n');\n        const hasMarkdown = /[#*`\\[\\]]/.test(content);\n        const hasHeaders = lines.some(line => /^#+\\s/.test(line));\n        const hasLists = lines.some(line => /^\\s*[-*+]\\s/.test(line));\n\n        if (!hasMarkdown && !hasHeaders && !hasLists) {\n            warnings.push('Content does not appear to be in a recognized changelog format');\n        }\n\n        // Try to estimate entry count\n        const headerCount = lines.filter(line => /^#+\\s/.test(line)).length;\n        const listItemCount = lines.filter(line => /^\\s*[-*+]\\s/.test(line)).length;\n        const estimatedEntries = Math.max(headerCount, Math.floor(listItemCount / 3));\n\n        if (estimatedEntries === 0) {\n            warnings.push('No potential changelog entries detected');\n        }\n\n        const stats = {\n            characterCount: content.length,\n            lineCount: lines.length,\n            estimatedEntries,\n            hasMarkdown\n        };\n\n        return {\n            isValid: errors.length === 0,\n            errors,\n            warnings,\n            stats\n        };\n    }\n}"
  },
  {
    "path": "lib/services/projects/importing/integrations/canny.service.ts",
    "content": "// lib/services/projects/importing/integrations/canny.service.ts\n\nimport {\n    CannyEntry,\n    CannyApiResponse,\n    CannyImportOptions\n} from '@/lib/types/projects/importing/canny';\nimport { ValidatedEntry } from '@/lib/types/projects/importing';\n\nexport class CannyService {\n    private static readonly API_URL = 'https://canny.io/api/v1/entries/list';\n\n    /**\n     * Validate API key by making a test request\n     */\n    static async validateApiKey(apiKey: string): Promise<{ valid: boolean; error?: string }> {\n        try {\n            const formData = new FormData();\n            formData.append('apiKey', apiKey);\n            formData.append('limit', '1');\n\n            const response = await fetch(this.API_URL, {\n                method: 'POST',\n                body: formData\n            });\n\n            if (response.status === 401 || response.status === 403) {\n                return { valid: false, error: 'Invalid API key or insufficient permissions' };\n            }\n\n            if (!response.ok) {\n                return { valid: false, error: `API error: ${response.status}` };\n            }\n\n            return { valid: true };\n        } catch {\n            return { valid: false, error: 'Network error' };\n        }\n    }\n\n    /**\n     * Fetch all entries from Canny\n     */\n    static async fetchEntries(options: CannyImportOptions): Promise<{\n        entries: CannyEntry[];\n        error?: string;\n    }> {\n        const allEntries: CannyEntry[] = [];\n        let skip = 0;\n        const limit = 50;\n\n        try {\n            while (allEntries.length < options.maxEntries) {\n                const formData = new FormData();\n                formData.append('apiKey', options.apiKey);\n                formData.append('limit', limit.toString());\n                formData.append('skip', skip.toString());\n                formData.append('sort', 'created');\n\n                const response = await fetch(this.API_URL, {\n                    method: 'POST',\n                    body: formData\n                });\n\n                if (!response.ok) {\n                    return { entries: [], error: `API request failed: ${response.status}` };\n                }\n\n                const data: CannyApiResponse = await response.json();\n\n                // Filter by status\n                const filteredEntries = options.statusFilter === 'all'\n                    ? data.entries\n                    : data.entries.filter(entry => entry.status === options.statusFilter);\n\n                allEntries.push(...filteredEntries);\n\n                if (!data.hasMore || filteredEntries.length === 0) {\n                    break;\n                }\n\n                skip += limit;\n\n                // Safety check\n                if (skip > 1000) {\n                    break;\n                }\n            }\n\n            // Limit to max entries\n            const limitedEntries = allEntries.slice(0, options.maxEntries);\n\n            return { entries: limitedEntries };\n        } catch (error) {\n            return {\n                entries: [],\n                error: error instanceof Error ? error.message : 'Unknown error'\n            };\n        }\n    }\n\n    /**\n     * Convert Canny entries to ValidatedEntry format\n     */\n    static convertEntries(entries: CannyEntry[], options: CannyImportOptions): ValidatedEntry[] {\n        return entries.map(entry => {\n            // Build content - ensure we have some content\n            let content = '';\n\n            if (entry.markdownDetails && entry.markdownDetails.trim()) {\n                content = entry.markdownDetails.trim();\n            } else if (entry.plaintextDetails && entry.plaintextDetails.trim()) {\n                content = entry.plaintextDetails.trim();\n            } else {\n                content = 'No content provided.';\n            }\n\n            // Add related posts if any\n            if (entry.posts && Array.isArray(entry.posts) && entry.posts.length > 0) {\n                content += '\\n\\n**Related Feature Requests:**\\n';\n                entry.posts.forEach(post => {\n                    if (post && post.title && post.url) {\n                        const status = post.status || 'unknown';\n                        content += `- [${post.title}](${post.url}) - ${status}\\n`;\n                    }\n                });\n            }\n\n            // Collect tags\n            const tags: string[] = [];\n\n            // Add entry types\n            if (entry.types && Array.isArray(entry.types)) {\n                tags.push(...entry.types.filter(type => type && typeof type === 'string'));\n            }\n\n            // Add labels if enabled\n            if (options.includeLabels && entry.labels && Array.isArray(entry.labels)) {\n                entry.labels.forEach(label => {\n                    if (label && label.name && typeof label.name === 'string') {\n                        tags.push(label.name.toLowerCase().trim());\n                    }\n                });\n            }\n\n            // Add post tags if enabled\n            if (options.includePostTags && entry.posts && Array.isArray(entry.posts)) {\n                entry.posts.forEach(post => {\n                    if (post && post.tags && Array.isArray(post.tags)) {\n                        post.tags.forEach(tag => {\n                            if (tag && tag.name && typeof tag.name === 'string') {\n                                tags.push(tag.name.toLowerCase().trim());\n                            }\n                        });\n                    }\n                });\n            }\n\n            // Remove duplicates and empty tags\n            const uniqueTags = [...new Set(tags.filter(tag => tag && tag.trim().length > 0))];\n\n            // Create the validated entry\n            const validatedEntry: ValidatedEntry = {\n                title: entry.title || 'Untitled Entry',\n                content: content,\n                version: undefined,\n                publishedAt: entry.publishedAt ? new Date(entry.publishedAt) : undefined,\n                tags: uniqueTags,\n                isValid: true,\n                errors: [],\n                warnings: [],\n                suggestedFixes: {},\n                metadata: {\n                    source: 'canny',\n                    cannyId: entry.id,\n                    cannyUrl: entry.url,\n                    likes: (entry.reactions && typeof entry.reactions.like === 'number') ? entry.reactions.like : 0,\n                    postCount: (entry.posts && Array.isArray(entry.posts)) ? entry.posts.length : 0,\n                    status: entry.status || 'unknown',\n                    importedAt: new Date().toISOString()\n                }\n            };\n\n            return validatedEntry;\n        });\n    }\n}"
  },
  {
    "path": "lib/services/projects/importing/markdown-parser.service.ts",
    "content": "// lib/services/projects/importing/markdown-parser.service.ts\n\nimport {\n    ParsedChangelog,\n    ParsedChangelogEntry,\n    ChangelogSection,\n    FormatDetectionResult,\n    ImportFormat\n} from '@/lib/types/projects/importing';\n\nexport class MarkdownParserService {\n    private static readonly VERSION_PATTERNS = [\n        /^#+\\s*(?:\\[?v?(\\d+\\.\\d+\\.\\d+(?:-[a-zA-Z0-9.-]+)?)\\]?)/i,\n        /^#+\\s*(?:version\\s+)?(\\d+\\.\\d+\\.\\d+(?:-[a-zA-Z0-9.-]+)?)/i,\n        /^#+\\s*(?:release\\s+)?(\\d+\\.\\d+\\.\\d+(?:-[a-zA-Z0-9.-]+)?)/i,\n        /^\\[(\\d+\\.\\d+\\.\\d+(?:-[a-zA-Z0-9.-]+)?)\\]/i, // [1.0.3] format\n    ];\n\n    private static readonly DATE_PATTERNS = [\n        /(\\d{4}-\\d{2}-\\d{2})/,\n        /(\\d{2}\\/\\d{2}\\/\\d{4})/,\n        /(\\d{1,2}\\s+(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\\s+\\d{4})/i,\n    ];\n\n    private static readonly SECTION_MARKERS = [\n        'added', 'changed', 'deprecated', 'removed', 'fixed', 'security',\n        'features', 'bug fixes', 'improvements', 'breaking changes',\n        'enhancements', 'patches', 'updates', 'new', 'fixes'\n    ];\n\n    /**\n     * Detect the format of the changelog\n     */\n    static detectFormat(content: string): FormatDetectionResult {\n        const lines = content.split('\\n');\n        const characteristics: string[] = [];\n        let confidence = 0;\n\n        // Check for Keep a Changelog format\n        const hasKeepAChangelogHeader = /keep\\s*a\\s*changelog/i.test(content);\n        const hasUnreleasedSection = /unreleased/i.test(content);\n        const hasVersionLinks = /\\[[\\d.]+\\]:\\s*http/i.test(content);\n\n        if (hasKeepAChangelogHeader || (hasUnreleasedSection && hasVersionLinks)) {\n            characteristics.push('Keep a Changelog format detected');\n            confidence += 0.4;\n        }\n\n        // Check for GitHub Releases format\n        const hasReleaseHeaders = lines.some(line =>\n            /^#+\\s*(?:release|v?\\d+\\.\\d+\\.\\d+)/i.test(line)\n        );\n\n        if (hasReleaseHeaders) {\n            characteristics.push('GitHub Releases format detected');\n            confidence += 0.3;\n        }\n\n        // Check for structured sections\n        const hasSectionHeaders = lines.some(line =>\n            this.SECTION_MARKERS.some(marker =>\n                new RegExp(`^#+\\\\s*${marker}`, 'i').test(line)\n            )\n        );\n\n        if (hasSectionHeaders) {\n            characteristics.push('Structured sections found');\n            confidence += 0.2;\n        }\n\n        // Analyze structure\n        const structure = {\n            hasVersionHeaders: lines.some(line =>\n                this.VERSION_PATTERNS.some(pattern => pattern.test(line))\n            ),\n            hasDateHeaders: lines.some(line =>\n                this.DATE_PATTERNS.some(pattern => pattern.test(line))\n            ),\n            hasTypeHeaders: hasSectionHeaders,\n            usesListFormat: /^\\s*[-*+]\\s/.test(content),\n            usesMarkdownSyntax: /[#*`\\[\\]]/.test(content)\n        };\n\n        // Determine format based on characteristics\n        let format: ImportFormat = 'simple';\n\n        if (hasKeepAChangelogHeader || (hasUnreleasedSection && hasVersionLinks)) {\n            format = 'keepachangelog';\n            confidence += 0.2;\n        } else if (hasReleaseHeaders && structure.hasVersionHeaders) {\n            format = 'github_releases';\n            confidence += 0.15;\n        } else if (structure.usesMarkdownSyntax && structure.hasVersionHeaders) {\n            format = 'custom';\n            confidence += 0.1;\n        }\n\n        return {\n            format,\n            confidence: Math.min(confidence, 1),\n            characteristics,\n            structure\n        };\n    }\n\n    /**\n     * Parse markdown changelog content\n     */\n    static parseChangelog(content: string): ParsedChangelog {\n        const lines = content.split('\\n');\n        const sections: ChangelogSection[] = [];\n        const entries: ParsedChangelogEntry[] = [];\n        const parseWarnings: string[] = [];\n\n        let currentEntry: Partial<ParsedChangelogEntry> | null = null;\n        let contentBuffer: string[] = [];\n        let inVersionSection = false;\n\n        for (let i = 0; i < lines.length; i++) {\n            const line = lines[i];\n            const trimmedLine = line.trim();\n\n            // Skip the main title (# Changelog)\n            if (trimmedLine.startsWith('# ') && (trimmedLine.toLowerCase().includes('changelog') || i === 0)) {\n                continue;\n            }\n\n            // Check for version headers (## [version] - date or ## [](link) (date))\n            const headerMatch = line.match(/^(#{1,3})\\s+(.+)$/);\n            if (headerMatch && headerMatch[1].length <= 2) { // Only h1 and h2 headers\n                // Save previous entry if exists\n                if (currentEntry && currentEntry.title && inVersionSection) {\n                    const processedContent = this.processContentBuffer(contentBuffer);\n                    if (processedContent.trim()) {\n                        currentEntry.content = processedContent;\n                        entries.push(currentEntry as ParsedChangelogEntry);\n                    }\n                    currentEntry = null;\n                    contentBuffer = [];\n                }\n\n                const level = headerMatch[1].length;\n                const headerText = headerMatch[2].trim();\n\n                // Parse header info\n                const { version, date, cleanTitle } = this.parseHeaderInfo(headerText);\n\n                // Only create entry for version headers (not subsections)\n                if (this.looksLikeVersionHeader(headerText)) {\n                    currentEntry = {\n                        title: cleanTitle,\n                        version: version || undefined,\n                        publishedAt: date || undefined,\n                        content: '',\n                        tags: []\n                    };\n                    inVersionSection = true;\n                    contentBuffer = [];\n                } else {\n                    inVersionSection = false;\n                }\n\n                // Create section for all headers\n                const currentSection: ChangelogSection = {\n                    heading: headerText,\n                    level,\n                    content: '',\n                    entries: [],\n                    rawContent: line\n                };\n                sections.push(currentSection);\n                continue;\n            }\n\n            // If we're in a version section, collect all content\n            if (inVersionSection && currentEntry) {\n                // Skip empty lines at the start\n                if (contentBuffer.length === 0 && !trimmedLine) {\n                    continue;\n                }\n                contentBuffer.push(line);\n            }\n        }\n\n        // Finalize last entry\n        if (currentEntry && currentEntry.title && inVersionSection) {\n            const processedContent = this.processContentBuffer(contentBuffer);\n            if (processedContent.trim()) {\n                currentEntry.content = processedContent;\n                entries.push(currentEntry as ParsedChangelogEntry);\n            }\n        }\n\n        // Filter entries and add warnings\n        if (entries.length === 0) {\n            parseWarnings.push('No valid changelog entries found');\n        }\n\n        // Analyze metadata\n        const hasVersions = entries.some(e => e.version);\n        const hasDates = entries.some(e => e.publishedAt);\n\n        const formatDetection = this.detectFormat(content);\n\n        return {\n            sections,\n            entries,\n            metadata: {\n                totalSections: sections.length,\n                totalEntries: entries.length,\n                hasVersions,\n                hasDates,\n                originalFormat: formatDetection.format,\n                parseWarnings\n            }\n        };\n    }\n\n    /**\n     * Parse header information to extract version, date, and clean title\n     */\n    private static parseHeaderInfo(headerText: string): {\n        version?: string;\n        date?: Date;\n        cleanTitle: string;\n    } {\n        let version: string | undefined;\n        let date: Date | undefined;\n        let cleanTitle = headerText;\n\n        // Handle [version] - date format (CLI generated) - MOST SPECIFIC FIRST\n        const cliVersionMatch = headerText.match(/^\\[(\\d+\\.\\d+\\.\\d+(?:-[a-zA-Z0-9.-]+)?)\\]\\s*-\\s*(.+)$/);\n        if (cliVersionMatch) {\n            version = cliVersionMatch[1]; // Extract exact version like \"1.0.3\"\n            const dateStr = cliVersionMatch[2];\n            const parsedDate = new Date(dateStr);\n            if (!isNaN(parsedDate.getTime())) {\n                date = parsedDate;\n            }\n            cleanTitle = `Version ${version} - ${dateStr}`;\n            return { version, date, cleanTitle };\n        }\n\n        // Handle GitHub-style links: [](https://github.com/repo/compare/v1.6.1...v) (2025-07-09)\n        const githubLinkMatch = headerText.match(/^\\[\\]\\([^)]*\\/compare\\/[^)]*\\)\\s*\\(([^)]+)\\)$/);\n        if (githubLinkMatch) {\n            const dateStr = githubLinkMatch[1];\n            const parsedDate = new Date(dateStr);\n            if (!isNaN(parsedDate.getTime())) {\n                date = parsedDate;\n            }\n\n            // Try to extract version from the compare URL\n            const urlMatch = headerText.match(/\\/compare\\/v?([^.]+\\.[^.]+\\.[^)]*)\\.\\.\\./);\n            if (urlMatch) {\n                version = urlMatch[1];\n            }\n\n            cleanTitle = `Release ${dateStr}`;\n            return { version, date, cleanTitle };\n        }\n\n        // Handle [version] (date) format\n        const versionDateMatch = headerText.match(/^\\[([^\\]]+)\\]\\s*\\(([^)]+)\\)$/);\n        if (versionDateMatch) {\n            version = versionDateMatch[1];\n            const dateStr = versionDateMatch[2];\n            const parsedDate = new Date(dateStr);\n            if (!isNaN(parsedDate.getTime())) {\n                date = parsedDate;\n            }\n            cleanTitle = `${version} - ${dateStr}`;\n            return { version, date, cleanTitle };\n        }\n\n        // Extract version from standard patterns - BUT CHECK CLI PATTERN FIRST\n        // Look for [version] at the start of the header text\n        const bracketVersionMatch = headerText.match(/^\\[(\\d+\\.\\d+\\.\\d+(?:-[a-zA-Z0-9.-]+)?)\\]/);\n        if (bracketVersionMatch) {\n            version = bracketVersionMatch[1];\n            cleanTitle = headerText.replace(bracketVersionMatch[0], '').trim();\n            // Remove leading dash if present\n            cleanTitle = cleanTitle.replace(/^-\\s*/, '').trim();\n        } else {\n            // Fall back to other version patterns\n            for (const pattern of this.VERSION_PATTERNS) {\n                const match = headerText.match(pattern);\n                if (match) {\n                    version = match[1];\n                    cleanTitle = headerText.replace(pattern, '').trim();\n                    break;\n                }\n            }\n        }\n\n        // Extract date from standard patterns if not already found\n        if (!date) {\n            for (const pattern of this.DATE_PATTERNS) {\n                const match = headerText.match(pattern);\n                if (match) {\n                    const parsedDate = new Date(match[1]);\n                    if (!isNaN(parsedDate.getTime())) {\n                        date = parsedDate;\n                        cleanTitle = cleanTitle.replace(pattern, '').trim();\n                    }\n                    break;\n                }\n            }\n        }\n\n        // Clean up title\n        cleanTitle = cleanTitle\n            .replace(/^\\[|\\]$/g, '') // Remove brackets\n            .replace(/^-\\s*/, '') // Remove leading dash\n            .replace(/^\\(\\s*|\\s*\\)$/g, '') // Remove parentheses\n            .trim();\n\n        // If title is empty, generate one\n        if (!cleanTitle) {\n            if (version) {\n                cleanTitle = `Version ${version}`;\n            } else if (date) {\n                cleanTitle = `Release ${date.toISOString().split('T')[0]}`;\n            } else {\n                cleanTitle = 'Release';\n            }\n        }\n\n        return { version, date, cleanTitle };\n    }\n\n    /**\n     * Parse list item to extract version and clean title\n     */\n    private static parseListItemInfo(itemText: string): {\n        version?: string;\n        cleanTitle: string;\n    } {\n        let version: string | undefined;\n        let cleanTitle = itemText;\n\n        // Check if list item starts with version\n        const versionMatch = itemText.match(/^(?:v?(\\d+\\.\\d+\\.\\d+(?:-[a-zA-Z0-9.-]+)?)):?\\s*(.+)$/);\n        if (versionMatch) {\n            version = versionMatch[1];\n            cleanTitle = versionMatch[2];\n        }\n\n        return { version, cleanTitle };\n    }\n\n    /**\n     * Extract tags from text using common patterns\n     */\n    private static extractTags(text: string): string[] {\n        const tags: string[] = [];\n\n        // Look for bracketed tags like [FEATURE], [BUG], etc.\n        const bracketTags = text.match(/\\[([A-Z]+)\\]/g);\n        if (bracketTags) {\n            tags.push(...bracketTags.map(tag => tag.slice(1, -1).toLowerCase()));\n        }\n\n        // Look for prefixed tags like \"feat:\", \"fix:\", etc.\n        const prefixMatch = text.match(/^(feat|fix|docs|style|refactor|test|chore|perf)(?:\\([^)]+\\))?:\\s*/i);\n        if (prefixMatch) {\n            tags.push(prefixMatch[1].toLowerCase());\n        }\n\n        return tags;\n    }\n\n    /**\n     * Process content buffer into formatted markdown content\n     */\n    private static processContentBuffer(contentBuffer: string[]): string {\n        if (contentBuffer.length === 0) return '';\n\n        const processedLines: string[] = [];\n        let currentSection = '';\n\n        for (let i = 0; i < contentBuffer.length; i++) {\n            const line = contentBuffer[i];\n            const trimmedLine = line.trim();\n\n            // Handle subsection headers (### Features, ### Bug Fixes, etc.)\n            const subHeaderMatch = line.match(/^(#{3,6})\\s+(.+)$/);\n            if (subHeaderMatch) {\n                const subHeaderText = subHeaderMatch[2].trim();\n                if (currentSection) {\n                    processedLines.push(''); // Add spacing before new section\n                }\n                processedLines.push(`**${subHeaderText}**`);\n                processedLines.push('');\n                currentSection = subHeaderText;\n                continue;\n            }\n\n            // Handle list items - preserve original formatting\n            const listItemMatch = line.match(/^\\s*[-*+]\\s+(.+)$/);\n            if (listItemMatch) {\n                processedLines.push(line);\n                continue;\n            }\n\n            // Handle regular content\n            if (trimmedLine || processedLines.length > 0) {\n                processedLines.push(line);\n            }\n        }\n\n        // Clean up trailing empty lines\n        while (processedLines.length > 0 && !processedLines[processedLines.length - 1].trim()) {\n            processedLines.pop();\n        }\n\n        return processedLines.join('\\n');\n    }\n\n    /**\n     * Check if header looks like a version header\n     */\n    private static looksLikeVersionHeader(headerText: string): boolean {\n        // Check for version patterns\n        if (this.VERSION_PATTERNS.some(pattern => pattern.test(headerText))) {\n            return true;\n        }\n\n        // Check for [version] - date format (CLI generated)\n        if (/^\\[\\d+\\.\\d+\\.\\d+(?:-[a-zA-Z0-9.-]+)?\\]\\s*-\\s*/.test(headerText)) {\n            return true;\n        }\n\n        // Check for GitHub-style empty link with date: [](link) (date)\n        if (/^\\[\\]\\([^)]+\\)\\s*\\([^)]+\\)$/.test(headerText)) {\n            return true;\n        }\n\n        // Check for [version] (date) format\n        if (/^\\[[^\\]]+\\]\\s*\\([^)]+\\)$/.test(headerText)) {\n            return true;\n        }\n\n        // Check for unreleased/latest\n        if (/^(unreleased|latest|current)/i.test(headerText)) {\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * Finalize an entry by processing accumulated content\n     */\n    private static finalizeEntry(\n        entry: Partial<ParsedChangelogEntry>,\n        contentBuffer: string[],\n        entries: ParsedChangelogEntry[]\n    ): void {\n        const content = this.processContentBuffer(contentBuffer);\n\n        if (entry.title && content.trim()) {\n            entries.push({\n                title: entry.title,\n                content: content,\n                version: entry.version,\n                publishedAt: entry.publishedAt,\n                tags: entry.tags || [],\n                metadata: {\n                    originalIndex: entries.length,\n                    hasContent: Boolean(content.trim()),\n                    estimatedReadingTime: Math.ceil(content.split(' ').length / 200)\n                }\n            });\n        }\n    }\n}"
  },
  {
    "path": "lib/services/projects/importing/processor.service.ts",
    "content": "// lib/services/projects/importing/processor.service.ts\n\nimport { db } from '@/lib/db';\nimport type { Prisma } from '@prisma/client';\nimport {\n    ValidatedEntry,\n    ImportOptions,\n    ImportResult,\n    ImportStats\n} from '@/lib/types/projects/importing';\n\ninterface ImportContext {\n    projectId: string;\n    changelogId: string;\n    userId: string;\n    options: ImportOptions;\n    stats: ImportStats;\n}\n\ntype PrismaTransaction = Prisma.TransactionClient;\n\nexport class ImportProcessorService {\n    /**\n     * Process validated entries and import them into the database\n     */\n    static async processImport(\n        projectId: string,\n        validatedEntries: ValidatedEntry[],\n        options: ImportOptions,\n        userId: string\n    ): Promise<ImportResult> {\n        const startTime = new Date();\n        const stats: ImportStats = {\n            processed: 0,\n            imported: 0,\n            skipped: 0,\n            errors: 0,\n            startTime\n        };\n\n        const result: ImportResult = {\n            success: false,\n            importedCount: 0,\n            skippedCount: 0,\n            errorCount: 0,\n            createdEntries: [],\n            warnings: [],\n            errors: [],\n            processingTime: 0\n        };\n\n        try {\n            // Get or create changelog for the project\n            const changelog = await this.ensureChangelog(projectId);\n\n            const context: ImportContext = {\n                projectId,\n                changelogId: changelog.id,\n                userId,\n                options,\n                stats\n            };\n\n            // Handle different strategies\n            if (options.strategy === 'replace') {\n                await this.handleReplaceStrategy(context, validatedEntries, result);\n            } else {\n                await this.handleMergeOrAppendStrategy(context, validatedEntries, result);\n            }\n\n            // Update final stats\n            stats.endTime = new Date();\n            result.processingTime = stats.endTime.getTime() - stats.startTime.getTime();\n            result.success = result.errorCount < validatedEntries.length / 2; // Success if less than 50% errors\n\n            return result;\n\n        } catch (error) {\n            console.error('Import processing failed:', error);\n\n            result.errors.push({\n                entry: validatedEntries[0] || {} as ValidatedEntry,\n                error: error instanceof Error ? error.message : 'Unknown error occurred'\n            });\n            result.errorCount = validatedEntries.length;\n            result.processingTime = Date.now() - startTime.getTime();\n\n            return result;\n        }\n    }\n\n    /**\n     * Ensure changelog exists for the project\n     */\n    private static async ensureChangelog(projectId: string) {\n        let changelog = await db.changelog.findFirst({\n            where: { projectId }\n        });\n\n        if (!changelog) {\n            changelog = await db.changelog.create({\n                data: { projectId }\n            });\n        }\n\n        return changelog;\n    }\n\n    /**\n     * Handle replace strategy - removes existing entries first\n     */\n    private static async handleReplaceStrategy(\n        context: ImportContext,\n        validatedEntries: ValidatedEntry[],\n        result: ImportResult\n    ): Promise<void> {\n        // Use transaction for replace strategy\n        await db.$transaction(async (tx) => {\n            // Remove existing entries if not preserving\n            if (!context.options.preserveExistingEntries) {\n                await tx.changelogEntry.deleteMany({\n                    where: { changelogId: context.changelogId }\n                });\n                result.warnings.push('All existing entries were replaced');\n            }\n\n            // Import new entries\n            await this.importEntries(context, validatedEntries, result, tx);\n        });\n    }\n\n    /**\n     * Handle merge or append strategy\n     */\n    private static async handleMergeOrAppendStrategy(\n        context: ImportContext,\n        validatedEntries: ValidatedEntry[],\n        result: ImportResult\n    ): Promise<void> {\n        // Get existing entries to check for conflicts\n        const existingEntries = await db.changelogEntry.findMany({\n            where: { changelogId: context.changelogId },\n            select: { version: true, title: true }\n        });\n\n        const existingVersions = new Set(\n            existingEntries.map(e => e.version).filter(Boolean)\n        );\n\n        // Filter entries based on conflict resolution\n        const entriesToImport = await this.resolveConflicts(\n            validatedEntries,\n            existingVersions,\n            context.options.conflictResolution,\n            result\n        );\n\n        // Import the resolved entries\n        await this.importEntries(context, entriesToImport, result);\n    }\n\n    /**\n     * Resolve conflicts between new and existing entries\n     */\n    private static async resolveConflicts(\n        validatedEntries: ValidatedEntry[],\n        existingVersions: Set<string | null>,\n        conflictResolution: string,\n        result: ImportResult\n    ): Promise<ValidatedEntry[]> {\n        const entriesToImport: ValidatedEntry[] = [];\n\n        for (const entry of validatedEntries) {\n            const hasVersionConflict = entry.version && existingVersions.has(entry.version);\n\n            if (hasVersionConflict) {\n                switch (conflictResolution) {\n                    case 'skip':\n                        result.skippedCount++;\n                        result.warnings.push(`Skipped entry with duplicate version: ${entry.version}`);\n                        break;\n\n                    case 'overwrite':\n                        entriesToImport.push(entry);\n                        result.warnings.push(`Will overwrite existing entry with version: ${entry.version}`);\n                        break;\n\n                    case 'prompt':\n                        // In a real implementation, this would prompt the user\n                        // For now, we'll default to skip\n                        result.skippedCount++;\n                        result.warnings.push(`Conflict detected for version ${entry.version} - skipped (would prompt user)`);\n                        break;\n\n                    default:\n                        entriesToImport.push(entry);\n                }\n            } else {\n                entriesToImport.push(entry);\n            }\n        }\n\n        return entriesToImport;\n    }\n\n    /**\n     * Import entries into the database\n     */\n    private static async importEntries(\n        context: ImportContext,\n        entries: ValidatedEntry[],\n        result: ImportResult,\n        tx?: PrismaTransaction\n    ): Promise<void> {\n        const dbClient = tx || db;\n\n        for (const entry of entries) {\n            context.stats.processed++;\n\n            try {\n                // Skip invalid entries\n                if (!entry.isValid) {\n                    context.stats.skipped++;\n                    result.skippedCount++;\n                    result.errors.push({\n                        entry,\n                        error: 'Entry failed validation'\n                    });\n                    continue;\n                }\n\n                // Prepare entry data\n                const entryData = await this.prepareEntryData(entry, context);\n\n                // Create the changelog entry\n                const createdEntry = await dbClient.changelogEntry.create({\n                    data: entryData,\n                    include: { tags: true }\n                });\n\n                // Track success\n                context.stats.imported++;\n                result.importedCount++;\n                result.createdEntries.push({\n                    id: createdEntry.id,\n                    title: createdEntry.title,\n                    version: createdEntry.version || undefined\n                });\n\n            } catch (error) {\n                context.stats.errors++;\n                result.errorCount++;\n                result.errors.push({\n                    entry,\n                    error: error instanceof Error ? error.message : 'Failed to create entry'\n                });\n                console.error(`Failed to import entry \"${entry.title}\":`, error);\n            }\n        }\n    }\n\n    /**\n     * Prepare entry data for database insertion\n     */\n    private static async prepareEntryData(entry: ValidatedEntry, context: ImportContext) {\n        // Handle date based on options\n        let publishedAt: Date | null = null;\n        let createdAt: Date = new Date(); // Default to current time\n\n        switch (context.options.dateHandling) {\n            case 'preserve':\n                if (entry.publishedAt) {\n                    publishedAt = context.options.publishImportedEntries ? entry.publishedAt : null;\n                    createdAt = entry.publishedAt; // Use the original date as creation date\n                }\n                break;\n            case 'current':\n                publishedAt = context.options.publishImportedEntries ? new Date() : null;\n                createdAt = new Date();\n                break;\n            case 'sequence':\n                // For sequence, we'd need to calculate based on order\n                publishedAt = context.options.publishImportedEntries ? new Date() : null;\n                createdAt = new Date();\n                break;\n        }\n\n        // Prepare tags\n        const tagConnections = await this.prepareTags(\n            entry.tags || [],\n            context.changelogId,\n            context.options.defaultTags\n        );\n\n        return {\n            title: entry.title,\n            content: entry.content,\n            version: entry.version || null,\n            publishedAt,\n            createdAt,\n            updatedAt: createdAt,\n            changelogId: context.changelogId,\n            tags: {\n                connect: tagConnections\n            }\n        };\n    }\n\n    /**\n     * Prepare tags for the entry\n     */\n    private static async prepareTags(\n        entryTags: string[],\n        changelogId: string,\n        defaultTags: string[]\n    ): Promise<Array<{ id: string }>> {\n        const allTagNames = [...new Set([...entryTags, ...defaultTags])];\n        const tagConnections: Array<{ id: string }> = [];\n\n        for (const tagName of allTagNames) {\n            if (!tagName.trim()) continue;\n\n            // Find or create tag\n            let tag = await db.changelogTag.findFirst({\n                where: {\n                    name: tagName.toLowerCase()\n                }\n            });\n\n            if (!tag) {\n                tag = await db.changelogTag.create({\n                    data: {\n                        name: tagName.toLowerCase()\n                    }\n                });\n            }\n\n            tagConnections.push({ id: tag.id });\n        }\n\n        return tagConnections;\n    }\n\n    /**\n     * Get import statistics for a project\n     */\n    static async getImportHistory(projectId: string): Promise<{\n        totalImports: number;\n        lastImportDate?: Date;\n        totalEntriesImported: number;\n    }> {\n        // This would require an import history table in a real implementation\n        // For now, we'll return basic stats from changelog entries\n\n        const changelog = await db.changelog.findFirst({\n            where: { projectId },\n            include: {\n                entries: {\n                    select: {\n                        createdAt: true\n                    },\n                    orderBy: {\n                        createdAt: 'desc'\n                    }\n                }\n            }\n        });\n\n        if (!changelog) {\n            return {\n                totalImports: 0,\n                totalEntriesImported: 0\n            };\n        }\n\n        return {\n            totalImports: 1, // Would be tracked in import history table\n            lastImportDate: changelog.entries[0]?.createdAt,\n            totalEntriesImported: changelog.entries.length\n        };\n    }\n\n    /**\n     * Validate that user has permission to import\n     */\n    static async validateImportPermissions(\n        userId: string,\n        projectId: string\n    ): Promise<{ canImport: boolean; reason?: string }> {\n        try {\n            // Check if user has access to the project\n            const project = await db.project.findFirst({\n                where: {\n                    id: projectId\n                }\n            });\n\n            if (!project) {\n                return {\n                    canImport: false,\n                    reason: 'Project not found'\n                };\n            }\n\n            // Check if user is admin (can access any project)\n            const user = await db.user.findUnique({\n                where: { id: userId },\n                select: { role: true }\n            });\n\n            if (!user) {\n                return {\n                    canImport: false,\n                    reason: 'User not found'\n                };\n            }\n\n            // Admin can import to any project\n            if (user.role === 'ADMIN') {\n                return { canImport: true };\n            }\n\n            // For now, allow any authenticated user to import\n            // This can be extended with project-specific permissions later\n            return { canImport: true };\n\n        } catch (error) {\n            console.error('Error validating import permissions:', error);\n            return {\n                canImport: false,\n                reason: 'Failed to validate permissions'\n            };\n        }\n    }\n}"
  },
  {
    "path": "lib/services/projects/importing/validation.service.ts",
    "content": "// lib/services/projects/importing/validation.service.ts\n\nimport {\n    ParsedChangelogEntry,\n    ValidatedEntry,\n    ValidationError,\n    ImportPreview,\n    ImportOptions\n} from '@/lib/types/projects/importing';\n\nexport class ImportValidationService {\n    private static readonly MAX_TITLE_LENGTH = 200;\n    private static readonly MAX_CONTENT_LENGTH = 50000;\n    private static readonly VERSION_REGEX = /^v?\\d+\\.\\d+\\.\\d+(?:-[a-zA-Z0-9.-]+)?$/;\n\n    /**\n     * Validate a single changelog entry\n     */\n    static validateEntry(entry: ParsedChangelogEntry): ValidatedEntry {\n        const errors: ValidationError[] = [];\n        const warnings: ValidationError[] = [];\n        const suggestedFixes: Record<string, unknown> = {};\n\n        // Validate title\n        if (!entry.title || !entry.title.trim()) {\n            errors.push({\n                type: 'missing_title',\n                message: 'Entry title is required',\n                field: 'title',\n                severity: 'error'\n            });\n        } else if (entry.title.length > this.MAX_TITLE_LENGTH) {\n            warnings.push({\n                type: 'content_too_long',\n                message: `Title is too long (${entry.title.length} chars, max ${this.MAX_TITLE_LENGTH})`,\n                field: 'title',\n                value: entry.title,\n                severity: 'warning'\n            });\n            suggestedFixes.title = entry.title.substring(0, this.MAX_TITLE_LENGTH - 3) + '...';\n        }\n\n        // Validate content\n        if (!entry.content || !entry.content.trim()) {\n            warnings.push({\n                type: 'missing_content',\n                message: 'Entry content is empty',\n                field: 'content',\n                severity: 'warning'\n            });\n            suggestedFixes.content = entry.title; // Use title as content if missing\n        } else if (entry.content.length > this.MAX_CONTENT_LENGTH) {\n            errors.push({\n                type: 'content_too_long',\n                message: `Content is too long (${entry.content.length} chars, max ${this.MAX_CONTENT_LENGTH})`,\n                field: 'content',\n                value: `${entry.content.length} characters`,\n                severity: 'error'\n            });\n        }\n\n        // Validate version format\n        if (entry.version && !this.VERSION_REGEX.test(entry.version)) {\n            warnings.push({\n                type: 'invalid_version',\n                message: `Version format may be invalid: \"${entry.version}\"`,\n                field: 'version',\n                value: entry.version,\n                severity: 'warning'\n            });\n\n            // Suggest a fix\n            const sanitized = this.sanitizeVersion(entry.version);\n            if (sanitized) {\n                suggestedFixes.version = sanitized;\n            }\n        }\n\n        // Validate date\n        if (entry.publishedAt && isNaN(entry.publishedAt.getTime())) {\n            errors.push({\n                type: 'invalid_date',\n                message: 'Published date is invalid',\n                field: 'publishedAt',\n                severity: 'error'\n            });\n        }\n\n        const isValid = errors.length === 0;\n\n        return {\n            ...entry,\n            isValid,\n            errors,\n            warnings,\n            suggestedFixes\n        };\n    }\n\n    /**\n     * Validate multiple entries and generate import preview\n     */\n    static validateEntries(entries: ParsedChangelogEntry[]): {\n        validatedEntries: ValidatedEntry[];\n        preview: ImportPreview;\n    } {\n        const validatedEntries = entries.map(entry => this.validateEntry(entry));\n\n        const validEntries = validatedEntries.filter(e => e.isValid);\n        const invalidEntries = validatedEntries.filter(e => !e.isValid);\n\n        // Check for duplicate versions\n        const versions = validatedEntries\n            .map(e => e.version)\n            .filter(Boolean) as string[];\n        const duplicateVersions = versions.filter((version, index) =>\n            versions.indexOf(version) !== index\n        );\n\n        // Count issues\n        const missingTitles = validatedEntries.filter(e =>\n            !e.title || !e.title.trim()\n        ).length;\n\n        const missingContent = validatedEntries.filter(e =>\n            !e.content || !e.content.trim()\n        ).length;\n\n        // Generate suggestions for version and tag mappings\n        const suggestedMappings = this.generateMappingSuggestions(validatedEntries);\n\n        // Collect all warnings and errors\n        const allWarnings = validatedEntries.flatMap(e => e.warnings.map(w => w.message));\n        const allErrors = validatedEntries.flatMap(e => e.errors.map(err => err.message));\n\n        const preview: ImportPreview = {\n            totalEntries: entries.length,\n            validEntries: validEntries.length,\n            invalidEntries: invalidEntries.length,\n            duplicateVersions: [...new Set(duplicateVersions)],\n            missingTitles,\n            missingContent,\n            suggestedMappings,\n            warnings: allWarnings,\n            errors: allErrors\n        };\n\n        return {validatedEntries, preview};\n    }\n\n    /**\n     * Generate mapping suggestions for versions and tags\n     */\n    private static generateMappingSuggestions(entries: ValidatedEntry[]): {\n        versions: Record<string, string>;\n        tags: Record<string, string>;\n    } {\n        const versionMappings: Record<string, string> = {};\n        const tagMappings: Record<string, string> = {};\n\n        // Generate version mappings for invalid versions\n        entries.forEach(entry => {\n            if (entry.version && entry.suggestedFixes.version) {\n                versionMappings[entry.version] = entry.suggestedFixes.version as string;\n            }\n        });\n\n        // Generate tag mappings (normalize common variations)\n        const allTags = entries.flatMap(e => e.tags || []);\n        const uniqueTags = [...new Set(allTags)];\n\n        uniqueTags.forEach(tag => {\n            const normalized = this.normalizeTag(tag);\n            if (normalized !== tag) {\n                tagMappings[tag] = normalized;\n            }\n        });\n\n        return {versions: versionMappings, tags: tagMappings};\n    }\n\n    /**\n     * Sanitize version string to make it semver-compatible\n     */\n    private static sanitizeVersion(version: string): string | null {\n        if (!version) return null;\n\n        // Remove common prefixes\n        const clean = version.replace(/^(version|release|v)\\s*/i, '');\n\n        // Extract semver-like pattern\n        const match = clean.match(/(\\d+)\\.?(\\d+)?\\.?(\\d+)?(?:-(.+))?/);\n        if (!match) return null;\n\n        const [, major, minor = '0', patch = '0', prerelease] = match;\n        let result = `${major}.${minor}.${patch}`;\n\n        if (prerelease) {\n            // Clean up prerelease part\n            const cleanPrerelease = prerelease.replace(/[^\\w.-]/g, '');\n            if (cleanPrerelease) {\n                result += `-${cleanPrerelease}`;\n            }\n        }\n\n        return result;\n    }\n\n    /**\n     * Normalize tag names to common conventions\n     */\n    private static normalizeTag(tag: string): string {\n        const normalized = tag.toLowerCase().trim();\n\n        const tagMappings: Record<string, string> = {\n            'bug': 'fix',\n            'bugfix': 'fix',\n            'bugs': 'fix',\n            'feature': 'feat',\n            'features': 'feat',\n            'enhancement': 'feat',\n            'enhancements': 'feat',\n            'improvement': 'feat',\n            'improvements': 'feat',\n            'documentation': 'docs',\n            'doc': 'docs',\n            'breaking': 'breaking-change',\n            'breaking-changes': 'breaking-change',\n            'performance': 'perf',\n            'optimization': 'perf',\n            'optimizations': 'perf',\n            'security': 'security',\n            'sec': 'security',\n            'maintenance': 'chore',\n            'housekeeping': 'chore',\n            'misc': 'chore',\n            'miscellaneous': 'chore'\n        };\n\n        return tagMappings[normalized] || normalized;\n    }\n\n    /**\n     * Check if entries would create conflicts with existing data\n     */\n    static checkConflicts(\n        entries: ValidatedEntry[],\n        existingVersions: string[]\n    ): {\n        versionConflicts: Array<{ entry: ValidatedEntry; existingVersion: string }>;\n        titleConflicts: Array<{ entry: ValidatedEntry; similarTitle: string }>;\n    } {\n        const versionConflicts: Array<{ entry: ValidatedEntry; existingVersion: string }> = [];\n        const titleConflicts: Array<{ entry: ValidatedEntry; similarTitle: string }> = [];\n\n        entries.forEach(entry => {\n            // Check version conflicts\n            if (entry.version && existingVersions.includes(entry.version)) {\n                versionConflicts.push({\n                    entry,\n                    existingVersion: entry.version\n                });\n            }\n\n            // Note: Title conflict checking would require existing titles\n            // This could be implemented if needed by passing existing entries\n        });\n\n        return {versionConflicts, titleConflicts};\n    }\n\n    /**\n     * Validate import options\n     */\n    static validateImportOptions(options: Partial<ImportOptions>): {\n        isValid: boolean;\n        errors: string[];\n        warnings: string[];\n    } {\n        const errors: string[] = [];\n        const warnings: string[] = [];\n\n        // Validate strategy\n        const validStrategies = ['merge', 'replace', 'append'];\n        if (options.strategy && !validStrategies.includes(options.strategy)) {\n            errors.push(`Invalid strategy: ${options.strategy}. Must be one of: ${validStrategies.join(', ')}`);\n        }\n\n        // Validate conflict resolution\n        const validResolutions = ['skip', 'overwrite', 'prompt'];\n        if (options.conflictResolution && !validResolutions.includes(options.conflictResolution)) {\n            errors.push(`Invalid conflict resolution: ${options.conflictResolution}. Must be one of: ${validResolutions.join(', ')}`);\n        }\n\n        // Validate date handling\n        const validDateHandling = ['preserve', 'current', 'sequence'];\n        if (options.dateHandling && !validDateHandling.includes(options.dateHandling)) {\n            errors.push(`Invalid date handling: ${options.dateHandling}. Must be one of: ${validDateHandling.join(', ')}`);\n        }\n\n        // Check for potentially problematic combinations\n        if (options.strategy === 'replace' && options.preserveExistingEntries) {\n            warnings.push('Replace strategy with preserve existing entries may cause unexpected behavior');\n        }\n\n        return {\n            isValid: errors.length === 0,\n            errors,\n            warnings\n        };\n    }\n}"
  },
  {
    "path": "lib/services/request/changelog-request.ts",
    "content": "import {db} from '@/lib/db';\nimport type {RequestStatus} from '@prisma/client';\nimport {sendNotificationEmail} from \"@/lib/services/email/notification\";\n\n// Types\ninterface ProcessRequestOptions {\n    requestId: string;\n    status: RequestStatus;\n    adminId: string;\n    metadata?: {\n        timestamp: string;\n        processedBy: string;\n    };\n}\n\ntype PrismaTransaction = Omit<typeof db, '$connect' | '$disconnect' | '$on' | '$transaction' | '$use'>;\n\n// Simplified type that works with our database structure\ninterface DatabaseChangelogRequest {\n    id: string;\n    type: string;\n    status: string;\n    staffId: string | null;\n    adminId: string | null;\n    projectId: string;\n    targetId: string | null;\n    createdAt: Date;\n    reviewedAt: Date | null;\n    changelogEntryId: string | null;\n    changelogTagId: string | null;\n    project: {\n        id: string;\n        name: string;\n        createdAt: Date;\n        updatedAt: Date;\n        isPublic: boolean;\n        allowAutoPublish: boolean;\n        requireApproval: boolean;\n        defaultTags: string[];\n        changelog: {\n            id: string;\n            projectId: string;\n            createdAt: Date;\n            updatedAt: Date;\n        } | null;\n    };\n    ChangelogEntry: {\n        id: string;\n        title: string;\n        content: string;\n        version: string | null;\n        publishedAt: Date | null;\n        scheduledAt: Date | null;\n        changelogId: string;\n        createdAt: Date;\n        updatedAt: Date;\n    } | null;\n    ChangelogTag: {\n        id: string;\n        name: string;\n        changelogId: string;\n        createdAt: Date;\n        updatedAt: Date;\n    } | null;\n    staff: {\n        id: string;\n        email: string;\n        name: string | null;\n    } | null;\n    customPublishedAt?: string | null;\n}\n\ninterface RequestContext {\n    tx: PrismaTransaction;\n    request: DatabaseChangelogRequest;\n}\n\n// Base interface for request processors\ninterface RequestProcessor {\n    processRequest(context: RequestContext): Promise<void>;\n}\n\n// Request processors\nclass DeleteProjectProcessor implements RequestProcessor {\n    async processRequest({tx, request}: RequestContext): Promise<void> {\n        if (!request.projectId) {\n            throw new Error('Project ID is required for project deletion');\n        }\n\n        // Delete all related requests first\n        await tx.changelogRequest.deleteMany({\n            where: {projectId: request.projectId}\n        });\n\n        // Find and delete changelog entries if they exist\n        const projectChangelog = await tx.changelog.findUnique({\n            where: {projectId: request.projectId},\n            include: {entries: true}\n        });\n\n        if (projectChangelog) {\n            await tx.changelogEntry.deleteMany({\n                where: {changelogId: projectChangelog.id}\n            });\n\n            // Delete the changelog\n            await tx.changelog.delete({\n                where: {id: projectChangelog.id}\n            });\n        }\n\n        // Finally, delete the project\n        await tx.project.delete({\n            where: {id: request.projectId}\n        });\n    }\n}\n\nclass DeleteTagProcessor implements RequestProcessor {\n    async processRequest({tx, request}: RequestContext): Promise<void> {\n        if (!request.projectId || !request.targetId) {\n            throw new Error('Project ID and target ID are required for tag deletion');\n        }\n\n        const project = await tx.project.findUnique({\n            where: {id: request.projectId},\n            include: {\n                changelog: {\n                    include: {\n                        entries: true\n                    }\n                }\n            }\n        });\n\n        if (!project) {\n            throw new Error('Project not found');\n        }\n\n        // Remove the tag from defaultTags array\n        const updatedTags = project.defaultTags.filter(\n            tag => tag !== request.targetId\n        );\n\n        // Update project with new tags array\n        await tx.project.update({\n            where: {id: request.projectId},\n            data: {\n                defaultTags: updatedTags\n            }\n        });\n\n        // For actual changelog tags (if they exist)\n        if (request.ChangelogTag?.id) {\n            await this.disconnectAndDeleteTag(tx, request.ChangelogTag.id);\n        }\n    }\n\n    private async disconnectAndDeleteTag(tx: PrismaTransaction, tagId: string): Promise<void> {\n        const entriesWithTag = await tx.changelogEntry.findMany({\n            where: {\n                tags: {\n                    some: {id: tagId}\n                }\n            }\n        });\n\n        for (const entry of entriesWithTag) {\n            await tx.changelogEntry.update({\n                where: {id: entry.id},\n                data: {\n                    tags: {\n                        disconnect: {id: tagId}\n                    }\n                }\n            });\n        }\n\n        await tx.changelogTag.delete({\n            where: {id: tagId}\n        });\n    }\n}\n\nclass DeleteEntryProcessor implements RequestProcessor {\n    async processRequest({tx, request}: RequestContext): Promise<void> {\n        if (!request.ChangelogEntry?.id) {\n            throw new Error('Changelog entry ID is required for entry deletion');\n        }\n\n        await tx.changelogEntry.delete({\n            where: {id: request.ChangelogEntry.id}\n        });\n    }\n}\n\nclass AllowPublishProcessor implements RequestProcessor {\n    async processRequest({tx, request}: RequestContext): Promise<void> {\n        if (!request.ChangelogEntry?.id) {\n            throw new Error('Changelog entry ID is required for publishing');\n        }\n\n        // Update the entry's publish status\n        // Use custom publishedAt date if provided, otherwise use current date\n        const publishedAt = request.customPublishedAt\n            ? new Date(request.customPublishedAt)\n            : new Date();\n\n        await tx.changelogEntry.update({\n            where: {id: request.ChangelogEntry.id},\n            data: {\n                publishedAt: publishedAt\n            }\n        });\n    }\n}\n\nclass AllowScheduleProcessor implements RequestProcessor {\n    async processRequest({tx, request}: RequestContext): Promise<void> {\n        if (!request.ChangelogEntry?.id) {\n            throw new Error('Changelog entry ID is required for scheduling');\n        }\n\n        if (!request.targetId) {\n            throw new Error('Scheduled time is required for schedule approval');\n        }\n\n        const scheduledAt = new Date(request.targetId);\n\n        // Validate the scheduled time is in the future\n        if (scheduledAt <= new Date()) {\n            throw new Error('Scheduled time must be in the future');\n        }\n\n        // Check if entry is already published\n        if (request.ChangelogEntry.publishedAt) {\n            throw new Error('Cannot schedule an already published entry');\n        }\n\n        // Update the entry with the scheduled time\n        await tx.changelogEntry.update({\n            where: {id: request.ChangelogEntry.id},\n            data: {\n                scheduledAt: scheduledAt\n            }\n        });\n\n        // Create the scheduled job - import dynamically to avoid circular dependencies\n        try {\n            const {ScheduledJobService} = await import('@/lib/services/jobs/scheduled-job.service');\n            const {ScheduledJobType} = await import('@/lib/services/jobs/scheduled-job.service');\n\n            await ScheduledJobService.createJob({\n                type: ScheduledJobType.PUBLISH_CHANGELOG_ENTRY,\n                entityId: request.ChangelogEntry.id,\n                scheduledAt: scheduledAt,\n            });\n        } catch (error) {\n            console.error('Failed to create scheduled job:', error);\n            // Don't fail the transaction if job creation fails\n            // The entry will still be scheduled, but might need manual intervention\n        }\n    }\n}\n\n// Processor registry and factory\nclass RequestProcessorRegistry {\n    private static processors: Record<string, RequestProcessor> = {\n        'DELETE_PROJECT': new DeleteProjectProcessor(),\n        'DELETE_TAG': new DeleteTagProcessor(),\n        'DELETE_ENTRY': new DeleteEntryProcessor(),\n        'ALLOW_PUBLISH': new AllowPublishProcessor(),\n        'ALLOW_SCHEDULE': new AllowScheduleProcessor()\n    };\n\n    static getProcessor(type: string): RequestProcessor {\n        const processor = this.processors[type];\n        if (!processor) {\n            throw new Error(`No processor found for request type: ${type}`);\n        }\n        return processor;\n    }\n\n    static registerProcessor(type: string, processor: RequestProcessor): void {\n        this.processors[type] = processor;\n    }\n}\n\n// Main service class\nclass ChangelogRequestService {\n    async processRequest(options: ProcessRequestOptions) {\n        const safeOptions = this.normalizeSafeOptions(options);\n\n        const result = await db.$transaction(async (tx) => {\n            const existingRequest = await this.findRequest(tx as PrismaTransaction, safeOptions.requestId);\n            const updatedRequest = await this.updateRequestStatus(tx as PrismaTransaction, existingRequest, safeOptions);\n\n            if (safeOptions.status === 'APPROVED') {\n                await this.processApprovedRequest(tx as PrismaTransaction, existingRequest);\n            }\n\n            await this.createAuditLog(tx as PrismaTransaction, updatedRequest, safeOptions);\n\n            return {\n                success: true,\n                data: updatedRequest,\n                metadata: safeOptions.metadata\n            };\n        });\n\n        // After transaction completes successfully, send notification\n        // Only send if the staff user still exists (not deleted)\n        if (result.data.staffId) {\n            try {\n                // Need to fetch staff with settings included since it's not in the transaction result\n                const staffWithSettings = await db.user.findUnique({\n                    where: {id: result.data.staffId},\n                    include: {settings: true}\n                });\n\n                // Only send notification if user exists and has them enabled (or if no preference is set)\n                if (staffWithSettings && staffWithSettings.settings?.enableNotifications !== false) {\n                    // Fetch admin name for the notification\n                    const admin = result.data.adminId\n                        ? await db.user.findUnique({\n                            where: {id: result.data.adminId},\n                            select: {name: true}\n                        })\n                        : null;\n\n                    const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';\n                    const dashboardUrl = `${appUrl}/dashboard/projects/${result.data.projectId}`;\n\n                    await sendNotificationEmail({\n                        userId: result.data.staffId,\n                        status: safeOptions.status,\n                        request: {\n                            type: result.data.type,\n                            projectName: result.data.project?.name || 'Unknown Project',\n                            entryTitle: result.data.ChangelogEntry?.title,\n                            adminName: admin?.name || 'an administrator'\n                        },\n                        dashboardUrl\n                    });\n                }\n            } catch (emailError) {\n                // Just log email errors, don't fail the request\n                console.error('Failed to send notification email:', emailError);\n            }\n        } else {\n            // Log that notification was skipped due to deleted user\n            console.log(`Skipping notification for request ${options.requestId} - staff user was deleted`);\n        }\n\n        return result;\n    }\n\n    private normalizeSafeOptions(options: ProcessRequestOptions) {\n        return {\n            ...options,\n            metadata: options.metadata || {\n                timestamp: new Date().toISOString(),\n                processedBy: options.adminId\n            }\n        };\n    }\n\n    private async findRequest(tx: PrismaTransaction, requestId: string): Promise<DatabaseChangelogRequest> {\n        const request = await tx.changelogRequest.findUnique({\n            where: {id: requestId},\n            include: {\n                project: {\n                    include: {\n                        changelog: true\n                    }\n                },\n                ChangelogEntry: true,\n                ChangelogTag: true,\n                staff: {\n                    select: {\n                        id: true,\n                        email: true,\n                        name: true\n                    }\n                }\n            }\n        });\n\n        if (!request) {\n            throw new Error('Request not found');\n        }\n\n        // Extract custom publishedAt from metadata if present\n        const metadata = (request as unknown as {metadata?: {customPublishedAt?: string} | null}).metadata;\n        const customPublishedAt = metadata?.customPublishedAt || null;\n\n        return {\n            ...request,\n            customPublishedAt\n        } as DatabaseChangelogRequest;\n    }\n\n    private async updateRequestStatus(\n        tx: PrismaTransaction,\n        request: DatabaseChangelogRequest,\n        options: ProcessRequestOptions\n    ): Promise<DatabaseChangelogRequest> {\n        const updatedRequest = await tx.changelogRequest.update({\n            where: {id: options.requestId},\n            data: {\n                status: options.status,\n                adminId: options.adminId,\n                reviewedAt: new Date(options.metadata?.timestamp ?? Date.now())\n            },\n            include: {\n                staff: {\n                    select: {\n                        id: true,\n                        email: true,\n                        name: true\n                    }\n                },\n                project: {\n                    include: {\n                        changelog: true\n                    }\n                },\n                ChangelogTag: true,\n                ChangelogEntry: true\n            }\n        });\n\n        return updatedRequest as DatabaseChangelogRequest;\n    }\n\n    private async processApprovedRequest(tx: PrismaTransaction, request: DatabaseChangelogRequest) {\n        try {\n            const processor = RequestProcessorRegistry.getProcessor(request.type);\n            await processor.processRequest({tx, request});\n        } catch (error) {\n            console.error('Error processing request:', error);\n            throw new Error(`Failed to process ${request.type}: ${(error as Error).message}`);\n        }\n    }\n\n    private async createAuditLog(\n        tx: PrismaTransaction,\n        request: DatabaseChangelogRequest,\n        options: ProcessRequestOptions\n    ) {\n        await tx.auditLog.create({\n            data: {\n                action: `REQUEST_${options.status}`,\n                userId: options.adminId,\n                targetUserId: request.staffId, // This can now be null, which is fine\n                details: {\n                    requestId: request.id,\n                    status: options.status,\n                    processedAt: options.metadata?.timestamp,\n                    processedBy: options.metadata?.processedBy,\n                    type: request.type,\n                    targetId: request.targetId,\n                    // Include preserved staff info if the user was deleted\n                    staffInfo: request.staff ? {\n                        id: request.staff.id,\n                        name: request.staff.name,\n                        email: request.staff.email\n                    } : {\n                        note: 'Staff user was deleted'\n                    }\n                }\n            }\n        });\n    }\n}\n\n// Export singleton instance\nexport const changelogRequestService = new ChangelogRequestService();\n\n// Also export types and registry for extensibility\nexport type {\n    ProcessRequestOptions,\n    RequestProcessor,\n    RequestContext,\n    DatabaseChangelogRequest\n};\n\nexport {RequestProcessorRegistry};"
  },
  {
    "path": "lib/services/search/service.ts",
    "content": "import {db} from '@/lib/db';\nimport {Role, Prisma} from '@prisma/client';\n\ninterface SearchUser {\n    id: string;\n    email: string;\n    name: string | null;\n    role: Role;\n}\n\ninterface SearchFilters {\n    tags?: string[];\n    dateRange?: {\n        start: Date;\n        end: Date;\n    };\n    hasVersion?: boolean;\n    projectIds?: string[];\n    publishedOnly?: boolean; // restrict to only published\n}\n\ninterface ChangelogEntryResult {\n    id: string;\n    title: string;\n    content: string;\n    version: string | null;\n    publishedAt: Date | null;\n    projectId: string;\n    projectName: string;\n    tags: Array<{\n        id: string;\n        name: string;\n        color: string | null;\n    }>;\n    type: 'entry';\n    url: string;\n    isPublished: boolean; // Add published status to results\n}\n\ninterface TagResult {\n    id: string;\n    name: string;\n    color: string | null;\n    entryCount: number;\n    type: 'tag';\n    url: string;\n}\n\ntype SearchResult = ChangelogEntryResult | TagResult;\n\ninterface SearchResponse {\n    results: SearchResult[];\n    total: number;\n    executionTime: number;\n}\n\n// Custom type to handle the changelog project filter properly\ninterface CustomChangelogWhereInput {\n    project?: Prisma.ProjectWhereInput;\n    projectId?: string | Prisma.StringFilter<\"Changelog\">;\n    id?: string | Prisma.StringFilter<\"Changelog\">;\n    createdAt?: Date | Prisma.DateTimeFilter<\"Changelog\">;\n    updatedAt?: Date | Prisma.DateTimeFilter<\"Changelog\">;\n    AND?: Prisma.ChangelogWhereInput | Prisma.ChangelogWhereInput[];\n    OR?: Prisma.ChangelogWhereInput[];\n    NOT?: Prisma.ChangelogWhereInput | Prisma.ChangelogWhereInput[];\n    entries?: Prisma.ChangelogEntryListRelationFilter;\n}\n\ninterface CustomChangelogEntryWhereInput extends Omit<Prisma.ChangelogEntryWhereInput, 'changelog'> {\n    changelog?: CustomChangelogWhereInput;\n}\n\nexport class ChangelogSearchService {\n    async searchAll(\n        user: SearchUser,\n        query: string,\n        filters?: SearchFilters,\n        limit: number = 20,\n        offset: number = 0\n    ): Promise<SearchResponse> {\n        const startTime = performance.now();\n\n        if (!query?.trim() || query.trim().length < 2) {\n            return {\n                results: [],\n                total: 0,\n                executionTime: Math.round(performance.now() - startTime)\n            };\n        }\n\n        try {\n            const [entryResults, tagResults] = await Promise.all([\n                this.searchEntries(user, query, filters, Math.min(limit, 15), offset),\n                this.searchTags(user, query, Math.min(10, limit))\n            ]);\n\n            const allResults = [...entryResults, ...tagResults];\n\n            return {\n                results: allResults,\n                total: allResults.length,\n                executionTime: Math.round(performance.now() - startTime)\n            };\n        } catch (error) {\n            console.error('Search error:', error);\n            return {\n                results: [],\n                total: 0,\n                executionTime: Math.round(performance.now() - startTime)\n            };\n        }\n    }\n\n    private async searchEntries(\n        user: SearchUser,\n        query: string,\n        filters?: SearchFilters,\n        limit: number = 15,\n        offset: number = 0\n    ): Promise<ChangelogEntryResult[]> {\n        const searchQuery = this.buildSearchQuery(query);\n        const whereConditions = this.buildEntryWhereConditions(user, filters);\n\n        // Enhanced search conditions - now searches across title, content, and version\n        const searchConditions = {\n            ...whereConditions,\n            OR: [\n                {title: {search: searchQuery}},\n                {content: {search: searchQuery}},\n                {version: {search: searchQuery}},\n                // Also search for contains (case-insensitive) for better partial matching\n                {title: {contains: query.trim(), mode: 'insensitive' as const}},\n                {content: {contains: query.trim(), mode: 'insensitive' as const}},\n                {version: {contains: query.trim(), mode: 'insensitive' as const}}\n            ]\n        } as Prisma.ChangelogEntryWhereInput;\n\n        const entries = await db.changelogEntry.findMany({\n            where: searchConditions,\n            include: {\n                tags: {\n                    select: {\n                        id: true,\n                        name: true,\n                        color: true\n                    }\n                },\n                changelog: {\n                    include: {\n                        project: {\n                            select: {\n                                id: true,\n                                name: true\n                            }\n                        }\n                    }\n                }\n            },\n            orderBy: [\n                // Prioritize published entries, then by date\n                {publishedAt: {sort: 'desc', nulls: 'last'}},\n                {createdAt: 'desc'}\n            ],\n            take: limit,\n            skip: offset\n        });\n\n        return entries.map(entry => ({\n            id: entry.id,\n            title: entry.title,\n            content: this.truncateContent(entry.content, 150),\n            version: entry.version,\n            publishedAt: entry.publishedAt,\n            projectId: entry.changelog.project.id,\n            projectName: entry.changelog.project.name,\n            tags: entry.tags,\n            type: 'entry' as const,\n            url: `/dashboard/projects/${entry.changelog.project.id}/changelog/${entry.id}`,\n            isPublished: entry.publishedAt !== null\n        }));\n    }\n\n    private async searchTags(\n        user: SearchUser,\n        query: string,\n        limit: number = 10\n    ): Promise<TagResult[]> {\n        const searchQuery = this.buildSearchQuery(query);\n\n        // Enhanced tag search with both full-text and contains matching\n        const tags = await db.changelogTag.findMany({\n            where: {\n                OR: [\n                    {name: {search: searchQuery}},\n                    {name: {contains: query.trim(), mode: 'insensitive'}}\n                ]\n            },\n            include: {\n                entries: {\n                    where: this.buildEntryWhereConditions(user) as Prisma.ChangelogEntryWhereInput,\n                    select: {id: true}\n                }\n            },\n            take: limit\n        });\n\n        return tags\n            .filter(tag => tag.entries.length > 0)\n            .map(tag => ({\n                id: tag.id,\n                name: tag.name,\n                color: tag.color,\n                entryCount: tag.entries.length,\n                type: 'tag' as const,\n                url: `/dashboard/tags/${tag.id}`\n            }));\n    }\n\n    private buildSearchQuery(query: string): string {\n        // Enhanced search query building with better term handling\n        return query\n            .trim()\n            .split(/\\s+/)\n            .filter(term => term.length > 0)\n            .map(term => {\n                // Handle special characters and ensure proper escaping\n                const cleanTerm = term.replace(/[^\\w\\s]/g, '');\n                return cleanTerm.length > 0 ? `${cleanTerm}:*` : '';\n            })\n            .filter(term => term.length > 0)\n            .join(' & ');\n    }\n\n    private buildEntryWhereConditions(user: SearchUser, filters?: SearchFilters): CustomChangelogEntryWhereInput {\n        const baseConditions: CustomChangelogEntryWhereInput = {};\n\n        // Role-based access control\n        switch (user.role) {\n            case Role.ADMIN:\n                // Admin sees everything - no additional restrictions\n                break;\n\n            case Role.STAFF:\n                // Staff sees only public projects for now\n                baseConditions.changelog = {\n                    project: {isPublic: true}\n                };\n                break;\n\n            case Role.VIEWER:\n            default:\n                // Viewers see only public projects ( JIC ltr implementation )\n                baseConditions.changelog = {\n                    project: {isPublic: true}\n                };\n                break;\n        }\n\n        // Apply filters\n        if (!filters) {\n            return baseConditions;\n        }\n\n        const conditions = {...baseConditions};\n\n        // Only filter by published status if explicitly requested\n        if (filters.publishedOnly === true) {\n            conditions.publishedAt = {not: null};\n        }\n\n        if (filters.tags?.length) {\n            conditions.tags = {\n                some: {id: {in: filters.tags}}\n            };\n        }\n\n        if (filters.dateRange) {\n            // Search in both publishedAt and createdAt for unpublished entries\n            conditions.OR = [\n                {\n                    publishedAt: {\n                        not: null,\n                        gte: filters.dateRange.start,\n                        lte: filters.dateRange.end\n                    }\n                },\n                {\n                    publishedAt: null,\n                    createdAt: {\n                        gte: filters.dateRange.start,\n                        lte: filters.dateRange.end\n                    }\n                }\n            ];\n        }\n\n        if (filters.hasVersion !== undefined) {\n            conditions.version = filters.hasVersion\n                ? {not: null}\n                : null;\n        }\n\n        if (filters.projectIds?.length) {\n            const existingProject = conditions.changelog?.project || {};\n            conditions.changelog = {\n                ...conditions.changelog,\n                project: {\n                    ...existingProject,\n                    id: {in: filters.projectIds}\n                }\n            };\n        }\n\n        return conditions;\n    }\n\n    private truncateContent(content: string, maxLength: number): string {\n        if (content.length <= maxLength) {\n            return content;\n        }\n\n        const truncated = content.substring(0, maxLength);\n        const lastSpace = truncated.lastIndexOf(' ');\n\n        if (lastSpace > maxLength * 0.8) {\n            return truncated.substring(0, lastSpace) + '...';\n        }\n\n        return truncated + '...';\n    }\n\n    // Helper method to search only published entries if needed\n    async searchPublishedOnly(\n        user: SearchUser,\n        query: string,\n        filters?: Omit<SearchFilters, 'publishedOnly'>,\n        limit: number = 20,\n        offset: number = 0\n    ): Promise<SearchResponse> {\n        return this.searchAll(user, query, {...filters, publishedOnly: true}, limit, offset);\n    }\n}\n\nexport const searchService = new ChangelogSearchService();"
  },
  {
    "path": "lib/services/slack/index.ts",
    "content": "export {postToSlack} from './post-message'\nexport type {SlackMessageOptions} from './post-message'"
  },
  {
    "path": "lib/services/slack/logo.tsx",
    "content": "/**\n * Official Slack Logo\n * Used in accordance with Slack's brand guidelines\n * https://slack.com/brand-guidelines\n */\nexport function SlackLogo({ className }: { className?: string }) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 270 270\"\n      className={className}\n    >\n      <style>{`\n        .slack-red { fill: #E01E5A; }\n        .slack-cyan { fill: #36C5F0; }\n        .slack-green { fill: #2EB67D; }\n        .slack-yellow { fill: #ECB22E; }\n      `}</style>\n      <g>\n        <g>\n          <path\n            className=\"slack-red\"\n            d=\"M99.4,151.2c0,7.1-5.8,12.9-12.9,12.9c-7.1,0-12.9-5.8-12.9-12.9c0-7.1,5.8-12.9,12.9-12.9h12.9V151.2z\"\n          />\n          <path\n            className=\"slack-red\"\n            d=\"M105.9,151.2c0-7.1,5.8-12.9,12.9-12.9s12.9,5.8,12.9,12.9v32.3c0,7.1-5.8,12.9-12.9,12.9s-12.9-5.8-12.9-12.9V151.2z\"\n          />\n        </g>\n        <g>\n          <path\n            className=\"slack-cyan\"\n            d=\"M118.8,99.4c-7.1,0-12.9-5.8-12.9-12.9c0-7.1,5.8-12.9,12.9-12.9s12.9,5.8,12.9,12.9v12.9H118.8z\"\n          />\n          <path\n            className=\"slack-cyan\"\n            d=\"M118.8,105.9c7.1,0,12.9,5.8,12.9,12.9s-5.8,12.9-12.9,12.9H86.5c-7.1,0-12.9-5.8-12.9-12.9s5.8-12.9,12.9-12.9H118.8z\"\n          />\n        </g>\n        <g>\n          <path\n            className=\"slack-green\"\n            d=\"M170.6,118.8c0-7.1,5.8-12.9,12.9-12.9c7.1,0,12.9,5.8,12.9,12.9s-5.8,12.9-12.9,12.9h-12.9V118.8z\"\n          />\n          <path\n            className=\"slack-green\"\n            d=\"M164.1,118.8c0,7.1-5.8,12.9-12.9,12.9c-7.1,0-12.9-5.8-12.9-12.9V86.5c0-7.1,5.8-12.9,12.9-12.9c7.1,0,12.9,5.8,12.9,12.9V118.8z\"\n          />\n        </g>\n        <g>\n          <path\n            className=\"slack-yellow\"\n            d=\"M151.2,170.6c7.1,0,12.9,5.8,12.9,12.9c0,7.1-5.8,12.9-12.9,12.9c-7.1,0-12.9-5.8-12.9-12.9v-12.9H151.2z\"\n          />\n          <path\n            className=\"slack-yellow\"\n            d=\"M151.2,164.1c-7.1,0-12.9-5.8-12.9-12.9c0-7.1,5.8-12.9,12.9-12.9h32.3c7.1,0,12.9,5.8,12.9,12.9c0,7.1-5.8,12.9-12.9,12.9H151.2z\"\n          />\n        </g>\n      </g>\n    </svg>\n  )\n}"
  },
  {
    "path": "lib/services/slack/post-message.ts",
    "content": "import {db} from '@/lib/db'\nimport {decryptToken} from '@/lib/utils/encryption'\nimport {slackifyMarkdown} from 'slackify-markdown'\nimport {truncateMarkdown} from '@/lib/utils/text'\n\nexport interface SlackMessageOptions {\n    projectId: string\n    channelId: string\n    entryId?: string\n    title: string\n    description?: string\n    url?: string\n    color?: string\n    version?: string\n    tags?: Array<{name: string}>\n}\n\n/**\n * Post a message to Slack for a changelog entry\n * Uses the project's Slack integration to send a formatted message\n */\nexport async function postToSlack(options: SlackMessageOptions) {\n    const {projectId, channelId, entryId, title, description, url, color = '#0099ff'} = options\n    let {version, tags} = options\n\n    try {\n        // If entryId is provided but version/tags aren't, fetch them from the database\n        if (entryId && (!version || !tags)) {\n            const entry = await db.changelogEntry.findUnique({\n                where: {id: entryId},\n                select: {\n                    version: true,\n                    tags: {\n                        select: {name: true},\n                    },\n                },\n            })\n            if (entry) {\n                version = entry.version || version\n                tags = entry.tags || tags\n            }\n        }\n\n        // Get the Slack integration for this project\n        const integration = await db.slackIntegration.findUnique({\n            where: {projectId},\n            select: {\n                accessToken: true,\n                enabled: true,\n            },\n        })\n\n        if (!integration || !integration.enabled || !integration.accessToken) {\n            throw new Error('Slack integration not enabled for this project')\n        }\n\n        // Decrypt the access token\n        let decryptedToken: string\n        try {\n            decryptedToken = decryptToken(integration.accessToken)\n        } catch (error) {\n            console.error('Failed to decrypt Slack access token:', error)\n            throw new Error('Failed to decrypt Slack access token')\n        }\n\n        // First, ensure the bot is in the channel by attempting to join\n        try {\n            const joinResponse = await fetch('https://slack.com/api/conversations.join', {\n                method: 'POST',\n                headers: {\n                    'Authorization': `Bearer ${decryptedToken}`,\n                    'Content-Type': 'application/x-www-form-urlencoded',\n                },\n                body: new URLSearchParams({\n                    channel: channelId,\n                }).toString(),\n            })\n\n            if (joinResponse.ok) {\n                const joinData = await joinResponse.json()\n                if (!joinData.ok) {\n                    console.warn('Failed to join Slack channel:', joinData.error)\n                }\n            }\n        } catch (joinError) {\n            console.error('Failed to join Slack channel:', joinError)\n            // Continue anyway - the channel might already be joined\n        }\n\n        // Add a small delay to ensure the join is processed before posting\n        await new Promise(resolve => setTimeout(resolve, 500))\n\n        // Truncate markdown to reasonable length, then convert to Slack mrkdwn format\n        const truncatedDescription = description ? truncateMarkdown(description) : undefined\n        let mrkdwnDescription = truncatedDescription ? slackifyMarkdown(truncatedDescription) : 'New changelog update'\n\n        // Add a note if content was truncated\n        if (truncatedDescription && description && truncatedDescription !== description) {\n            mrkdwnDescription += '\\n\\n_This is an excerpt. Click \"View Update\" to read the full changelog._'\n        }\n\n        // Build version and tags info\n        let versionAndTagsInfo = ''\n        if (version) {\n            versionAndTagsInfo += `*Version:* ${version}`\n        }\n        if (tags && tags.length > 0) {\n            const tagNames = tags.map(t => t.name).join(', ')\n            if (versionAndTagsInfo) {\n                versionAndTagsInfo += ` | *Tags:* ${tagNames}`\n            } else {\n                versionAndTagsInfo += `*Tags:* ${tagNames}`\n            }\n        }\n\n        // Build the Slack message block kit format\n        const blocks: any[] = [\n            {\n                type: 'header',\n                text: {\n                    type: 'plain_text',\n                    text: title,\n                    emoji: true,\n                },\n            },\n            {\n                type: 'section',\n                text: {\n                    type: 'mrkdwn',\n                    text: mrkdwnDescription,\n                },\n            },\n        ]\n\n        // Add version and tags section if available\n        if (versionAndTagsInfo) {\n            blocks.push({\n                type: 'section',\n                text: {\n                    type: 'mrkdwn',\n                    text: versionAndTagsInfo,\n                },\n            })\n        }\n\n        if (url) {\n            blocks.push({\n                type: 'actions',\n                elements: [\n                    {\n                        type: 'button',\n                        text: {\n                            type: 'plain_text',\n                            text: 'View Update',\n                            emoji: true,\n                        },\n                        url,\n                        style: 'primary',\n                    },\n                ],\n            })\n        }\n\n        // Send the message to Slack\n        const slackResponse = await fetch('https://slack.com/api/chat.postMessage', {\n            method: 'POST',\n            headers: {\n                'Authorization': `Bearer ${decryptedToken}`,\n                'Content-Type': 'application/json',\n            },\n            body: JSON.stringify({\n                channel: channelId,\n                blocks,\n                attachments: [\n                    {\n                        color,\n                        fallback: title,\n                    },\n                ],\n            }),\n        })\n\n        if (!slackResponse.ok) {\n            throw new Error('Failed to post to Slack API')\n        }\n\n        const slackData = await slackResponse.json()\n\n        if (!slackData.ok) {\n            throw new Error(`Slack error: ${slackData.error}`)\n        }\n\n        // Update the integration's post count and last sync time\n        await db.slackIntegration.update({\n            where: {projectId},\n            data: {\n                postCount: {increment: 1},\n                lastSyncAt: new Date(),\n                lastErrorMessage: null,\n            },\n        })\n\n        // Attempt to have the bot leave the channel after posting\n        try {\n            const leaveResponse = await fetch('https://slack.com/api/conversations.leave', {\n                method: 'POST',\n                headers: {\n                    'Authorization': `Bearer ${decryptedToken}`,\n                    'Content-Type': 'application/x-www-form-urlencoded',\n                },\n                body: new URLSearchParams({\n                    channel: channelId,\n                }).toString(),\n            })\n\n            if (leaveResponse.ok) {\n                const leaveData = await leaveResponse.json()\n                if (!leaveData.ok) {\n                    console.warn('Failed to leave Slack channel:', leaveData.error)\n                }\n            }\n        } catch (leaveError) {\n            console.error('Failed to leave Slack channel:', leaveError)\n            // Don't fail if we can't leave - the message was already posted\n        }\n\n        return {success: true, messageTs: slackData.ts}\n    } catch (error) {\n        // Log the error to the integration\n        const errorMessage = error instanceof Error ? error.message : 'Unknown error'\n        console.error('Slack post failed:', errorMessage)\n\n        try {\n            await db.slackIntegration.update({\n                where: {projectId},\n                data: {\n                    lastErrorMessage: errorMessage,\n                    lastSyncAt: new Date(),\n                },\n            })\n        } catch (updateError) {\n            console.error('Failed to update integration error state:', updateError)\n        }\n\n        throw error\n    }\n}"
  },
  {
    "path": "lib/services/sponsor/service.ts",
    "content": "import {db} from '@/lib/db'\nimport {encryptToken, decryptToken} from '@/lib/utils/encryption'\nimport crypto from 'crypto'\n\nconst _ep = [104, 116, 116, 112, 115, 58, 47, 47, 100, 108, 46, 115, 117, 112, 101, 114, 115, 48, 102, 116, 46, 117, 115, 47, 99, 104, 97, 110, 103, 101, 114, 97, 119, 114, 47, 115, 112, 111, 110, 115, 111, 114];\nconst _base = String.fromCharCode(..._ep);\nconst _t = 10000;\nconst _ri = 864e5;\nconst _product = 'changerawr';\n\nconst _pk = `-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxfi0IKoAejLgQ2jsUg5X\nzmLAdHse9nPVs8DPn3dA/bl+KfOpH+XELcxStrPCHIL3RN/yooqF1+sWrWoyvs9X\n1kKiByDfTCX4mPz99LFBIbpv0PAhXs9ViQR4KSZraF/OnGPfJP2gf/FSvAnigA8N\nbiezAhxWqpSbAPkOwX+058XhIRWdfY5qvSHWOrP2AYf7FLA4DQM4ls8omN5hjzCp\nZY7sXWknevl+D9krp0gXnyuB78QQCc7+HXWGz93K1E1l+zcZjqtkQmwUACGLAd+k\nEQ7WGgGB98ZOOcLZu4krQs8Z/7noCLbz+jf/gKzCWLO+frm2yfxEc4vssqBSIeB9\nNwIDAQAB\n-----END PUBLIC KEY-----`;\n\ninterface _VR {\n    valid: boolean;\n    features: string[];\n    proof?: string;\n    payload?: string\n}\n\ninterface _AR {\n    success: boolean;\n    message?: string;\n    features?: string[];\n    proof?: string;\n    payload?: string\n}\n\nfunction _vSig(payload: string, signature: string): boolean {\n    try {\n        const v = crypto.createVerify('SHA256');\n        v.update(payload);\n        return v.verify(_pk, signature, 'base64');\n    } catch {\n        return false;\n    }\n}\n\nfunction _vPayload(raw: string, instanceId: string): { valid: boolean; features: string[] } {\n    try {\n        const d = JSON.parse(raw);\n        if (d.p !== _product) return {valid: false, features: []};\n        if (d.i !== instanceId) return {valid: false, features: []};\n        if (typeof d.v !== 'boolean') return {valid: false, features: []};\n        const age = Math.floor(Date.now() / 1000) - (d.t || 0);\n        if (age > 604800) return {valid: false, features: []};\n        return {valid: d.v === true, features: Array.isArray(d.f) ? d.f : []};\n    } catch {\n        return {valid: false, features: []};\n    }\n}\n\nasync function _fetch(path: string, opts: RequestInit): Promise<Response> {\n    const c = new AbortController();\n    const t = setTimeout(() => c.abort(), _t);\n    try {\n        const r = await fetch(`${_base}${path}`, {...opts, signal: c.signal});\n        clearTimeout(t);\n        return r;\n    } catch (e) {\n        clearTimeout(t);\n        throw e;\n    }\n}\n\nexport class SponsorService {\n\n    static async verifyLicense(licenseKey: string, instanceId: string): Promise<_VR> {\n        try {\n            const r = await _fetch('/api/verify', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({license_key: licenseKey, instance_id: instanceId, product: _product}),\n            });\n            if (!r.ok) return {valid: false, features: []};\n            const d = await r.json();\n            if (typeof d !== 'object' || typeof d.valid !== 'boolean') return {valid: false, features: []};\n            return {\n                valid: d.valid === true,\n                features: Array.isArray(d.features) ? d.features : [],\n                proof: d.proof,\n                payload: d.payload,\n            };\n        } catch {\n            throw new Error('NETWORK_ERROR');\n        }\n    }\n\n    static async activateLicense(licenseKey: string, instanceId: string, instanceName?: string): Promise<_AR> {\n        try {\n            const b: Record<string, string> = {license_key: licenseKey, instance_id: instanceId, product: _product};\n            if (instanceName) b.instance_name = instanceName;\n            const r = await _fetch('/api/activate', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify(b),\n            });\n            if (!r.ok) {\n                const e = await r.json().catch(() => ({}));\n                return {success: false, message: e.error || 'Activation failed'};\n            }\n            return await r.json();\n        } catch {\n            return {\n                success: false,\n                message: 'Unable to reach the licensing server. If you have a firewall, please allow connections to dl.supers0ft.us',\n            };\n        }\n    }\n\n    static async requestChallenge(licenseKey: string, instanceId: string): Promise<{challenge_id: string, challenge_code: string, expires_in: number}> {\n        try {\n            const r = await _fetch('/api/activate/challenge', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({license_key: licenseKey, instance_id: instanceId}),\n            });\n            if (!r.ok) {\n                const e = await r.json().catch(() => ({}));\n                throw new Error(e.error || 'Challenge request failed');\n            }\n            return await r.json();\n        } catch (e) {\n            if (e instanceof Error && e.message !== 'Challenge request failed') {\n                throw new Error('Unable to reach the licensing server. If you have a firewall, please allow connections to dl.supers0ft.us');\n            }\n            throw e;\n        }\n    }\n\n    static async confirmChallenge(challengeId: string, responseCode: string, instanceName?: string): Promise<_AR> {\n        try {\n            const b: Record<string, string> = {challenge_id: challengeId, response_code: responseCode, product: _product};\n            if (instanceName) b.instance_name = instanceName;\n            const r = await _fetch('/api/activate/confirm', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify(b),\n            });\n            if (!r.ok) {\n                const e = await r.json().catch(() => ({}));\n                return {success: false, message: e.error || 'Confirmation failed'};\n            }\n            return await r.json();\n        } catch {\n            return {\n                success: false,\n                message: 'Unable to reach the licensing server. If you have a firewall, please allow connections to dl.supers0ft.us',\n            };\n        }\n    }\n\n    static async deactivateLicense(licenseKey: string, instanceId: string): Promise<void> {\n        try {\n            await _fetch('/api/deactivate', {\n                method: 'POST',\n                headers: {'Content-Type': 'application/json'},\n                body: JSON.stringify({license_key: licenseKey, instance_id: instanceId, product: _product}),\n            });\n        } catch {\n        }\n    }\n\n    static async getLicenseStatus(): Promise<{ active: boolean, features: string[] }> {\n        try {\n            const config = await db.systemConfig.findFirst({\n                select: {\n                    sponsorLicenseValid: true,\n                    sponsorLastVerified: true,\n                    sponsorProof: true,\n                    sponsorPayload: true,\n                    sponsorLicenseKey: true,\n                    telemetryInstanceId: true,\n                },\n            });\n            if (!config) return {active: false, features: []};\n            if (!config.sponsorLicenseValid) return {active: false, features: []};\n            if (!config.sponsorProof || !config.sponsorPayload || !config.sponsorLastVerified) {\n                await SponsorService._rs();\n                return {active: false, features: []};\n            }\n\n            const rawPayload = Buffer.from(config.sponsorPayload, 'base64').toString('utf-8');\n            if (!_vSig(rawPayload, config.sponsorProof)) {\n                await SponsorService._rs();\n                return {active: false, features: []};\n            }\n\n            const instanceId = config.telemetryInstanceId || '';\n            const parsed = _vPayload(rawPayload, instanceId);\n            if (!parsed.valid) {\n                await SponsorService._rs();\n                return {active: false, features: []};\n            }\n\n            const elapsed = Date.now() - new Date(config.sponsorLastVerified).getTime();\n            if (elapsed > _ri) {\n                SponsorService._bgr().catch(() => {\n                });\n            }\n\n            return {active: true, features: parsed.features};\n        } catch {\n            return {active: false, features: []};\n        }\n    }\n\n    static getEffectiveMaxEntries(configMax: number, isLicensed: boolean): number {\n        return isLicensed ? Number.MAX_SAFE_INTEGER : configMax;\n    }\n\n    static async storeLicenseActivation(\n        licenseKey: string, valid: boolean, proof?: string, payload?: string\n    ): Promise<void> {\n        const encrypted = encryptToken(licenseKey);\n        await db.systemConfig.updateMany({\n            data: {\n                sponsorLicenseKey: encrypted,\n                sponsorLicenseValid: valid,\n                sponsorLastVerified: new Date(),\n                sponsorProof: proof || null,\n                sponsorPayload: payload || null,\n            },\n        });\n    }\n\n    static async clearLicenseState(): Promise<void> {\n        await db.systemConfig.updateMany({\n            data: {\n                sponsorLicenseKey: null,\n                sponsorLicenseValid: false,\n                sponsorLastVerified: null,\n                sponsorProof: null,\n                sponsorPayload: null,\n            },\n        });\n    }\n\n    static async getStoredLicenseKey(): Promise<string | null> {\n        const c = await db.systemConfig.findFirst({select: {sponsorLicenseKey: true}});\n        if (!c?.sponsorLicenseKey) return null;\n        try {\n            return decryptToken(c.sponsorLicenseKey);\n        } catch {\n            return null;\n        }\n    }\n\n    static async getInstanceId(): Promise<string | null> {\n        const c = await db.systemConfig.findFirst({select: {telemetryInstanceId: true}});\n        return c?.telemetryInstanceId ?? null;\n    }\n\n    static async checkEntryAllowed(projectId: string): Promise<boolean> {\n        const config = await db.systemConfig.findFirst({\n            select: {\n                maxChangelogEntriesPerProject: true,\n                sponsorLicenseValid: true,\n                sponsorProof: true,\n                sponsorPayload: true,\n                sponsorLastVerified: true,\n                telemetryInstanceId: true,\n            },\n        });\n        if (!config) return true;\n\n        const changelog = await db.changelog.findUnique({\n            where: {projectId},\n            select: {_count: {select: {entries: true}}},\n        });\n        const count = changelog?._count?.entries || 0;\n\n        let ceiling = config.maxChangelogEntriesPerProject;\n        if (config.sponsorLicenseValid && config.sponsorProof && config.sponsorPayload && config.sponsorLastVerified) {\n            const raw = Buffer.from(config.sponsorPayload, 'base64').toString('utf-8');\n            if (_vSig(raw, config.sponsorProof)) {\n                const p = _vPayload(raw, config.telemetryInstanceId || '');\n                if (p.valid) ceiling = Number.MAX_SAFE_INTEGER;\n            }\n        }\n\n        return count < ceiling;\n    }\n\n    static async needsReverification(): Promise<boolean> {\n        const c = await db.systemConfig.findFirst({\n            select: {sponsorLastVerified: true, sponsorLicenseValid: true},\n        });\n        if (!c?.sponsorLicenseValid || !c?.sponsorLastVerified) return false;\n        return Date.now() - new Date(c.sponsorLastVerified).getTime() > _ri;\n    }\n\n    static async _bgr(): Promise<void> {\n        const k = await SponsorService.getStoredLicenseKey();\n        const i = await SponsorService.getInstanceId();\n        if (!k || !i) return;\n        try {\n            const r = await SponsorService.verifyLicense(k, i);\n            await SponsorService.storeLicenseActivation(k, r.valid, r.proof, r.payload);\n        } catch {\n        }\n    }\n\n    private static async _rs(): Promise<void> {\n        await db.systemConfig.updateMany({\n            data: {\n                sponsorLicenseValid: false,\n                sponsorLastVerified: null,\n                sponsorProof: null,\n                sponsorPayload: null,\n            },\n        });\n    }\n}\n"
  },
  {
    "path": "lib/services/telemetry/service.ts",
    "content": "import {db} from '@/lib/db';\nimport {TelemetryState} from '@prisma/client';\nimport {TelemetryConfig, TelemetryData, TelemetryResponse, TelemetryStats} from '@/lib/types/telemetry';\nimport {appInfo, getVersionString} from '@/lib/app-info';\n\nexport class TelemetryService {\n    private static readonly TELEMETRY_URL = 'https://dl.supers0ft.us/changerawr/telemetry';\n    private static readonly REGISTER_URL = 'https://dl.supers0ft.us/changerawr/telemetry/register';\n    private static readonly DEACTIVATE_URL = 'https://dl.supers0ft.us/changerawr/telemetry/deactivate';\n    private static readonly STATS_URL = 'https://dl.supers0ft.us/changerawr/telemetry/stats';\n    private static readonly SEND_TELEMETRY_URL = 'https://dl.supers0ft.us/changerawr/telemetry/send';\n    private static readonly DEFAULT_CONFIG_ID = 1;\n\n    /**\n     * Check if telemetry logging is enabled\n     */\n    private static shouldLog(): boolean {\n        return process.env.SHOW_TELEMETRY_LOGS === 'true';\n    }\n\n    /**\n     * Log message if telemetry logging is enabled\n     */\n    private static log(...args: unknown[]): void {\n        if (this.shouldLog()) {\n            console.log(...args);\n        }\n    }\n\n    /**\n     * Log error (always shown)\n     */\n    private static logError(...args: unknown[]): void {\n        console.error(...args);\n    }\n\n    /**\n     * Log warning (always shown)\n     */\n    private static logWarn(...args: unknown[]): void {\n        console.warn(...args);\n    }\n\n    /**\n     * Get telemetry configuration\n     */\n    static async getTelemetryConfig(): Promise<TelemetryConfig> {\n        const config = await db.systemConfig.findFirst({\n            where: {id: this.DEFAULT_CONFIG_ID}\n        });\n\n        if (!config) {\n            const newConfig = await db.systemConfig.create({\n                data: {\n                    id: this.DEFAULT_CONFIG_ID,\n                    allowTelemetry: TelemetryState.PROMPT,\n                }\n            });\n\n            return {\n                allowTelemetry: this.mapTelemetryState(newConfig.allowTelemetry),\n                instanceId: newConfig.telemetryInstanceId || undefined,\n            };\n        }\n\n        return {\n            allowTelemetry: this.mapTelemetryState(config.allowTelemetry),\n            instanceId: config.telemetryInstanceId || undefined,\n        };\n    }\n\n    /**\n     * Reactivate an existing instance\n     */\n    static async reactivateInstance(instanceId: string): Promise<void> {\n        this.log('Reactivating existing instance:', instanceId);\n\n        const reactivationData: TelemetryData = {\n            instanceId,\n            version: getVersionString(),\n            status: appInfo.status,\n            environment: appInfo.environment,\n            timestamp: new Date().toISOString(),\n        };\n\n        try {\n            await this.sendTelemetry(reactivationData);\n            this.log('Instance reactivated successfully:', instanceId);\n        } catch (error) {\n            this.logError('Failed to reactivate instance:', error);\n            throw error;\n        }\n    }\n\n    /**\n     * Update telemetry configuration\n     */\n    static async updateTelemetryConfig(config: TelemetryConfig): Promise<void> {\n        const currentConfig = await this.getTelemetryConfig();\n\n        await db.systemConfig.upsert({\n            where: { id: this.DEFAULT_CONFIG_ID },\n            create: {\n                id: this.DEFAULT_CONFIG_ID,\n                allowTelemetry: this.mapToDbTelemetryState(config.allowTelemetry),\n                telemetryInstanceId: config.instanceId,\n            },\n            update: {\n                allowTelemetry: this.mapToDbTelemetryState(config.allowTelemetry),\n                telemetryInstanceId: config.instanceId,\n            }\n        });\n\n        // Handle job scheduling and reactivation based on telemetry state\n        if (config.allowTelemetry === 'enabled') {\n            // If we have an instance ID and we're going from disabled to enabled, reactivate\n            if (config.instanceId && currentConfig.allowTelemetry === 'disabled') {\n                this.log('Reactivating previously disabled instance');\n                try {\n                    await this.reactivateInstance(config.instanceId);\n                } catch (error) {\n                    this.logWarn('Failed to reactivate instance, but continuing with scheduling:', error);\n                }\n            }\n\n            await this.scheduleTelemetryJob();\n        } else {\n            await this.cancelTelemetryJobs();\n        }\n    }\n\n    /**\n     * Register instance with telemetry server and send initial telemetry\n     */\n    static async registerInstance(): Promise<string> {\n        const registrationData = {\n            version: getVersionString(),\n            status: appInfo.status,\n            environment: appInfo.environment,\n            timestamp: new Date().toISOString(),\n        };\n\n        this.log('Registering new instance with telemetry server...');\n        this.log('Registration data:', registrationData);\n\n        try {\n            const controller = new AbortController();\n            const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout\n\n            const response = await fetch(this.REGISTER_URL, {\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json',\n                    'Accept': 'application/json',\n                    'User-Agent': `Changerawr/${getVersionString()}`,\n                },\n                body: JSON.stringify(registrationData),\n                signal: controller.signal,\n            });\n\n            clearTimeout(timeoutId);\n\n            this.log('Registration response status:', response.status);\n\n            if (!response.ok) {\n                const errorText = await response.text();\n                this.logError('Registration HTTP error response:', errorText);\n                throw new Error(`Registration HTTP ${response.status}: ${errorText}`);\n            }\n\n            const responseText = await response.text();\n            this.log('Registration raw response text:', responseText);\n\n            let parsedResponse: TelemetryResponse;\n            try {\n                parsedResponse = JSON.parse(responseText);\n            } catch (parseError) {\n                this.logError('Registration JSON parse error:', parseError);\n                throw new Error(`Invalid JSON response: ${responseText.substring(0, 200)}...`);\n            }\n\n            if (!parsedResponse.success || !parsedResponse.instanceId) {\n                throw new Error(`Registration failed: ${parsedResponse || 'No instance ID returned'}`);\n            }\n\n            const instanceId = parsedResponse.instanceId;\n            this.log('Instance registered successfully:', instanceId);\n\n            // Send initial telemetry data immediately after registration\n            this.log('Sending initial telemetry data...');\n            try {\n                const initialTelemetryData: TelemetryData = {\n                    instanceId,\n                    version: getVersionString(),\n                    status: appInfo.status,\n                    environment: appInfo.environment,\n                    timestamp: new Date().toISOString(),\n                };\n\n                await this.sendTelemetry(initialTelemetryData);\n                this.log('Initial telemetry sent successfully');\n            } catch (telemetryError) {\n                this.logWarn('Failed to send initial telemetry (registration still successful):', telemetryError);\n                // Don't throw here - registration was successful, telemetry can be retried later\n            }\n\n            return instanceId;\n\n        } catch (error) {\n            if (error instanceof Error) {\n                if (error.name === 'AbortError') {\n                    this.logError('Registration request timed out');\n                    throw new Error('Registration request timed out after 30 seconds');\n                }\n                this.logError('Registration error:', error.message);\n                throw error;\n            }\n            this.logError('Unknown registration error:', error);\n            throw new Error('Unknown error occurred during registration');\n        }\n    }\n\n    /**\n     * Send telemetry data to server\n     */\n    static async sendTelemetry(data: TelemetryData): Promise<TelemetryResponse> {\n        this.log('Sending telemetry to:', this.SEND_TELEMETRY_URL);\n        this.log('Payload:', JSON.stringify(data, null, 2));\n\n        try {\n            const controller = new AbortController();\n            const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout\n\n            const response = await fetch(this.SEND_TELEMETRY_URL, {\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json',\n                    'Accept': 'application/json',\n                    'User-Agent': `Changerawr/${getVersionString()}`,\n                },\n                body: JSON.stringify(data),\n                signal: controller.signal,\n            });\n\n            clearTimeout(timeoutId);\n\n            this.log('Response status:', response.status);\n            this.log('Response headers:', Object.fromEntries(response.headers.entries()));\n\n            if (!response.ok) {\n                const errorText = await response.text();\n                this.logError('HTTP error response:', errorText);\n                throw new Error(`HTTP ${response.status}: ${errorText}`);\n            }\n\n            const contentType = response.headers.get('content-type');\n            if (!contentType || !contentType.includes('application/json')) {\n                const responseText = await response.text();\n                this.logError('Non-JSON response received:', responseText);\n                throw new Error(`Expected JSON response, got: ${contentType}. Response: ${responseText}`);\n            }\n\n            const responseText = await response.text();\n            this.log('Raw response text:', responseText);\n\n            if (!responseText || responseText.trim() === '') {\n                throw new Error('Empty response from telemetry server');\n            }\n\n            let parsedResponse: TelemetryResponse;\n            try {\n                parsedResponse = JSON.parse(responseText);\n            } catch (parseError) {\n                this.logError('JSON parse error:', parseError);\n                this.logError('Response text was:', responseText);\n                throw new Error(`Invalid JSON response: ${responseText.substring(0, 200)}...`);\n            }\n\n            this.log('Parsed response:', parsedResponse);\n\n            if (!parsedResponse.success) {\n                throw new Error(`Server error: ${parsedResponse || 'Unknown error'}`);\n            }\n\n            return parsedResponse;\n\n        } catch (error) {\n            if (error instanceof Error) {\n                if (error.name === 'AbortError') {\n                    this.logError('Telemetry request timed out');\n                    throw new Error('Telemetry request timed out after 30 seconds');\n                }\n                this.logError('Telemetry send error:', error.message);\n                throw error;\n            }\n            this.logError('Unknown telemetry error:', error);\n            throw new Error('Unknown error occurred while sending telemetry');\n        }\n    }\n\n    /**\n     * Send telemetry now (used by job executor)\n     */\n    static async sendTelemetryNow(): Promise<void> {\n        const config = await this.getTelemetryConfig();\n\n        if (config.allowTelemetry !== 'enabled' || !config.instanceId) {\n            this.log('Telemetry not enabled or no instance ID, skipping send');\n            return;\n        }\n\n        const data: TelemetryData = {\n            instanceId: config.instanceId,\n            version: getVersionString(),\n            status: appInfo.status,\n            environment: appInfo.environment,\n            timestamp: new Date().toISOString(),\n        };\n\n        this.log('Sending scheduled telemetry for instance:', config.instanceId);\n        await this.sendTelemetry(data);\n        this.log('Scheduled telemetry sent successfully');\n    }\n\n    /**\n     * Deactivate instance\n     */\n    static async deactivateInstance(instanceId: string): Promise<void> {\n        this.log('Deactivating instance:', instanceId);\n\n        try {\n            const controller = new AbortController();\n            const timeoutId = setTimeout(() => controller.abort(), 15000); // 15 second timeout\n\n            const response = await fetch(this.DEACTIVATE_URL, {\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json',\n                    'Accept': 'application/json',\n                    'User-Agent': `Changerawr/${getVersionString()}`,\n                },\n                body: JSON.stringify({instanceId}),\n                signal: controller.signal,\n            });\n\n            clearTimeout(timeoutId);\n\n            if (response.ok) {\n                const result = await response.json();\n                this.log('Instance deactivated successfully:', result);\n            } else {\n                this.logWarn('Failed to deactivate instance:', response.status, await response.text());\n            }\n        } catch (error) {\n            this.logWarn('Failed to deactivate instance:', error);\n            // Don't throw - deactivation failures shouldn't break the app\n        }\n    }\n\n    /**\n     * Get telemetry statistics\n     */\n    static async getTelemetryStats(): Promise<TelemetryStats> {\n        this.log('Fetching telemetry statistics...');\n\n        try {\n            const response = await fetch(this.STATS_URL, {\n                method: 'GET',\n                headers: {\n                    'Accept': 'application/json',\n                    'User-Agent': `Changerawr/${getVersionString()}`,\n                },\n            });\n\n            if (!response.ok) {\n                throw new Error(`HTTP ${response.status}: ${await response.text()}`);\n            }\n\n            const stats = await response.json();\n            this.log('Retrieved telemetry stats:', stats);\n            return stats;\n        } catch (error) {\n            this.logError('Failed to fetch telemetry stats:', error);\n            throw error;\n        }\n    }\n\n    /**\n     * Schedule telemetry job (every hour)\n     */\n    private static async scheduleTelemetryJob(): Promise<void> {\n        // Dynamically import to avoid circular dependencies\n        const {ScheduledJobService, ScheduledJobType} = await import('@/lib/services/jobs/scheduled-job.service');\n\n        // Cancel existing jobs first\n        await this.cancelTelemetryJobs();\n\n        // Schedule next telemetry send\n        const nextRun = new Date(Date.now() + 60 * 60 * 1000); // 1 hour from now\n\n        const jobId = await ScheduledJobService.createJob({\n            type: ScheduledJobType.TELEMETRY_SEND,\n            entityId: 'telemetry-system', // Use a string that won't conflict with foreign keys\n            scheduledAt: nextRun,\n            maxRetries: 3,\n        });\n\n        this.log(`Scheduled telemetry job ${jobId} for:`, nextRun.toISOString());\n    }\n\n    /**\n     * Cancel all telemetry jobs\n     */\n    private static async cancelTelemetryJobs(): Promise<void> {\n        // Dynamically import to avoid circular dependencies\n        const {ScheduledJobType} = await import('@/lib/services/jobs/scheduled-job.service');\n\n        // Find and cancel pending telemetry jobs\n        const pendingJobs = await db.scheduledJob.findMany({\n            where: {\n                type: ScheduledJobType.TELEMETRY_SEND,\n                entityId: 'telemetry-system', // Match the same entityId we use for creation\n                status: 'PENDING'\n            }\n        });\n\n        for (const job of pendingJobs) {\n            await db.scheduledJob.update({\n                where: {id: job.id},\n                data: {status: 'CANCELLED'}\n            });\n        }\n\n        this.log(`Cancelled ${pendingJobs.length} pending telemetry jobs`);\n    }\n\n    /**\n     * Initialize telemetry (call on app startup)\n     */\n    static async initialize(): Promise<void> {\n        this.log('Initializing telemetry service...');\n\n        try {\n            const config = await this.getTelemetryConfig();\n            this.log('Current telemetry config:', config);\n\n            if (config.allowTelemetry === 'enabled') {\n                if (config.instanceId) {\n                    this.log('Telemetry enabled for instance:', config.instanceId);\n                    await this.scheduleTelemetryJob();\n                } else {\n                    this.log('Telemetry enabled but no instance ID - will prompt for registration');\n                }\n            } else {\n                this.log('Telemetry disabled or in prompt mode');\n            }\n        } catch (error) {\n            this.logError('Failed to initialize telemetry:', error);\n            // Don't throw - telemetry failures shouldn't break app startup\n        }\n    }\n\n    /**\n     * Handle app shutdown\n     */\n    static async shutdown(): Promise<void> {\n        this.log('Shutting down telemetry service...');\n\n        try {\n            const config = await this.getTelemetryConfig();\n\n            if (config.allowTelemetry === 'enabled' && config.instanceId) {\n                this.log('Deactivating instance on shutdown:', config.instanceId);\n                await this.deactivateInstance(config.instanceId);\n            }\n        } catch (error) {\n            this.logError('Error during telemetry shutdown:', error);\n            // Don't throw - shutdown errors shouldn't prevent app termination\n        }\n    }\n\n    /**\n     * Test telemetry connection (for debugging)\n     */\n    static async testConnection(): Promise<void> {\n        this.log('Testing telemetry connection...');\n\n        try {\n            // First test if the server is responding\n            const healthResponse = await fetch(this.TELEMETRY_URL, {\n                method: 'GET',\n                headers: {\n                    'Accept': 'application/json',\n                    'User-Agent': `Changerawr/${getVersionString()}`,\n                },\n            });\n\n            this.log('Health check response:', healthResponse.status);\n\n            if (healthResponse.ok) {\n                const healthData = await healthResponse.json();\n                this.log('Server health data:', healthData);\n            }\n\n            // Then test actual telemetry submission\n            const testData: TelemetryData = {\n                instanceId: 'test-' + Date.now(),\n                version: getVersionString(),\n                status: appInfo.status,\n                environment: 'test',\n                timestamp: new Date().toISOString(),\n            };\n\n            this.log('Testing telemetry submission...');\n            const result = await this.sendTelemetry(testData);\n            this.log('Test submission result:', result);\n\n            // Test deactivation\n            if (result.instanceId) {\n                this.log('Testing instance deactivation...');\n                await this.deactivateInstance(result.instanceId);\n            }\n\n            this.log('Telemetry connection test completed successfully');\n        } catch (error) {\n            this.logError('Telemetry connection test failed:', error);\n            throw error;\n        }\n    }\n\n    /**\n     * Map Prisma enum to our type\n     */\n    private static mapTelemetryState(state: TelemetryState): TelemetryConfig['allowTelemetry'] {\n        switch (state) {\n            case TelemetryState.PROMPT:\n                return 'prompt';\n            case TelemetryState.ENABLED:\n                return 'enabled';\n            case TelemetryState.DISABLED:\n                return 'disabled';\n            default:\n                return 'prompt';\n        }\n    }\n\n    /**\n     * Map our type to Prisma enum\n     */\n    private static mapToDbTelemetryState(state: TelemetryConfig['allowTelemetry']): TelemetryState {\n        switch (state) {\n            case 'prompt':\n                return TelemetryState.PROMPT;\n            case 'enabled':\n                return TelemetryState.ENABLED;\n            case 'disabled':\n                return TelemetryState.DISABLED;\n            default:\n                return TelemetryState.PROMPT;\n        }\n    }\n}\n"
  },
  {
    "path": "lib/types/analytics.ts",
    "content": "// types/analytics.ts\n\nexport interface AnalyticsView {\n    id: string;\n    projectId: string;\n    changelogEntryId?: string;\n    ipHash: string;\n    country?: string;\n    userAgent?: string;\n    referrer?: string;\n    viewedAt: Date;\n    sessionHash: string;\n}\n\nexport interface AnalyticsTimeRange {\n    start: Date;\n    end: Date;\n}\n\nexport type AnalyticsPeriod = '7d' | '30d' | '90d' | '1y';\n\nexport interface DailyAnalytics {\n    date: string;\n    views: number;\n    uniqueVisitors: number;\n}\n\nexport interface CountryAnalytics {\n    country: string;\n    count: number;\n    percentage?: number;\n}\n\nexport interface EntryAnalytics {\n    entryId: string;\n    title: string;\n    views: number;\n    uniqueVisitors: number;\n    percentage?: number;\n}\n\nexport interface ReferrerAnalytics {\n    referrer: string;\n    count: number;\n    percentage?: number;\n}\n\nexport interface ProjectAnalyticsData {\n    totalViews: number;\n    uniqueVisitors: number;\n    topCountries: CountryAnalytics[];\n    dailyViews: DailyAnalytics[];\n    topEntries: EntryAnalytics[];\n    topReferrers: ReferrerAnalytics[];\n    period: AnalyticsPeriod;\n    timeRange: AnalyticsTimeRange;\n    projectName?: string;\n}\n\nexport interface ProjectAnalyticsSummary {\n    projectId: string;\n    projectName: string;\n    views: number;\n    uniqueVisitors: number;\n    percentage?: number;\n}\n\nexport interface SystemAnalyticsData extends ProjectAnalyticsData {\n    topProjects: ProjectAnalyticsSummary[];\n}\n\nexport interface AnalyticsMetric {\n    label: string;\n    value: number;\n    change?: number; // Percentage change from previous period\n    changeType?: 'increase' | 'decrease' | 'neutral';\n}\n\nexport interface AnalyticsChartData {\n    labels: string[];\n    datasets: Array<{\n        label: string;\n        data: number[];\n        borderColor?: string;\n        backgroundColor?: string;\n        fill?: boolean;\n    }>;\n}\n\nexport interface AnalyticsExportData {\n    projectId?: string;\n    projectName?: string;\n    period: AnalyticsPeriod;\n    timeRange: AnalyticsTimeRange;\n    exportedAt: Date;\n    data: {\n        summary: {\n            totalViews: number;\n            uniqueVisitors: number;\n        };\n        daily: DailyAnalytics[];\n        countries: CountryAnalytics[];\n        entries?: EntryAnalytics[];\n        referrers: ReferrerAnalytics[];\n        projects?: ProjectAnalyticsSummary[];\n    };\n}\n\n// API Response types\nexport interface AnalyticsApiResponse<T = ProjectAnalyticsData> {\n    success: boolean;\n    data?: T;\n    error?: string;\n}\n\nexport interface AnalyticsApiError {\n    success: false;\n    error: string;\n    code?: number;\n}\n\n// Request types\nexport interface AnalyticsQueryParams {\n    period?: AnalyticsPeriod;\n    startDate?: string;\n    endDate?: string;\n}\n\nexport interface TrackingEventData {\n    projectId: string;\n    changelogEntryId?: string;\n    timestamp?: Date;\n    metadata?: Record<string, unknown>;\n}"
  },
  {
    "path": "lib/types/auth.ts",
    "content": "import {Settings} from \"@prisma/client\";\n\nexport enum Role {\n    ADMIN = 'ADMIN',\n    STAFF = 'STAFF',\n    VIEWER = 'VIEWER'\n}\n\nexport interface User {\n    id: string;\n    email: string;\n    name: string | null;\n    role: Role;\n    createdAt: Date;\n    updatedAt: Date;\n    lastLoginAt: Date;\n    settings: Settings;\n}\n\nexport interface LoginCredentials {\n    email: string\n    password: string\n}\n\nexport interface AuthTokens {\n    accessToken: string\n    refreshToken: string\n}\n\nexport interface LoginResponse {\n    user: {\n        id: string\n        email: string\n        name: string | null\n        role: Role\n        lastLoginAt: Date | null\n    }\n    accessToken: string\n    refreshToken: string\n}\n\nexport interface RefreshTokenResponse {\n    accessToken: string\n    user: User\n}\n\n// utils\n// Permission helpers that properly handle undefined/null roles\nexport const hasAdminAccess = (role: Role | null | undefined): boolean =>\n    role === Role.ADMIN;\n\nexport const hasStaffAccess = (role: Role | null | undefined): boolean =>\n    role === Role.ADMIN || role === Role.STAFF;\n\nexport const canManageChangelog = (role: Role | null | undefined): boolean =>\n    hasStaffAccess(role);\n\nexport const canApproveChanges = (role: Role | null | undefined): boolean =>\n    hasAdminAccess(role);\n\n// Type guard to check if a value is a valid Role\nexport const isValidRole = (role: unknown): role is Role => {\n    return typeof role === 'string' && Object.values(Role).includes(role as Role);\n};"
  },
  {
    "path": "lib/types/changelog.ts",
    "content": "export type RequestType = 'DELETE_PROJECT' | 'DELETE_TAG'\nexport type RequestStatus = 'PENDING' | 'APPROVED' | 'REJECTED'\n\nexport interface ChangelogEntry {\n    id: string\n    title: string\n    content: string\n    version?: string | null\n    publishedAt?: Date | null\n    scheduledAt?: Date | null\n    createdAt: Date\n    updatedAt: Date\n    tags: ChangelogTag[]\n}\n\nexport interface ChangelogTag {\n    id: string\n    name: string\n    color?: string | null\n}\n\nexport interface Changelog {\n    id: string\n    projectId: string\n    entries: ChangelogEntry[]\n    createdAt: Date\n    updatedAt: Date\n}\n\nexport interface ChangelogRequest {\n    id: string\n    type: RequestType\n    status: RequestStatus\n    staffId: string\n    adminId?: string | null\n    projectId: string\n    targetId?: string | null\n    createdAt: string\n    reviewedAt?: string | null\n    changelogEntryId?: string | null\n    changelogTagId?: string | null\n    staff: {\n        id: string\n        email: string\n        name: string | null\n    }\n    project: {\n        id: string\n        name: string\n    }\n    ChangelogEntry?: {\n        id: string\n        title: string\n    } | null\n    ChangelogTag?: {\n        id: string\n        name: string\n        color?: string | null\n    } | null\n}\n\nexport interface Project {\n    changelog: {\n        id: string;\n        projectId: string;\n        createdAt: Date;\n        updatedAt: Date;\n    } | null;\n    name: string;\n    id: string;\n    createdAt: Date;\n    updatedAt: Date;\n    isPublic: boolean;\n    allowAutoPublish: boolean;\n    requireApproval: boolean;\n    defaultTags: string[];\n}\n\nexport interface RequestDataType {\n    type: string;\n    status: string;\n    projectId: string;\n    targetId: string | null;\n    id: string;\n    createdAt: Date;\n    staffId: string;\n    adminId: string | null;\n    reviewedAt: Date | null;\n    changelogEntryId: string | null;\n    changelogTagId: string | null;\n    project: {\n        changelog: {\n            id: string;\n            projectId: string;\n            createdAt: Date;\n            updatedAt: Date;\n        } | null;\n        name: string;\n        id: string;\n        createdAt: Date;\n        updatedAt: Date;\n        isPublic: boolean;\n        allowAutoPublish: boolean;\n        requireApproval: boolean;\n        defaultTags: string[];\n    };\n    ChangelogEntry: unknown | null;\n    ChangelogTag: {\n        id: string;\n        name: string;\n        color?: string | null;  // Added color support\n    } | null;\n}\n\n// Color utility types and constants\nexport type TagColorOption = {\n    value: string;\n    label: string;\n    color: string;\n    textColor?: string;\n}\n\nexport const TAG_COLOR_OPTIONS: TagColorOption[] = [\n    {value: 'blue', label: 'Blue', color: '#3b82f6', textColor: '#ffffff'},\n    {value: 'green', label: 'Green', color: '#10b981', textColor: '#ffffff'},\n    {value: 'red', label: 'Red', color: '#ef4444', textColor: '#ffffff'},\n    {value: 'yellow', label: 'Yellow', color: '#f59e0b', textColor: '#000000'},\n    {value: 'purple', label: 'Purple', color: '#8b5cf6', textColor: '#ffffff'},\n    {value: 'pink', label: 'Pink', color: '#ec4899', textColor: '#ffffff'},\n    {value: 'indigo', label: 'Indigo', color: '#6366f1', textColor: '#ffffff'},\n    {value: 'orange', label: 'Orange', color: '#f97316', textColor: '#ffffff'},\n    {value: 'teal', label: 'Teal', color: '#14b8a6', textColor: '#ffffff'},\n    {value: 'cyan', label: 'Cyan', color: '#06b6d4', textColor: '#ffffff'},\n    {value: 'gray', label: 'Gray', color: '#6b7280', textColor: '#ffffff'},\n    {value: 'slate', label: 'Slate', color: '#475569', textColor: '#ffffff'},\n];\n\nexport const DEFAULT_TAG_COLOR = '#6b7280'; // Gray color as default\n\n// Utility function to get color info\nexport function getTagColorInfo(color: string | null | undefined): TagColorOption {\n    if (!color) {\n        return TAG_COLOR_OPTIONS.find(option => option.value === 'gray') || TAG_COLOR_OPTIONS[0];\n    }\n\n    return TAG_COLOR_OPTIONS.find(option => option.color === color || option.value === color) ||\n        {value: 'custom', label: 'Custom', color: color, textColor: '#ffffff'};\n}"
  },
  {
    "path": "lib/types/cli/project-api.ts",
    "content": "// Types for CLI Project API endpoints\n\nexport interface ProjectLinkRequest {\n    repositoryUrl?: string;\n    branch?: string;\n    localPath?: string;\n}\n\nexport interface ProjectLinkResponse {\n    success: boolean;\n    message: string;\n    linkId?: string;\n    linkedAt: string;\n}\n\nexport interface ProjectUnlinkRequest {\n    reason?: string;\n    preserveData?: boolean;\n}\n\nexport interface ProjectUnlinkResponse {\n    success: boolean;\n    message: string;\n    unlinkedAt: string;\n}\n\nexport interface CommitData {\n    hash: string;\n    message: string;\n    author: string;\n    email: string;\n    date: string;\n    files: string[];\n    type?: ConventionalCommitType;\n    scope?: string;\n    breaking?: boolean;\n    body?: string;\n    footer?: string;\n}\n\nexport interface SyncRequest {\n    commits: CommitData[];\n    lastSyncHash?: string;\n    branch: string;\n    repositoryUrl?: string;\n    metadata?: {\n        cliVersion?: string;\n        platform?: string;\n        timestamp: string;\n    };\n}\n\nexport interface SyncResponse {\n    success: boolean;\n    processed: number;\n    skipped: number;\n    errors: string[];\n    warnings: string[];\n    newSyncHash: string;\n    syncedAt: string;\n    nextSyncRecommendedAt?: string;\n}\n\nexport interface SyncStatusResponse {\n    success: boolean;\n    lastSync?: {\n        syncHash: string;\n        syncedAt: string;\n        commitCount: number;\n        branch: string;\n    };\n    pendingCommits: number;\n    totalCommits: number;\n    repositoryInfo: {\n        url?: string;\n        branch: string;\n        lastCommitHash?: string;\n        linkedAt: string;\n    };\n    syncSettings: {\n        autoSync: boolean;\n        lastSyncInterval: number;\n        maxCommitsPerSync: number;\n    };\n}\n\nexport type ConventionalCommitType =\n    | 'feat'\n    | 'fix'\n    | 'docs'\n    | 'style'\n    | 'refactor'\n    | 'perf'\n    | 'test'\n    | 'build'\n    | 'ci'\n    | 'chore'\n    | 'revert';\n\nexport interface ProjectSyncMetadata {\n    id: string;\n    projectId: string;\n    lastSyncHash?: string;\n    lastSyncedAt?: Date;\n    totalCommitsSynced: number;\n    repositoryUrl?: string;\n    branch: string;\n    createdAt: Date;\n    updatedAt: Date;\n}\n\nexport interface SyncedCommit {\n    id: string;\n    projectId: string;\n    commitHash: string;\n    commitMessage: string;\n    commitAuthor: string;\n    commitEmail: string;\n    commitDate: Date;\n    commitFiles: string[];\n    conventionalType?: ConventionalCommitType;\n    conventionalScope?: string;\n    isBreaking: boolean;\n    commitBody?: string;\n    commitFooter?: string;\n    syncedAt: Date;\n    branch: string;\n}\n\n// Error response types\nexport interface ProjectApiError {\n    success: false;\n    error: string;\n    message: string;\n    code?: string;\n    details?: Record<string, unknown>;\n}\n\n// Success response wrapper\nexport interface ProjectApiSuccess<T = unknown> {\n    success: true;\n    data: T;\n    message?: string;\n}"
  },
  {
    "path": "lib/types/custom-domains.ts",
    "content": "export interface CustomDomain {\n    id: string\n    domain: string\n    projectId: string\n    verificationToken: string\n    verified: boolean\n    createdAt: Date\n    verifiedAt: Date | null\n    userId: string | null\n    forceHttps: boolean\n    sslMode: 'NONE' | 'LETS_ENCRYPT' | 'EXTERNAL'\n    dnsInstructions?: DNSInstructions\n    certificates?: DomainCertificate[]\n    browserRules?: DomainBrowserRule[]\n    throttleConfig?: DomainThrottleConfig | null\n}\n\nexport interface DomainCertificate {\n    id: string\n    domainId: string\n    status: 'PENDING_HTTP01' | 'PENDING_DNS01' | 'ISSUED' | 'EXPIRED' | 'FAILED' | 'REVOKED'\n    challengeType: 'HTTP01' | 'DNS01'\n    privateKeyPem: string // Required - encrypted private key\n    certificatePem: string | null\n    fullChainPem: string | null\n    csrPem: string // Required - Certificate Signing Request\n    acmeOrderUrl: string | null\n    challengeToken: string | null\n    challengeKeyAuth: string | null\n    dnsTxtValue: string | null\n    issuedAt: Date | null\n    expiresAt: Date | null\n    lastError: string | null\n    renewalAttempts: number\n    createdAt: Date\n    updatedAt: Date\n}\n\nexport interface DomainBrowserRule {\n    id: string\n    domainId: string\n    userAgentPattern: string\n    ruleType: 'BLOCK' | 'ALLOW'\n    isEnabled: boolean\n    createdAt: Date\n    updatedAt: Date\n}\n\nexport interface DomainThrottleConfig {\n    id: string\n    domainId: string\n    enabled: boolean\n    requestsPerSecond: number\n    burstSize: number\n    createdAt: Date\n    updatedAt: Date\n}\n\nexport interface DNSVerificationResult {\n    cnameValid: boolean\n    txtValid: boolean\n    cnameTarget?: string\n    txtRecord?: string\n    errors?: string[]\n}\n\nexport interface DNSInstructions {\n    cname: {\n        name: string\n        value: string\n        description: string\n    }\n    txt: {\n        name: string\n        value: string\n        description: string\n    }\n}\n\nexport interface AddDomainRequest {\n    domain: string\n    projectId: string\n    userId?: string\n}\n\nexport interface AddDomainResponse {\n    success: boolean\n    domain?: {\n        id: string\n        domain: string\n        projectId: string\n        verificationToken: string\n        dnsInstructions: DNSInstructions\n    }\n    error?: string\n}\n\nexport interface VerifyDomainRequest {\n    domain: string\n}\n\nexport interface VerifyDomainResponse {\n    success: boolean\n    verification?: DNSVerificationResult & {\n        verified: boolean\n    }\n    error?: string\n}\n\nexport interface ListDomainsResponse {\n    success: boolean\n    domains?: CustomDomain[]\n    error?: string\n}\n\nexport interface DeleteDomainResponse {\n    success: boolean\n    error?: string\n}"
  },
  {
    "path": "lib/types/dashboard.ts",
    "content": "export interface ProjectPreview {\n    id: string\n    name: string\n    changelogCount: number\n    lastUpdated: string\n}\n\nexport interface Activity {\n    id: string\n    message: string\n    projectId: string\n    projectName: string\n    timestamp: string\n}\n\nexport interface DashboardStats {\n    totalProjects: number\n    totalChangelogs: number\n    projectPreviews: ProjectPreview[]\n    recentActivity: Activity[]\n}"
  },
  {
    "path": "lib/types/easypanel.ts",
    "content": "export interface EasypanelConfig {\n    projectId: string;\n    serviceId: string;\n    panelUrl: string;\n    apiKey: string;\n}\n\nexport interface EasypanelUpdateImagePayload {\n    json: {\n        projectName: string;\n        serviceName: string;\n        image: string;\n        username?: string;\n        password?: string;\n    };\n}\n\nexport interface EasypanelDeployPayload {\n    json: {\n        projectName: string;\n        serviceName: string;\n        forceRebuild: boolean;\n    };\n}\n\nexport interface EasypanelApiResponse<T = unknown> {\n    result?: {\n        data: T;\n    };\n    error?: {\n        message: string;\n        code: number;\n    };\n}\n\nexport interface UpdateStatus {\n    available: boolean;\n    currentVersion: string;\n    latestVersion: string;\n    canAutoUpdate: boolean;\n    easypanelConfigured: boolean;\n}"
  },
  {
    "path": "lib/types/oauth.ts",
    "content": "export interface OAuthProvider {\n    id: string;\n    name: string;\n    clientId: string;\n    clientSecret: string;\n    authorizationUrl: string;\n    tokenUrl: string;\n    userInfoUrl: string;\n    callbackUrl: string;\n    scopes: string[];\n    enabled: boolean;\n    isDefault?: boolean;\n}\n\nexport type OAuthProviderUpdateData = {\n    name?: string;\n    clientId?: string;\n    callbackUrl?: string;\n    clientSecret?: string;\n    scopes?: string[];\n    enabled?: boolean;\n    isDefault?: boolean;\n    authorizationUrl?: string;\n    tokenUrl?: string;\n    userInfoUrl?: string;\n    allowedEmailDomains?: string[];\n    blockExistingUsers?: boolean;\n    requiredClaims?: any; // Prisma InputJsonValue\n};\n\nexport interface OAuthUserInfo {\n    id: string;\n    email: string;\n    name?: string;\n    picture?: string;\n    [key: string]: unknown;\n}\n\nexport interface OAuthCallbackParams {\n    code: string;\n    state?: string;\n    error?: string;\n}\n\nexport interface OAuthConfig {\n    providers: OAuthProvider[];\n    defaultProviderId?: string;\n}"
  },
  {
    "path": "lib/types/projects/catch-up/types.ts",
    "content": "export interface CatchUpSummary {\n    features: number;\n    fixes: number;\n    other: number;\n}\n\nexport interface CatchUpTag {\n    id: string;\n    name: string;\n    color: string | null;\n}\n\nexport interface CatchUpEntry {\n    id: string;\n    title: string;\n    content: string;\n    version: string | null;\n    publishedAt: Date | null;\n    tags: CatchUpTag[];\n}\n\nexport interface CatchUpResponse {\n    fromDate: string;\n    fromVersion: string | null;\n    toVersion: string | null;\n    totalEntries: number;\n    summary: CatchUpSummary;\n    entries: CatchUpEntry[];\n}\n\nexport interface SinceOption {\n    label: string;\n    value: string;\n    type: 'auto' | 'relative' | 'version' | 'date';\n    description?: string;\n}\n\nexport interface CatchUpFilters {\n    since: string;\n    showEmpty: boolean;\n}\n\n// New AI-specific types\nexport interface AICapabilities {\n    enabled: boolean;\n    model: string | null;\n    features: {\n        summaries: boolean;\n        insights: boolean;\n        recommendations: boolean;\n    };\n}\n\nexport interface UserAIStatus {\n    hasAIEnabled: boolean;\n    lastChecked: Date;\n}"
  },
  {
    "path": "lib/types/projects/importing/canny.ts",
    "content": "export interface CannyUser {\n    id: string;\n    created: string;\n    email: string;\n    isAdmin: boolean;\n    name: string;\n    url: string;\n    userID: string;\n}\n\nexport interface CannyBoard {\n    id: string;\n    created: string;\n    name: string;\n    postCount: number;\n    url: string;\n}\n\nexport interface CannyCategory {\n    id: string;\n    name: string;\n    parentID: string | null;\n    postCount: number;\n    url: string;\n}\n\nexport interface CannyTag {\n    id: string;\n    name: string;\n    postCount: number;\n    url: string;\n}\n\nexport interface CannyPost {\n    id: string;\n    author?: CannyUser;\n    board?: CannyBoard;\n    by?: CannyUser;\n    category?: CannyCategory;\n    commentCount?: number;\n    created: string;\n    clickup?: {\n        linkedTasks: Array<{\n            id: string;\n            linkID: string;\n            name: string;\n            postID: string;\n            status: string;\n            url: string;\n        }>;\n    };\n    details?: string;\n    eta?: string;\n    imageURLs?: string[];\n    jira?: {\n        linkedIssues: Array<{\n            id: string;\n            key: string;\n            url: string;\n        }>;\n    };\n    linear?: {\n        linkedIssueIDs: string[];\n    };\n    owner?: CannyUser;\n    score?: number;\n    status?: string;\n    tags?: CannyTag[];\n    title: string;\n    url: string;\n}\n\nexport interface CannyLabel {\n    id: string;\n    created: string;\n    entryCount: number;\n    name: string;\n    url: string;\n}\n\nexport interface CannyEntry {\n    id: string;\n    created: string;\n    labels?: CannyLabel[];\n    lastSaved: string;\n    markdownDetails?: string;\n    plaintextDetails?: string;\n    posts?: CannyPost[];\n    publishedAt?: string;\n    reactions?: {\n        like?: number;\n    };\n    scheduledFor?: string | null;\n    status: 'published' | 'draft' | 'scheduled';\n    title: string;\n    types?: string[];\n    url: string;\n}\n\nexport interface CannyApiResponse {\n    hasMore: boolean;\n    entries: CannyEntry[];\n}\n\nexport interface CannyImportOptions {\n    apiKey: string;\n    includeLabels: boolean;\n    includePostTags: boolean;\n    statusFilter: 'all' | 'published' | 'draft';\n    maxEntries: number;\n}"
  },
  {
    "path": "lib/types/projects/importing.ts",
    "content": "// lib/types/projects/importing.ts\n\nexport interface ParsedChangelogEntry {\n    title: string;\n    content: string;\n    version?: string;\n    publishedAt?: Date;\n    tags?: string[];\n    metadata?: Record<string, unknown>;\n}\n\nexport interface ChangelogSection {\n    heading: string;\n    level: number; // 1-6 for h1-h6\n    content: string;\n    entries: ParsedChangelogEntry[];\n    rawContent: string;\n}\n\nexport interface ParsedChangelog {\n    sections: ChangelogSection[];\n    entries: ParsedChangelogEntry[];\n    metadata: {\n        totalSections: number;\n        totalEntries: number;\n        hasVersions: boolean;\n        hasDates: boolean;\n        originalFormat: 'keepachangelog' | 'github_releases' | 'simple' | 'custom';\n        parseWarnings: string[];\n    };\n}\n\nexport interface ImportPreview {\n    totalEntries: number;\n    validEntries: number;\n    invalidEntries: number;\n    duplicateVersions: string[];\n    missingTitles: number;\n    missingContent: number;\n    suggestedMappings: {\n        versions: Record<string, string>;\n        tags: Record<string, string>;\n    };\n    warnings: string[];\n    errors: string[];\n}\n\nexport interface ImportOptions {\n    strategy: 'merge' | 'replace' | 'append';\n    preserveExistingEntries: boolean;\n    autoGenerateVersions: boolean;\n    defaultTags: string[];\n    publishImportedEntries: boolean;\n    dateHandling: 'preserve' | 'current' | 'sequence';\n    conflictResolution: 'skip' | 'overwrite' | 'prompt';\n}\n\nexport interface ImportResult {\n    success: boolean;\n    importedCount: number;\n    skippedCount: number;\n    errorCount: number;\n    createdEntries: Array<{\n        id: string;\n        title: string;\n        version?: string;\n    }>;\n    warnings: string[];\n    errors: Array<{\n        entry: ParsedChangelogEntry;\n        error: string;\n    }>;\n    processingTime: number;\n}\n\nexport interface ImportStats {\n    processed: number;\n    imported: number;\n    skipped: number;\n    errors: number;\n    startTime: Date;\n    endTime?: Date;\n}\n\n// Supported import formats\nexport type ImportFormat =\n    | 'keepachangelog'\n    | 'github_releases'\n    | 'simple'\n    | 'custom';\n\nexport interface FormatDetectionResult {\n    format: ImportFormat;\n    confidence: number; // 0-1\n    characteristics: string[];\n    structure: {\n        hasVersionHeaders: boolean;\n        hasDateHeaders: boolean;\n        hasTypeHeaders: boolean;\n        usesListFormat: boolean;\n        usesMarkdownSyntax: boolean;\n    };\n}\n\n// Import validation errors\nexport interface ValidationError {\n    type: 'missing_title' | 'missing_content' | 'invalid_version' | 'duplicate_version' | 'invalid_date' | 'content_too_long';\n    message: string;\n    field?: string;\n    value?: string;\n    severity: 'error' | 'warning';\n}\n\nexport interface ValidatedEntry extends ParsedChangelogEntry {\n    isValid: boolean;\n    errors: ValidationError[];\n    warnings: ValidationError[];\n    suggestedFixes: Record<string, unknown>;\n}"
  },
  {
    "path": "lib/types/saml.ts",
    "content": "export interface SAMLProvider {\n    id: string;\n    name: string;\n    entityId: string;\n    ssoUrl: string;\n    certificate: string;\n    spEntityId?: string | null;\n    nameIdFormat: string;\n    emailAttribute: string;\n    nameAttribute: string;\n    enabled: boolean;\n    isDefault: boolean;\n    createdAt?: string;\n    updatedAt?: string;\n}\n\nexport interface SAMLConnection {\n    id: string;\n    providerId: string;\n    provider: Pick<SAMLProvider, 'id' | 'name' | 'enabled' | 'isDefault'>;\n    userId: string;\n    nameId: string;\n    sessionIndex?: string | null;\n    createdAt: string;\n    updatedAt: string;\n}\n\nexport interface SAMLUserInfo {\n    nameId: string;\n    email: string;\n    name?: string;\n    sessionIndex?: string;\n    rawProfile?: Record<string, unknown>;\n}\n\nexport interface SAMLCallbackParams {\n    samlResponse: string;\n    relayState?: string;\n}\n\nexport type SAMLProviderUpdateData = {\n    name?: string;\n    entityId?: string;\n    ssoUrl?: string;\n    certificate?: string;\n    spEntityId?: string | null;\n    nameIdFormat?: string;\n    emailAttribute?: string;\n    nameAttribute?: string;\n    enabled?: boolean;\n    isDefault?: boolean;\n    allowedEmailDomains?: string[];\n    blockExistingUsers?: boolean;\n    requiredClaims?: any; // Prisma InputJsonValue\n};\n"
  },
  {
    "path": "lib/types/settings.ts",
    "content": "// types/settings.ts\n\nimport { Role } from \"@prisma/client\"\n\nexport interface ProjectSettings {\n    id: string\n    name: string\n    isPublic: boolean\n    allowAutoPublish: boolean\n    requireApproval: boolean\n    defaultTags: string[]\n    updatedAt: string\n}\n\nexport type ProjectSettingsFormData = Omit<ProjectSettings, 'id' | 'updatedAt'>\n\nexport interface ProjectSettingsUpdateRequest extends Partial<ProjectSettingsFormData> {\n    projectId: string\n}\n\nexport interface ProjectSettingsResponse {\n    success: boolean\n    data?: ProjectSettings\n    error?: string\n}\n\n// For role-based access control in settings\nexport interface SettingsPermission {\n    role: Role\n    actions: SettingsAction[]\n}\n\nexport type SettingsAction =\n    | 'view'\n    | 'edit'\n    | 'delete'\n    | 'manage_tags'\n    | 'manage_access'\n    | 'manage_approval'\n\nexport interface SettingsTab {\n    id: TabId\n    label: string\n    icon: string\n    requiredPermission?: SettingsAction\n}\n\nexport type TabId = 'general' | 'access' | 'tags' | 'danger'\n\n// Validation schemas (if using Zod)\nexport const settingsValidationSchemas = {\n    projectName: {\n        min: 2,\n        max: 50,\n        message: 'Project name must be between 2 and 50 characters'\n    },\n    tag: {\n        min: 1,\n        max: 20,\n        message: 'Tag must be between 1 and 20 characters'\n    }\n} as const\n\n// API routes type definitions\nexport interface SettingsAPIRoutes {\n    base: '/api/projects/:projectId/settings'\n    methods: {\n        GET: {\n            response: ProjectSettingsResponse\n        }\n        PATCH: {\n            body: ProjectSettingsUpdateRequest\n            response: ProjectSettingsResponse\n        }\n        DELETE: {\n            response: { success: boolean, error?: string }\n        }\n    }\n}\n\n// Error types\nexport interface SettingsError extends Error {\n    code: SettingsErrorCode\n    field?: keyof ProjectSettings\n}\n\nexport type SettingsErrorCode =\n    | 'INVALID_INPUT'\n    | 'UNAUTHORIZED'\n    | 'NOT_FOUND'\n    | 'SERVER_ERROR'\n    | 'VALIDATION_ERROR'\n\n// Event types\nexport interface SettingsChangeEvent {\n    type: 'settings_updated' | 'settings_deleted'\n    projectId: string\n    changes?: Partial<ProjectSettings>\n    timestamp: string\n    userId: string\n}\n\n// Utility types\nexport type SettingsKey = keyof ProjectSettings\nexport type SettingsValue = ProjectSettings[SettingsKey]\n\nexport interface SettingsAuditLog {\n    id: string\n    projectId: string\n    userId: string\n    action: SettingsAction\n    changes: Partial<ProjectSettings>\n    createdAt: string\n}"
  },
  {
    "path": "lib/types/sso.ts",
    "content": "export interface OAuthProvider {\n    id: string;\n    name: string;\n    enabled: boolean;\n    isDefault: boolean;\n    clientId?: string;\n    authorizationUrl?: string;\n    tokenUrl?: string;\n    userInfoUrl?: string;\n    callbackUrl?: string;\n    scopes?: string[];\n    createdAt?: string;\n    updatedAt?: string;\n}\n\nexport interface OAuthConnection {\n    id: string;\n    providerId: string;\n    provider: OAuthProvider;\n    providerUserId: string;\n    accessToken?: string;\n    refreshToken?: string;\n    expiresAt: string | null;\n    createdAt: string;\n    updatedAt: string;\n}\n\nexport interface UserConnectionsResponse {\n    connections: OAuthConnection[];\n    allProviders: OAuthProvider[];\n}\n\nexport type ConnectionStatus = 'connected' | 'expired' | 'disabled';\n\nexport interface SsoConnectionsData {\n    connections: OAuthConnection[];\n    allProviders: OAuthProvider[];\n}\n\n// For API responses\nexport interface ConnectionApiResponse {\n    success: boolean;\n    data?: UserConnectionsResponse;\n    error?: string;\n}\n\n// Provider status helpers\nexport interface ProviderStatusInfo {\n    status: ConnectionStatus;\n    message: string;\n    actionRequired?: boolean;\n}\n\n// Connection analytics\nexport interface ConnectionAnalytics {\n    totalConnections: number;\n    activeConnections: number;\n    expiredConnections: number;\n    disabledProviderConnections: number;\n    mostRecentConnection?: OAuthConnection;\n    oldestConnection?: OAuthConnection;\n}"
  },
  {
    "path": "lib/types/telemetry.ts",
    "content": "export type TelemetryState = 'prompt' | 'enabled' | 'disabled';\n\nexport interface TelemetryConfig {\n    allowTelemetry: TelemetryState;\n    instanceId?: string;\n}\n\nexport interface TelemetryData {\n    instanceId: string;\n    version: string;\n    status: string;\n    environment: string;\n    timestamp: string;\n}\n\nexport interface TelemetryResponse {\n    success: boolean;\n    instanceId?: string;\n    message?: string;\n}\n\nexport interface TelemetryStats {\n    success: boolean;\n    stats: {\n        totalInstances: number;\n        activeInstances: number;\n        recentInstances: number;\n        lastUpdate: string | null;\n        versions: Array<{\n            version: string;\n            count: number;\n        }>;\n        environments: Array<{\n            environment: string;\n            count: number;\n        }>;\n    };\n}"
  },
  {
    "path": "lib/utils/ai/prompts.ts",
    "content": "/**\n * Template prompts for AI completions in the markdown editor\n * Optimized for predictable, consistent responses\n */\nimport { AICompletionType, AIEditorRequest, AIMessage } from './types';\n\n/**\n * System messages for different completion types\n */\nexport const SYSTEM_MESSAGES: Record<AICompletionType, string> = {\n    [AICompletionType.COMPLETE]:\n        \"You are a helpful writing assistant. Complete the text in a natural way that maintains the style, tone, and context of the original content. Only generate the completion text. Do not repeat any of the original content.\",\n\n    [AICompletionType.EXPAND]:\n        \"You are a helpful writing assistant. Expand on the given content by adding more details, examples, explanations, or ideas. Maintain the original style and tone. Only generate the expanded text, leaving the original intact.\",\n\n    [AICompletionType.IMPROVE]:\n        \"You are a helpful writing assistant. Improve the given text by enhancing clarity, flow, and style. Make it more engaging and professional while preserving the meaning. Return the complete improved version of the text.\",\n\n    [AICompletionType.SUMMARIZE]:\n        \"You are a helpful writing assistant. Summarize the given content concisely while preserving key points and main ideas. Return only the summary.\",\n\n    [AICompletionType.REPHRASE]:\n        \"You are a helpful writing assistant. Rephrase the given text to convey the same meaning using different wording and sentence structure. Return only the rephrased version.\",\n\n    [AICompletionType.FIX_GRAMMAR]:\n        \"You are a helpful writing assistant. Fix grammar, spelling, and punctuation errors in the text without changing its meaning or style. Return only the corrected text.\",\n\n    [AICompletionType.CUSTOM]:\n        \"You are a helpful writing assistant. Follow the specific instructions to process the provided content. Only return the processed text as specified, with no explanations, questions, or additional comments.\"\n};\n\n/**\n * Generate a prompt based on the editor request with guardrails\n * for predictable responses\n */\nexport function generatePrompt(request: AIEditorRequest): string {\n    // Guard against empty content\n    const content = request.content?.trim() || \"\";\n    const isEmpty = content.length === 0;\n\n    switch (request.type) {\n        case AICompletionType.COMPLETE:\n            if (isEmpty) return \"Please provide some text to complete.\";\n            return [\n                `Please continue the following text:`,\n                `\"\"\"`,\n                `${content}`,\n                `\"\"\"`,\n                `Continue directly from where it ends. Return only the new content with no additional context, explanations, or preamble.`\n            ].join('\\n');\n\n        case AICompletionType.EXPAND:\n            if (isEmpty) return \"Please provide content to expand upon.\";\n            return [\n                `Please expand on the following text with more details, examples, or explanations:`,\n                `\"\"\"`,\n                `${content}`,\n                `\"\"\"`,\n                `Return the expanded version of the text with no additional context, explanations, or preamble.`\n            ].join('\\n');\n\n        case AICompletionType.IMPROVE:\n            if (isEmpty) return \"Please provide content to improve.\";\n            return [\n                `Please improve the following text by enhancing clarity, flow, and style:`,\n                `\"\"\"`,\n                `${content}`,\n                `\"\"\"`,\n                `Return the improved version of the text with no additional context, explanations, or preamble.`\n            ].join('\\n');\n\n        case AICompletionType.SUMMARIZE:\n            if (isEmpty) return \"Please provide content to summarize.\";\n            return [\n                `Please summarize the following content while preserving the key points:`,\n                `\"\"\"`,\n                `${content}`,\n                `\"\"\"`,\n                `Return only the summary with no additional context, explanations, or preamble.`\n            ].join('\\n');\n\n        case AICompletionType.REPHRASE:\n            if (isEmpty) return \"Please provide content to rephrase.\";\n            return [\n                `Please rephrase the following text while preserving its meaning:`,\n                `\"\"\"`,\n                `${content}`,\n                `\"\"\"`,\n                `Return only the rephrased text with no additional context, explanations, or preamble.`\n            ].join('\\n');\n\n        case AICompletionType.FIX_GRAMMAR:\n            if (isEmpty) return \"Please provide content to fix grammar.\";\n            return [\n                `Please correct any grammar, spelling, and punctuation errors in the following text:`,\n                `\"\"\"`,\n                `${content}`,\n                `\"\"\"`,\n                `Return only the corrected text with no additional context, explanations, or preamble.`\n            ].join('\\n');\n\n        case AICompletionType.CUSTOM:\n            if (!request.customPrompt) {\n                return `Please provide specific instructions for how to process the text.`;\n            }\n\n            return [\n                `${request.customPrompt}`,\n                ``,\n                `Here is the content to work with:`,\n                `\"\"\"`,\n                `${isEmpty ? \"(No content provided)\" : content}`,\n                `\"\"\"`,\n                `Return only the processed text with no additional context, explanations, or preamble.`\n            ].join('\\n');\n\n        default:\n            return isEmpty ? \"Please provide some content.\" : content;\n    }\n}\n\n/**\n * Get system and user messages for a completion request\n */\nexport function getMessagesForRequest(request: AIEditorRequest): AIMessage[] {\n    return [\n        {\n            role: 'system',\n            content: SYSTEM_MESSAGES[request.type]\n        },\n        {\n            role: 'user',\n            content: generatePrompt(request)\n        }\n    ];\n}\n\n/**\n * Get a descriptive label for an AI completion type\n */\nexport function getCompletionTypeLabel(type: AICompletionType): string {\n    switch (type) {\n        case AICompletionType.COMPLETE:\n            return 'Continue Writing';\n        case AICompletionType.EXPAND:\n            return 'Expand Content';\n        case AICompletionType.IMPROVE:\n            return 'Improve Writing';\n        case AICompletionType.SUMMARIZE:\n            return 'Summarize';\n        case AICompletionType.REPHRASE:\n            return 'Rephrase';\n        case AICompletionType.FIX_GRAMMAR:\n            return 'Fix Grammar';\n        case AICompletionType.CUSTOM:\n            return 'Custom Instruction';\n        default:\n            return 'AI Assistant';\n    }\n}\n\n/**\n * Get a description for an AI completion type\n */\nexport function getCompletionTypeDescription(type: AICompletionType): string {\n    switch (type) {\n        case AICompletionType.COMPLETE:\n            return 'Continue writing from where you left off';\n        case AICompletionType.EXPAND:\n            return 'Add more details, examples, or explanations';\n        case AICompletionType.IMPROVE:\n            return 'Enhance clarity, flow, and style';\n        case AICompletionType.SUMMARIZE:\n            return 'Create a concise summary of the text';\n        case AICompletionType.REPHRASE:\n            return 'Rewrite with different wording';\n        case AICompletionType.FIX_GRAMMAR:\n            return 'Fix grammar, spelling, and punctuation';\n        case AICompletionType.CUSTOM:\n            return 'Write your own instructions';\n        default:\n            return '';\n    }\n}"
  },
  {
    "path": "lib/utils/ai/secton.ts",
    "content": "/**\n * Secton API Integration\n * Provides utilities for interacting with the Secton AI service\n */\n\nimport {AIError, AIMessage, AIModel, CompletionRequest, CompletionResponse} from './types';\n\n/**\n * Configuration for Secton API\n */\nexport interface SectonConfig {\n    apiKey: string;\n    baseUrl?: string;\n    defaultModel?: string;\n}\n\n/**\n * Default configuration\n */\nconst DEFAULT_CONFIG: Partial<SectonConfig> = {\n    baseUrl: 'https://api.secton.org/v1',\n    defaultModel: 'copilot-zero',\n};\n\n/**\n * Secton API Client\n */\nexport class SectonClient {\n    private config: SectonConfig;\n\n    constructor(config: SectonConfig) {\n        this.config = {\n            ...DEFAULT_CONFIG,\n            ...config,\n        };\n\n        console.log('SectonClient initialized:', {\n            baseUrl: this.config.baseUrl,\n            defaultModel: this.config.defaultModel,\n            hasApiKey: !!this.config.apiKey,\n            apiKeyPrefix: this.config.apiKey ? this.config.apiKey.substring(0, 8) + '...' : 'none'\n        });\n    }\n\n    /**\n     * Get available models from the API\n     */\n    async getModels(): Promise<AIModel[]> {\n        // console.log('Fetching available models...');\n\n        try {\n            const url = `${this.config.baseUrl}/models`;\n            // console.log('Models request URL:', url);\n\n            const response = await fetch(url, {\n                method: 'GET',\n                headers: {\n                    'Authorization': `Bearer ${this.config.apiKey}`,\n                    'Content-Type': 'application/json',\n                },\n            });\n\n            // console.log('📡 Models response status:', response.status, response.statusText);\n            // console.log('📡 Models response headers:', Object.fromEntries(response.headers.entries()));\n\n            if (!response.ok) {\n                const errorText = await response.text();\n                console.error('❌ Models API error response:', errorText);\n\n                let error;\n                try {\n                    error = JSON.parse(errorText);\n                } catch {\n                    error = {message: errorText};\n                }\n\n                throw new AIError('Failed to fetch models', response.status, error);\n            }\n\n            const models = await response.json();\n            // console.log('✅ Models fetched successfully:', models);\n            return models;\n        } catch (error) {\n            console.error('💥 Models fetch error:', error);\n            if (error instanceof AIError) {\n                throw error;\n            }\n            throw new AIError('Failed to fetch models', 500, error);\n        }\n    }\n\n    /**\n     * Generate a completion using the chat API\n     */\n    async createCompletion(request: Partial<CompletionRequest>): Promise<CompletionResponse> {\n        // console.log('🚀 Starting completion request...');\n        // console.log('📝 Input request:', JSON.stringify(request, null, 2));\n\n        try {\n            const completionRequest: CompletionRequest = {\n                model: request.model || this.config.defaultModel || 'copilot-zero',\n                messages: request.messages || [],\n                temperature: request.temperature ?? 0.7,\n                max_tokens: request.max_tokens ?? 1024,\n            };\n\n            // console.log('📋 Final completion request:', JSON.stringify(completionRequest, null, 2));\n            // console.log('🔑 API Key being used:', this.config.apiKey ? this.config.apiKey.substring(0, 8) + '...' : 'none');\n\n            const url = `${this.config.baseUrl}/chat/completions`;\n            console.log('📡 Request URL:', url);\n\n            const headers = {\n                'Content-Type': 'application/json',\n                'Authorization': `Bearer ${this.config.apiKey}`,\n                'User-Agent': 'Changerawr/1.0',\n            };\n            // console.log('📋 Request headers:', {\n            //     ...headers,\n            //     'Authorization': headers.Authorization ? headers.Authorization.substring(0, 20) + '...' : 'none'\n            // });\n\n            const requestBody = JSON.stringify(completionRequest);\n            // console.log('📦 Request body size:', requestBody.length, 'characters');\n\n            // console.log('📡 Making fetch request...');\n            const response = await fetch(url, {\n                method: 'POST',\n                headers,\n                body: requestBody,\n            });\n\n            // console.log('📡 Response received - Status:', response.status, response.statusText);\n            // console.log('📡 Response headers:', Object.fromEntries(response.headers.entries()));\n\n            if (!response.ok) {\n                console.error('❌ Response not OK, reading error...');\n                const errorText = await response.text();\n                console.error('❌ Error response text:', errorText);\n\n                let error;\n                try {\n                    error = JSON.parse(errorText);\n                    console.error('❌ Parsed error object:', error);\n                } catch (parseError) {\n                    console.error('❌ Failed to parse error as JSON:', parseError);\n                    error = {message: errorText};\n                }\n\n                throw new AIError('Failed to create completion', response.status, error);\n            }\n\n            // console.log('📖 Reading response body...');\n            const responseText = await response.text();\n            // console.log('📄 Raw response text length:', responseText.length);\n            // console.log('📄 Raw response preview (first 500 chars):', responseText.substring(0, 500));\n\n            let jsonResponse;\n            try {\n                // console.log('🔄 Parsing JSON response...');\n                jsonResponse = JSON.parse(responseText);\n                // console.log('✅ JSON parsed successfully');\n                // console.log('📋 Response structure:', {\n                //     hasObject: 'object' in jsonResponse,\n                //     hasModel: 'model' in jsonResponse,\n                //     hasMessages: 'messages' in jsonResponse,\n                //     hasUsage: 'usage' in jsonResponse,\n                //     keys: Object.keys(jsonResponse)\n                // });\n            } catch (parseError) {\n                console.error('💥 Failed to parse JSON response:', parseError);\n                console.error('💥 Response text that failed to parse:', responseText);\n                throw new AIError('Invalid JSON response from AI service', 500, {\n                    originalError: parseError,\n                    responseText: responseText.substring(0, 1000)\n                });\n            }\n\n            // Log the complete response structure\n            console.log('📋 Complete response object:', JSON.stringify(jsonResponse, null, 2));\n\n            // Validate response structure\n            if (!jsonResponse.messages || !Array.isArray(jsonResponse.messages)) {\n                console.error('❌ Invalid response structure - missing or invalid messages array');\n                console.error('❌ Response keys:', Object.keys(jsonResponse));\n                throw new AIError('Invalid response structure from AI service', 500, jsonResponse);\n            }\n\n            // console.log('✅ Completion successful!');\n            // console.log('📊 Response summary:', {\n            //     model: jsonResponse.model,\n            //     messageCount: jsonResponse.messages?.length,\n            //     hasUsage: !!jsonResponse.usage,\n            //     totalTokens: jsonResponse.usage?.total_tokens\n            // });\n\n            return jsonResponse as CompletionResponse;\n        } catch (error) {\n            console.error('💥 AI Completion Error Details:');\n            console.error('💥 Error type:', typeof error);\n            console.error('💥 Error constructor:', error?.constructor?.name);\n            console.error('💥 Error message:', error instanceof Error ? error.message : 'Unknown error');\n            console.error('💥 Full error object:', error);\n\n            if (error instanceof AIError) {\n                console.error('💥 AIError details:', {\n                    message: error.message,\n                    statusCode: error.statusCode,\n                    details: error.details\n                });\n                throw error;\n            }\n\n            if (error instanceof Error) {\n                throw new AIError('Failed to create completion', 500, {\n                    originalMessage: error.message,\n                    originalStack: error.stack,\n                    originalError: error\n                });\n            }\n\n            throw new AIError('Unknown error during completion', 500, error);\n        }\n    }\n\n    /**\n     * Helper to quickly generate text from a prompt\n     */\n    async generateText(prompt: string, options: Partial<CompletionRequest> = {}): Promise<string> {\n        // console.log('📝 generateText called with prompt length:', prompt.length);\n        // console.log('📝 generateText options:', options);\n\n        const messages: AIMessage[] = [\n            {role: 'user', content: prompt}\n        ];\n\n        // console.log('📝 Constructed messages:', messages);\n\n        const completion = await this.createCompletion({\n            ...options,\n            messages,\n        });\n\n        // console.log('📝 Completion response received');\n        // console.log('📝 Response messages:', completion.messages);\n\n        // Find the assistant's response\n        const assistantMessage = completion.messages.find(m => m.role === 'assistant');\n        // console.log('📝 Assistant message found:', !!assistantMessage);\n\n        if (!assistantMessage) {\n            console.error('❌ No assistant message in response');\n            console.error('❌ Available messages:', completion.messages.map(m => ({\n                role: m.role,\n                contentLength: m.content?.length\n            })));\n            throw new AIError('No assistant response found', 500, completion);\n        }\n\n        const result = assistantMessage.content || '';\n        // console.log('✅ generateText result length:', result.length);\n        // console.log('✅ generateText result preview:', result.substring(0, 200) + (result.length > 200 ? '...' : ''));\n\n        return result;\n    }\n\n    /**\n     * Check if the API key is valid by fetching models\n     */\n    async validateApiKey(): Promise<boolean> {\n        // console.log('🔑 Validating API key...');\n\n        try {\n            await this.getModels();\n            // console.log('✅ API key validation successful');\n            return true;\n        } catch (error) {\n            console.error('❌ API key validation failed:', error);\n            return false;\n        }\n    }\n}\n\n/**\n * Create a new Secton client\n */\nexport function createSectonClient(config: SectonConfig): SectonClient {\n    console.log('Creating new SectonClient with config:', {\n        ...config,\n        apiKey: config.apiKey ? config.apiKey.substring(0, 8) + '...' : 'none'\n    });\n\n    return new SectonClient(config);\n}"
  },
  {
    "path": "lib/utils/ai/types.ts",
    "content": "/**\n * Type definitions for AI-related functionality\n */\n\n// AI Completion types for editor integration\nexport enum AICompletionType {\n    COMPLETE = 'complete',\n    EXPAND = 'expand',\n    IMPROVE = 'improve',\n    SUMMARIZE = 'summarize',\n    REPHRASE = 'rephrase',\n    FIX_GRAMMAR = 'fix_grammar',\n    CUSTOM = 'custom',\n}\n\n// AI Completion request from editor\nexport interface AIEditorRequest {\n    type: AICompletionType;\n    content: string;\n    customPrompt?: string;\n    selection?: {\n        start: number;\n        end: number;\n        text: string;\n    };\n    options?: {\n        temperature?: number;\n        max_tokens?: number;\n    };\n}\n\n// AI Completion result for editor\nexport interface AIEditorResult {\n    text: string;\n    originalRequest: AIEditorRequest;\n    usage?: {\n        prompt_tokens: number;\n        completion_tokens: number;\n        total_tokens: number;\n    };\n    metadata?: {\n        model: string;\n        timestamp: number;\n    };\n}\n\n// AI Message in a conversation\nexport interface AIMessage {\n    role: 'system' | 'user' | 'assistant';\n    content: string;\n}\n\n// Request to create a completion\nexport interface CompletionRequest {\n    model: string;\n    messages: AIMessage[];\n    temperature?: number;\n    max_tokens?: number;\n}\n\n// Response from the completion API\nexport interface CompletionResponse {\n    object: string;\n    model: string;\n    messages: AIMessage[];\n    usage: {\n        prompt_tokens: number;\n        completion_tokens: number;\n        total_tokens: number;\n    };\n}\n\n// AI Model information\nexport interface AIModel {\n    id: string;\n    name?: string;\n    provider?: string;\n}\n\n/**\n * Custom error class for AI-related errors\n */\nexport class AIError extends Error {\n    statusCode: number;\n    details: unknown;\n\n    constructor(message: string, statusCode: number, details?: unknown) {\n        super(message);\n        this.name = 'AIError';\n        this.statusCode = statusCode;\n        this.details = details;\n    }\n}"
  },
  {
    "path": "lib/utils/analytics.ts",
    "content": "// lib/utils/analytics.ts\nimport {db} from '@/lib/db';\n\nexport interface AnalyticsTimeRange {\n    start: Date;\n    end: Date;\n}\n\nexport interface ProjectAnalytics {\n    totalViews: number;\n    uniqueVisitors: number;\n    topCountries: Array<{ country: string; count: number }>;\n    dailyViews: Array<{ date: string; views: number; uniqueVisitors: number }>;\n    topEntries: Array<{\n        entryId: string;\n        title: string;\n        views: number;\n        uniqueVisitors: number\n    }>;\n    topReferrers: Array<{ referrer: string; count: number }>;\n}\n\nexport interface SystemAnalytics extends ProjectAnalytics {\n    topProjects: Array<{\n        projectId: string;\n        projectName: string;\n        views: number;\n        uniqueVisitors: number\n    }>;\n}\n\n/**\n * Get time range for analytics queries\n */\nexport function getTimeRange(period: '7d' | '30d' | '90d' | '1y'): AnalyticsTimeRange {\n    const end = new Date();\n    const start = new Date();\n\n    switch (period) {\n        case '7d':\n            start.setDate(end.getDate() - 7);\n            break;\n        case '30d':\n            start.setDate(end.getDate() - 30);\n            break;\n        case '90d':\n            start.setDate(end.getDate() - 90);\n            break;\n        case '1y':\n            start.setFullYear(end.getFullYear() - 1);\n            break;\n    }\n\n    return {start, end};\n}\n\n/**\n * Get analytics for a specific project\n */\nexport async function getProjectAnalytics(\n    projectId: string,\n    timeRange: AnalyticsTimeRange\n): Promise<ProjectAnalytics> {\n    // Base where clause for project\n    const baseWhere = {\n        projectId,\n        viewedAt: {\n            gte: timeRange.start,\n            lte: timeRange.end\n        }\n    };\n\n    // Total views in time range\n    const totalViews = await db.publicChangelogAnalytics.count({\n        where: baseWhere\n    });\n\n    // Unique visitors using raw query for better performance\n    const uniqueVisitorsData = await db.$queryRaw<Array<{ count: bigint }>>`\n        SELECT COUNT(DISTINCT \"sessionHash\") as count\n        FROM \"PublicChangelogAnalytics\"\n        WHERE \"projectId\" = ${projectId}\n          AND \"viewedAt\" >= ${timeRange.start}\n          AND \"viewedAt\" <= ${timeRange.end}\n    `;\n    const uniqueVisitors = Number(uniqueVisitorsData[0]?.count || 0);\n\n    // Top countries using raw query\n    const topCountriesData = await db.$queryRaw<Array<{\n        country: string;\n        count: bigint\n    }>>`\n        SELECT \"country\", COUNT(*) as count\n        FROM \"PublicChangelogAnalytics\"\n        WHERE \"projectId\" = ${projectId}\n          AND \"country\" IS NOT NULL\n          AND \"viewedAt\" >= ${timeRange.start}\n          AND \"viewedAt\" <= ${timeRange.end}\n        GROUP BY \"country\"\n        ORDER BY count DESC\n        LIMIT 10\n    `;\n\n    const topCountries = topCountriesData.map(item => ({\n        country: item.country || 'Unknown',\n        count: Number(item.count)\n    }));\n\n    // Daily views using raw query\n    const dailyViewsData = await db.$queryRaw<Array<{\n        date: string;\n        views: bigint;\n        unique_visitors: bigint;\n    }>>`\n        SELECT\n            DATE(\"viewedAt\") as date,\n            COUNT(*) as views,\n            COUNT(DISTINCT \"sessionHash\") as unique_visitors\n        FROM \"PublicChangelogAnalytics\"\n        WHERE \"projectId\" = ${projectId}\n          AND \"viewedAt\" >= ${timeRange.start}\n          AND \"viewedAt\" <= ${timeRange.end}\n        GROUP BY DATE(\"viewedAt\")\n        ORDER BY date DESC\n    `;\n\n    const dailyViews = dailyViewsData.map(item => ({\n        date: item.date,\n        views: Number(item.views),\n        uniqueVisitors: Number(item.unique_visitors)\n    }));\n\n    // Top entries using raw query\n    const topEntriesData = await db.$queryRaw<Array<{\n        changelogEntryId: string;\n        views: bigint;\n        unique_visitors: bigint;\n    }>>`\n        SELECT\n            \"changelogEntryId\",\n            COUNT(*) as views,\n            COUNT(DISTINCT \"sessionHash\") as unique_visitors\n        FROM \"PublicChangelogAnalytics\"\n        WHERE \"projectId\" = ${projectId}\n          AND \"changelogEntryId\" IS NOT NULL\n          AND \"viewedAt\" >= ${timeRange.start}\n          AND \"viewedAt\" <= ${timeRange.end}\n        GROUP BY \"changelogEntryId\"\n        ORDER BY views DESC\n        LIMIT 10\n    `;\n\n    // Get entry titles for the top entries\n    const entryIds = topEntriesData.map(item => item.changelogEntryId);\n    const entries = entryIds.length > 0 ? await db.changelogEntry.findMany({\n        where: {id: {in: entryIds}},\n        select: {id: true, title: true}\n    }) : [];\n\n    const entriesMap = new Map(entries.map(entry => [entry.id, entry.title]));\n\n    const topEntries = topEntriesData.map(item => ({\n        entryId: item.changelogEntryId,\n        title: entriesMap.get(item.changelogEntryId) || 'Unknown Entry',\n        views: Number(item.views),\n        uniqueVisitors: Number(item.unique_visitors)\n    }));\n\n    // Top referrers using raw query\n    const topReferrersData = await db.$queryRaw<Array<{\n        referrer: string;\n        count: bigint\n    }>>`\n        SELECT \"referrer\", COUNT(*) as count\n        FROM \"PublicChangelogAnalytics\"\n        WHERE \"projectId\" = ${projectId}\n          AND \"referrer\" IS NOT NULL\n          AND \"viewedAt\" >= ${timeRange.start}\n          AND \"viewedAt\" <= ${timeRange.end}\n        GROUP BY \"referrer\"\n        ORDER BY count DESC\n        LIMIT 10\n    `;\n\n    const topReferrers = topReferrersData.map(item => ({\n        referrer: item.referrer || 'Direct',\n        count: Number(item.count)\n    }));\n\n    return {\n        totalViews,\n        uniqueVisitors,\n        topCountries,\n        dailyViews,\n        topEntries,\n        topReferrers\n    };\n}\n\n/**\n * Get system-wide analytics (admin only)\n */\nexport async function getSystemAnalytics(timeRange: AnalyticsTimeRange): Promise<SystemAnalytics> {\n    // Base where clause for time range\n    const timeWhere = {\n        viewedAt: {\n            gte: timeRange.start,\n            lte: timeRange.end\n        }\n    };\n\n    // Total views system-wide\n    const totalViews = await db.publicChangelogAnalytics.count({\n        where: timeWhere\n    });\n\n    // Unique visitors system-wide using raw query\n    const uniqueVisitorsData = await db.$queryRaw<Array<{ count: bigint }>>`\n        SELECT COUNT(DISTINCT \"sessionHash\") as count\n        FROM \"PublicChangelogAnalytics\"\n        WHERE \"viewedAt\" >= ${timeRange.start}\n          AND \"viewedAt\" <= ${timeRange.end}\n    `;\n    const uniqueVisitors = Number(uniqueVisitorsData[0]?.count || 0);\n\n    // Top countries system-wide using raw query\n    const topCountriesData = await db.$queryRaw<Array<{\n        country: string;\n        count: bigint\n    }>>`\n        SELECT \"country\", COUNT(*) as count\n        FROM \"PublicChangelogAnalytics\"\n        WHERE \"country\" IS NOT NULL\n          AND \"viewedAt\" >= ${timeRange.start}\n          AND \"viewedAt\" <= ${timeRange.end}\n        GROUP BY \"country\"\n        ORDER BY count DESC\n        LIMIT 10\n    `;\n\n    const topCountries = topCountriesData.map(item => ({\n        country: item.country || 'Unknown',\n        count: Number(item.count)\n    }));\n\n    // Daily views system-wide using raw query\n    const dailyViewsData = await db.$queryRaw<Array<{\n        date: string;\n        views: bigint;\n        unique_visitors: bigint;\n    }>>`\n        SELECT\n            DATE(\"viewedAt\") as date,\n            COUNT(*) as views,\n            COUNT(DISTINCT \"sessionHash\") as unique_visitors\n        FROM \"PublicChangelogAnalytics\"\n        WHERE \"viewedAt\" >= ${timeRange.start}\n          AND \"viewedAt\" <= ${timeRange.end}\n        GROUP BY DATE(\"viewedAt\")\n        ORDER BY date DESC\n    `;\n\n    const dailyViews = dailyViewsData.map(item => ({\n        date: item.date,\n        views: Number(item.views),\n        uniqueVisitors: Number(item.unique_visitors)\n    }));\n\n    // Top entries system-wide using raw query\n    const topEntriesData = await db.$queryRaw<Array<{\n        changelogEntryId: string;\n        views: bigint;\n        unique_visitors: bigint;\n    }>>`\n        SELECT\n            \"changelogEntryId\",\n            COUNT(*) as views,\n            COUNT(DISTINCT \"sessionHash\") as unique_visitors\n        FROM \"PublicChangelogAnalytics\"\n        WHERE \"changelogEntryId\" IS NOT NULL\n          AND \"viewedAt\" >= ${timeRange.start}\n          AND \"viewedAt\" <= ${timeRange.end}\n        GROUP BY \"changelogEntryId\"\n        ORDER BY views DESC\n        LIMIT 10\n    `;\n\n    // Get entry titles for the top entries\n    const entryIds = topEntriesData.map(item => item.changelogEntryId);\n    const entries = entryIds.length > 0 ? await db.changelogEntry.findMany({\n        where: {id: {in: entryIds}},\n        select: {id: true, title: true}\n    }) : [];\n\n    const entriesMap = new Map(entries.map(entry => [entry.id, entry.title]));\n\n    const topEntries = topEntriesData.map(item => ({\n        entryId: item.changelogEntryId,\n        title: entriesMap.get(item.changelogEntryId) || 'Unknown Entry',\n        views: Number(item.views),\n        uniqueVisitors: Number(item.unique_visitors)\n    }));\n\n    // Top referrers system-wide using raw query\n    const topReferrersData = await db.$queryRaw<Array<{\n        referrer: string;\n        count: bigint\n    }>>`\n        SELECT \"referrer\", COUNT(*) as count\n        FROM \"PublicChangelogAnalytics\"\n        WHERE \"referrer\" IS NOT NULL\n          AND \"viewedAt\" >= ${timeRange.start}\n          AND \"viewedAt\" <= ${timeRange.end}\n        GROUP BY \"referrer\"\n        ORDER BY count DESC\n        LIMIT 10\n    `;\n\n    const topReferrers = topReferrersData.map(item => ({\n        referrer: item.referrer || 'Direct',\n        count: Number(item.count)\n    }));\n\n    // Top projects using raw query\n    const topProjectsData = await db.$queryRaw<Array<{\n        projectId: string;\n        views: bigint;\n        unique_visitors: bigint;\n    }>>`\n        SELECT\n            \"projectId\",\n            COUNT(*) as views,\n            COUNT(DISTINCT \"sessionHash\") as unique_visitors\n        FROM \"PublicChangelogAnalytics\"\n        WHERE \"viewedAt\" >= ${timeRange.start}\n          AND \"viewedAt\" <= ${timeRange.end}\n        GROUP BY \"projectId\"\n        ORDER BY views DESC\n        LIMIT 10\n    `;\n\n    // Get project names for the top projects\n    const projectIds = topProjectsData.map(item => item.projectId);\n    const projects = projectIds.length > 0 ? await db.project.findMany({\n        where: {id: {in: projectIds}},\n        select: {id: true, name: true}\n    }) : [];\n\n    const projectsMap = new Map(projects.map(project => [project.id, project.name]));\n\n    const topProjects = topProjectsData.map(item => ({\n        projectId: item.projectId,\n        projectName: projectsMap.get(item.projectId) || 'Unknown Project',\n        views: Number(item.views),\n        uniqueVisitors: Number(item.unique_visitors)\n    }));\n\n    return {\n        totalViews,\n        uniqueVisitors,\n        topCountries,\n        dailyViews,\n        topEntries,\n        topReferrers,\n        topProjects\n    };\n}"
  },
  {
    "path": "lib/utils/api.ts",
    "content": "export async function fetchWithAuth(\n    input: RequestInfo | URL,\n    init?: RequestInit\n) {\n    const accessToken = localStorage.getItem('accessToken')\n    if (!accessToken) {\n        throw new Error('No access token')\n    }\n\n    const headers = new Headers(init?.headers)\n    headers.set('Authorization', `Bearer ${accessToken}`)\n\n    const response = await fetch(input, {\n        ...init,\n        headers\n    })\n\n    if (response.status === 401) {\n        // Try to refresh the token\n        const refreshToken = localStorage.getItem('refreshToken')\n        if (!refreshToken) {\n            throw new Error('No refresh token')\n        }\n\n        const refreshResponse = await fetch('/api/auth/refresh', {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/json'\n            },\n            body: JSON.stringify({ refreshToken })\n        })\n\n        if (!refreshResponse.ok) {\n            // Clear tokens and throw error\n            localStorage.removeItem('accessToken')\n            localStorage.removeItem('refreshToken')\n            throw new Error('Session expired')\n        }\n\n        const { accessToken: newAccessToken } = await refreshResponse.json()\n        localStorage.setItem('accessToken', newAccessToken)\n\n        // Retry the original request with new token\n        headers.set('Authorization', `Bearer ${newAccessToken}`)\n        return fetch(input, {\n            ...init,\n            headers\n        })\n    }\n\n    return response\n}"
  },
  {
    "path": "lib/utils/auditLog.ts",
    "content": "import {db} from '@/lib/db';\nimport type {Prisma} from '@prisma/client';\nimport {SYSTEM_USER_ID} from '@/lib/services/core/system-user/service';\n\ninterface AuditLogDetails {\n    [key: string]: unknown;\n}\n\n/**\n * Creates an audit log entry\n * @param action The type of action performed\n * @param performedById ID of the user performing the action (use 'system' for system actions)\n * @param targetUserId ID of the user the action is performed on (can be null)\n * @param details Optional additional details about the action\n */\nexport async function createAuditLog(\n    action: string,\n    performedById: string | null,\n    targetUserId: string | null,\n    details?: AuditLogDetails\n): Promise<{ id: string } | null> {\n    try {\n        // Map 'system' string to SYSTEM_USER_ID constant\n        if (performedById === 'system') {\n            performedById = SYSTEM_USER_ID;\n        }\n        if (targetUserId === 'system') {\n            targetUserId = SYSTEM_USER_ID;\n        }\n\n        let safeDetails: AuditLogDetails = {};\n\n        if (details && typeof details === 'object') {\n            safeDetails = details;\n        } else if (details !== undefined) {\n            console.warn(\n                `Audit log received invalid details type: ${typeof details}`,\n                details\n            );\n        }\n\n        const baseData = {\n            action,\n            details: JSON.stringify(safeDetails),\n        };\n\n        // Conditionally add userId and targetUserId only if they exist\n        const data = {\n            ...baseData,\n            ...(performedById && {userId: performedById}),\n            ...(targetUserId && {targetUserId: targetUserId}),\n        } as Prisma.AuditLogUncheckedCreateInput;\n\n        return await db.auditLog.create({data}); // Return the created log with ID\n    } catch (error) {\n        console.error('Failed to create audit log:', error);\n        return null; // Return null on error\n    }\n}"
  },
  {
    "path": "lib/utils/changelog.ts",
    "content": "import {z} from 'zod'\nimport {db} from '@/lib/db'\nimport {RequestStatus} from '@prisma/client'\nimport {cookies, headers} from \"next/headers\";\nimport type {AuthRequest} from '@/lib/auth/api-key';\n\n// Zod Schemas\nexport const requestStatusSchema = z.object({\n    status: z.enum(['PENDING', 'APPROVED', 'REJECTED'] as const)\n}) satisfies z.ZodType<{ status: RequestStatus }>\n\nexport const changelogEntrySchema = z.object({\n    title: z.string().min(1, 'Title is required'),\n    content: z.string().min(1, 'Content is required'),\n    version: z.string().optional(),\n    tags: z.array(z.string()).optional()\n})\n\nexport type ChangelogEntryInput = z.infer<typeof changelogEntrySchema>\n\n// Auth Helper\nexport async function validateAuthAndGetUser() {\n    // Create a NextRequest-like object from headers/cookies\n    const headersList = await headers();\n    const cookieStore = await cookies();\n\n    // Build a minimal request object for authenticateRequest\n    const requestLike: AuthRequest = {\n        headers: {\n            get: (name: string) => headersList.get(name)\n        },\n        cookies: {\n            get: (name: string) => cookieStore.get(name)\n        }\n    };\n\n    const { authenticateRequest } = await import('@/lib/auth/api-key');\n    const ctx = await authenticateRequest(requestLike);\n\n    if (!ctx) {\n        throw new Error('Authentication required');\n    }\n\n    // Get the actual user from the database\n    const user = await db.user.findUnique({\n        where: { id: ctx.userId }\n    });\n\n    if (!user) {\n        throw new Error('User not found');\n    }\n\n    return user;\n}\n\n// Response Helpers\nexport function sendError(message: string, status: number = 400) {\n    return new Response(\n        JSON.stringify({error: message}),\n        {\n            status,\n            headers: {'Content-Type': 'application/json'}\n        }\n    )\n}\n\nexport function sendSuccess(data: unknown, status: number = 200) {\n    return new Response(\n        JSON.stringify(data),\n        {\n            status,\n            headers: {'Content-Type': 'application/json'}\n        }\n    )\n}\n\n// Excerpt Helper\nconst EXCERPT_LENGTH = 300;\n\nexport function generateExcerpt(content: string): string {\n    if (!content) return '';\n\n    // Basic markdown cleanup - remove common formatting\n    const cleaned = content\n        .replace(/^#{1,6}\\s+/gm, '') // Remove headers\n        .replace(/\\*\\*([^*]+)\\*\\*/g, '$1') // Remove bold\n        .replace(/\\*([^*]+)\\*/g, '$1') // Remove italic\n        .replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, '$1') // Remove links, keep text\n        .replace(/`([^`]+)`/g, '$1') // Remove inline code\n        .replace(/```[\\s\\S]*?```/g, '') // Remove code blocks\n        .replace(/\\n+/g, ' ') // Replace newlines with spaces\n        .replace(/\\s+/g, ' ') // Normalize whitespace\n        .trim();\n\n    // Truncate to excerpt length\n    if (cleaned.length <= EXCERPT_LENGTH) {\n        return cleaned;\n    }\n\n    // Try to truncate at a word boundary\n    const truncated = cleaned.substring(0, EXCERPT_LENGTH);\n    const lastSpace = truncated.lastIndexOf(' ');\n\n    if (lastSpace > EXCERPT_LENGTH * 0.8) {\n        // If we found a space in the last 20%, use it\n        return truncated.substring(0, lastSpace) + '...';\n    }\n\n    // Otherwise just truncate and add ellipsis\n    return truncated + '...';\n}"
  },
  {
    "path": "lib/utils/cookies.ts",
    "content": "/**\n * Determine if cookies should use the `secure` flag based on protocol and env config.\n *\n * Environment variables:\n * - COOKIE_SECURE=false          → disables secure flag entirely\n * - COOKIE_INSECURE_DOMAINS=*    → disables secure flag for all domains\n * - COOKIE_INSECURE_DOMAINS=localhost,internal.local → disables for listed hosts\n */\nexport function shouldUseSecureCookies(request: Request): boolean {\n    // Global override: COOKIE_SECURE=false disables secure cookies entirely\n    if (process.env.COOKIE_SECURE === 'false') {\n        return false\n    }\n\n    // Per-domain override\n    const insecureDomains =\n        process.env.COOKIE_INSECURE_DOMAINS?.trim() || '*'\n\n    if (insecureDomains === '*') {\n        return false\n    }\n\n    try {\n        const hostname = new URL(request.url).hostname.toLowerCase()\n        const allowed = insecureDomains\n            .split(',')\n            .map(d => d.trim().toLowerCase())\n            .filter(Boolean)\n\n        if (allowed.includes(hostname)) {\n            return false\n        }\n    } catch {\n        // Ignore URL parse errors\n    }\n\n    // Default: secure in production over HTTPS\n    if (process.env.NODE_ENV !== 'production') {\n        return false\n    }\n\n    const protocol = request.headers.get('x-forwarded-proto') || (request.url.startsWith('https') ? 'https' : 'http')\n    return protocol === 'https'\n}\n"
  },
  {
    "path": "lib/utils/docker.ts",
    "content": "export interface DockerImageConfig {\n    registry: string;\n    namespace: string;\n    repository: string;\n    defaultTag: string;\n}\n\nexport const DEFAULT_DOCKER_CONFIG: DockerImageConfig = {\n    registry: 'ghcr.io',\n    namespace: 'supernova3339',\n    repository: 'changerawr',\n    defaultTag: 'latest',\n};\n\n// Debug configuration\n// TODO: set to FALSE before pushing out 1.0.0\nconst DEBUG_MODE = false; // Set to false for production\nconst DEBUG_IMAGE = 'traefik/whoami';\n\n/**\n * Generate Docker image name with tag\n */\nexport function generateDockerImage(\n    version: string,\n    config: DockerImageConfig = DEFAULT_DOCKER_CONFIG\n): string {\n    // Debug mode override\n    if (DEBUG_MODE) {\n        console.log(`🐛 DEBUG MODE: Using debug image '${DEBUG_IMAGE}' instead of version ${version}`);\n        return DEBUG_IMAGE;\n    }\n\n    const { registry, namespace, repository } = config;\n    const tag = version.startsWith('v') ? version : `v${version}`;\n\n    return `${registry}/${namespace}/${repository}:${tag}`;\n}\n\n/**\n * Check if debug mode is enabled\n */\nexport function isDebugMode(): boolean {\n    return DEBUG_MODE;\n}\n\n/**\n * Get the debug image being used\n */\nexport function getDebugImage(): string {\n    return DEBUG_IMAGE;\n}\n\n/**\n * Generate Docker image name with debug info\n */\nexport function generateDockerImageWithDebugInfo(\n    version: string,\n    config: DockerImageConfig = DEFAULT_DOCKER_CONFIG\n): {\n    image: string;\n    isDebug: boolean;\n    originalImage?: string;\n} {\n    if (DEBUG_MODE) {\n        const originalImage = `${config.registry}/${config.namespace}/${config.repository}:${version.startsWith('v') ? version : `v${version}`}`;\n        return {\n            image: DEBUG_IMAGE,\n            isDebug: true,\n            originalImage,\n        };\n    }\n\n    const image = generateDockerImage(version, config);\n    return {\n        image,\n        isDebug: false,\n    };\n}\n\n/**\n * Parse Docker image string into components\n */\nexport function parseDockerImage(imageString: string): {\n    registry?: string;\n    namespace?: string;\n    repository: string;\n    tag?: string;\n} {\n    // Handle formats like:\n    // - changerawr:v1.0.0\n    // - supernova3339/changerawr:v1.0.0\n    // - ghcr.io/supernova3339/changerawr:v1.0.0\n\n    const parts = imageString.split(':');\n    const tag = parts.length > 1 ? parts[parts.length - 1] : undefined;\n    const imagePart = parts[0];\n\n    const pathParts = imagePart.split('/');\n\n    if (pathParts.length === 1) {\n        // Just repository name\n        return {\n            repository: pathParts[0],\n            tag,\n        };\n    } else if (pathParts.length === 2) {\n        // namespace/repository\n        return {\n            namespace: pathParts[0],\n            repository: pathParts[1],\n            tag,\n        };\n    } else if (pathParts.length >= 3) {\n        // registry/namespace/repository\n        return {\n            registry: pathParts[0],\n            namespace: pathParts[1],\n            repository: pathParts.slice(2).join('/'),\n            tag,\n        };\n    }\n\n    return {\n        repository: imageString,\n        tag,\n    };\n}\n\n/**\n * Validate Docker image format\n */\nexport function validateDockerImage(imageString: string): {\n    valid: boolean;\n    error?: string;\n} {\n    if (!imageString || typeof imageString !== 'string') {\n        return {\n            valid: false,\n            error: 'Image string is required and must be a string',\n        };\n    }\n\n    // Debug mode: allow traefik/whoami specifically\n    if (DEBUG_MODE && imageString === DEBUG_IMAGE) {\n        return { valid: true };\n    }\n\n    // Basic validation - check for invalid characters\n    const invalidChars = /[^a-zA-Z0-9.\\-_/:]/;\n    if (invalidChars.test(imageString)) {\n        return {\n            valid: false,\n            error: 'Image string contains invalid characters',\n        };\n    }\n\n    // Check if it has a reasonable format\n    const parsed = parseDockerImage(imageString);\n    if (!parsed.repository) {\n        return {\n            valid: false,\n            error: 'Invalid image format - repository name is required',\n        };\n    }\n\n    return { valid: true };\n}\n\n/**\n * Get available image variants for a version\n */\nexport function getImageVariants(\n    version: string,\n    config: DockerImageConfig = DEFAULT_DOCKER_CONFIG\n): string[] {\n    // Debug mode: return debug image only\n    if (DEBUG_MODE) {\n        return [DEBUG_IMAGE];\n    }\n\n    const variants = [\n        generateDockerImage(version, config),\n        generateDockerImage(`${version}-alpine`, config),\n    ];\n\n    // Add latest tag if this is a stable version (no pre-release identifiers)\n    if (!version.includes('-') && !version.includes('alpha') && !version.includes('beta')) {\n        variants.push(`${config.registry}/${config.namespace}/${config.repository}:latest`);\n    }\n\n    return variants;\n}\n\n/**\n * Log debug information about image generation\n */\nexport function logDockerDebugInfo(version: string): void {\n    if (DEBUG_MODE) {\n        console.log('🐛 Docker Debug Mode Active');\n        console.log(`   Requested version: ${version}`);\n        console.log(`   Debug image: ${DEBUG_IMAGE}`);\n        console.log(`   Original image would be: ${DEFAULT_DOCKER_CONFIG.registry}/${DEFAULT_DOCKER_CONFIG.namespace}/${DEFAULT_DOCKER_CONFIG.repository}:v${version}`);\n        console.log('   This will deploy a simple service that shows browser/request information');\n        console.log('   To disable debug mode, set DEBUG_MODE = false in lib/utils/docker.ts');\n    }\n}"
  },
  {
    "path": "lib/utils/encryption.ts",
    "content": "// Encryption key from environment - should be 32 bytes for AES-256\nimport crypto from \"crypto\";\n\nconst ENCRYPTION_KEY_HEX = process.env.GITHUB_ENCRYPTION_KEY || 'default-32-byte-key-change-this!!';\nconst ALGORITHM = 'aes-256-cbc';\nconst IV_LENGTH = 16; // For AES, this is always 16\n\n// Convert hex key to buffer, or create buffer from string if not hex\nfunction getKeyBuffer(): Buffer {\n    try {\n        // If it's a hex string (64 characters), convert from hex\n        if (ENCRYPTION_KEY_HEX.length === 64 && /^[0-9a-fA-F]+$/.test(ENCRYPTION_KEY_HEX)) {\n            return Buffer.from(ENCRYPTION_KEY_HEX, 'hex');\n        }\n        // Otherwise, treat as string and pad/truncate to 32 bytes\n        const keyBuffer = Buffer.from(ENCRYPTION_KEY_HEX, 'utf8');\n        if (keyBuffer.length === 32) {\n            return keyBuffer;\n        }\n        // Pad or truncate to 32 bytes\n        const paddedKey = Buffer.alloc(32);\n        keyBuffer.copy(paddedKey, 0, 0, Math.min(keyBuffer.length, 32));\n        return paddedKey;\n    } catch {\n        // Fallback: create a 32-byte buffer from the string\n        const keyBuffer = Buffer.from(ENCRYPTION_KEY_HEX, 'utf8');\n        const paddedKey = Buffer.alloc(32);\n        keyBuffer.copy(paddedKey, 0, 0, Math.min(keyBuffer.length, 32));\n        return paddedKey;\n    }\n}\n\n// Simple encryption for access tokens\nfunction encryptToken(token: string): string {\n    if (!token || token.trim() === '') {\n        return '';\n    }\n\n    try {\n        const iv = crypto.randomBytes(IV_LENGTH);\n        const key = getKeyBuffer();\n        const cipher = crypto.createCipheriv(ALGORITHM, key, iv);\n        let encrypted = cipher.update(token, 'utf8', 'hex');\n        encrypted += cipher.final('hex');\n        return iv.toString('hex') + ':' + encrypted;\n    } catch (error) {\n        console.error('Token encryption failed:', error);\n        throw new Error('Failed to encrypt access token');\n    }\n}\n\nfunction decryptToken(encryptedToken: string): string {\n    if (!encryptedToken || encryptedToken.trim() === '') {\n        return '';\n    }\n\n    try {\n        const textParts = encryptedToken.split(':');\n        if (textParts.length !== 2) {\n            throw new Error('Invalid encrypted token format');\n        }\n\n        const iv = Buffer.from(textParts[0], 'hex');\n        const encryptedText = textParts[1];\n        const key = getKeyBuffer();\n        const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);\n        let decrypted = decipher.update(encryptedText, 'hex', 'utf8');\n        decrypted += decipher.final('utf8');\n        return decrypted;\n    } catch (error) {\n        console.error('Token decryption failed:', error);\n        throw new Error('Failed to decrypt access token');\n    }\n}\n\nexport { encryptToken, decryptToken };"
  },
  {
    "path": "lib/utils/format-date.ts",
    "content": "/**\n * Timezone-aware date formatting utilities.\n *\n * These helpers format dates for user-facing display using the effective\n * timezone (user override or system global) fetched from /api/config/timezone.\n *\n * WHEN TO USE:\n *   - Displaying absolute dates to users (created at, published at, etc.)\n *   - Rendering dates in emails sent to users\n *\n * WHEN NOT TO USE:\n *   - Relative times (use date-fns formatDistanceToNow — timezone-safe)\n *   - Storing / comparing dates (keep ISO/UTC)\n *   - Server-side scheduling logic\n */\n\ntype DateInput = string | Date | number\n\nfunction toDate(input: DateInput): Date {\n    return input instanceof Date ? input : new Date(input)\n}\n\n// ─── Formatters (require a timezone string) ────────────────────────────\n\n/** \"January 15, 2025\" */\nexport function formatDateLong(input: DateInput, timeZone = 'UTC'): string {\n    return toDate(input).toLocaleDateString('en-US', {\n        year: 'numeric',\n        month: 'long',\n        day: 'numeric',\n        timeZone,\n    })\n}\n\n/** \"Jan 15, 2025\" */\nexport function formatDateMedium(input: DateInput, timeZone = 'UTC'): string {\n    return toDate(input).toLocaleDateString('en-US', {\n        year: 'numeric',\n        month: 'short',\n        day: 'numeric',\n        timeZone,\n    })\n}\n\n/** \"1/15/2025\" */\nexport function formatDateShort(input: DateInput, timeZone = 'UTC'): string {\n    return toDate(input).toLocaleDateString('en-US', {\n        year: 'numeric',\n        month: 'numeric',\n        day: 'numeric',\n        timeZone,\n    })\n}\n\n/** \"January 15, 2025 at 3:45 PM\" */\nexport function formatDateTime(input: DateInput, timeZone = 'UTC'): string {\n    return toDate(input).toLocaleString('en-US', {\n        year: 'numeric',\n        month: 'long',\n        day: 'numeric',\n        hour: 'numeric',\n        minute: '2-digit',\n        timeZone,\n    })\n}\n\n/** \"Jan 15, 2025, 3:45 PM\" */\nexport function formatDateTimeMedium(input: DateInput, timeZone = 'UTC'): string {\n    return toDate(input).toLocaleString('en-US', {\n        year: 'numeric',\n        month: 'short',\n        day: 'numeric',\n        hour: 'numeric',\n        minute: '2-digit',\n        timeZone,\n    })\n}\n\n/** \"3:45 PM\" */\nexport function formatTime(input: DateInput, timeZone = 'UTC'): string {\n    return toDate(input).toLocaleTimeString('en-US', {\n        hour: 'numeric',\n        minute: '2-digit',\n        timeZone,\n    })\n}\n\n/** \"Jan 15, 2025, 3:45 PM EST\" — includes timezone abbreviation */\nexport function formatDateTimeWithZone(input: DateInput, timeZone = 'UTC'): string {\n    return toDate(input).toLocaleString('en-US', {\n        year: 'numeric',\n        month: 'short',\n        day: 'numeric',\n        hour: 'numeric',\n        minute: '2-digit',\n        timeZoneName: 'short',\n        timeZone,\n    })\n}\n"
  },
  {
    "path": "lib/utils/gravatar.ts",
    "content": "import md5 from 'md5'\n\nexport function getGravatarUrl(email: string, size: number = 80): string {\n    const cleanEmail = email.trim().toLowerCase()\n    const hash = md5(cleanEmail)\n\n    // Use internal proxy to avoid tracking prevention blocks\n    // The proxy caches avatars and proxies requests to Gravatar\n    return `/api/avatar/${hash}?s=${size}`\n}"
  },
  {
    "path": "lib/utils/rate-limit.ts",
    "content": "/**\n * Simple in-memory rate limiter suitable for single-instance deployments.\n * For multi-instance setups, replace the store with a shared cache (Redis, etc.).\n */\n\ninterface RateLimitEntry {\n    count: number\n    resetAt: number\n}\n\nconst store = new Map<string, RateLimitEntry>()\n\n// Purge expired entries every 5 minutes to avoid unbounded memory growth\nsetInterval(() => {\n    const now = Date.now()\n    for (const [key, entry] of store.entries()) {\n        if (entry.resetAt <= now) store.delete(key)\n    }\n}, 5 * 60 * 1000)\n\nexport interface RateLimitResult {\n    allowed: boolean\n    remaining: number\n    resetAt: number\n}\n\n/**\n * Check and increment the rate limit counter for a given key.\n *\n * @param key    Unique identifier (e.g. `\"login:${ip}\"`)\n * @param limit  Maximum number of requests allowed in the window\n * @param windowMs  Window duration in milliseconds\n */\nexport function checkRateLimit(\n    key: string,\n    limit: number,\n    windowMs: number\n): RateLimitResult {\n    const now = Date.now()\n    const existing = store.get(key)\n\n    if (!existing || existing.resetAt <= now) {\n        // First request in this window\n        const resetAt = now + windowMs\n        store.set(key, { count: 1, resetAt })\n        return { allowed: true, remaining: limit - 1, resetAt }\n    }\n\n    existing.count++\n    const allowed = existing.count <= limit\n    return {\n        allowed,\n        remaining: Math.max(0, limit - existing.count),\n        resetAt: existing.resetAt,\n    }\n}\n\n/**\n * Extract a best-effort client IP from request headers.\n * Trusts x-forwarded-for (set by nginx/caddy in front of the app).\n */\nexport function getClientIp(request: Request): string {\n    const forwarded = (request as { headers: Headers }).headers.get('x-forwarded-for')\n    if (forwarded) return forwarded.split(',')[0].trim()\n    return 'unknown'\n}\n"
  },
  {
    "path": "lib/utils/text.ts",
    "content": "/**\n * Text formatting and manipulation utilities\n /**\n * Truncate text to a maximum length with ellipsis.\n * More reliable than CSS truncate for consistent server-side and client-side rendering.\n *\n * @param text - The text to truncate\n * @param maxLength - Maximum length before truncation (default: 60)\n * @returns Truncated text with ellipsis if needed\n *\n * @example\n * truncateText(\"This is a very long title that needs to be shortened\", 20)\n * // Returns: \"This is a very long...\"\n */\nexport function truncateText(text: string, maxLength: number = 60): string {\n    if (text.length <= maxLength) return text\n    return text.slice(0, maxLength).trim() + '...'\n}\n\n/**\n * Truncates markdown content to a specified character limit while preserving structure\n * Attempts to break at paragraph or sentence boundaries for better readability\n *\n * @param content - The markdown content to truncate\n * @param charLimit - Maximum number of characters (default: 400)\n * @returns Truncated content with ellipsis if exceeded\n *\n * @example\n * truncateMarkdown(\"# Heading\\n\\nLong paragraph...\", 50)\n * // Returns: \"# Heading\\n\\nLong paragraph…\"\n */\nexport function truncateMarkdown(content: string, charLimit: number = 400): string {\n    if (content.length <= charLimit) {\n        return content\n    }\n\n    // Find a good break point - prefer breaking at paragraph boundaries\n    let truncated = content.substring(0, charLimit)\n\n    // Try to break at the last paragraph break\n    const lastParagraphBreak = truncated.lastIndexOf('\\n\\n')\n    if (lastParagraphBreak > charLimit * 0.6) { // Only use if it's not too early\n        truncated = truncated.substring(0, lastParagraphBreak)\n    } else {\n        // Otherwise break at the last sentence\n        const lastPeriod = truncated.lastIndexOf('. ')\n        if (lastPeriod > charLimit * 0.6) {\n            truncated = truncated.substring(0, lastPeriod + 1)\n        } else {\n            // Break at the last space\n            const lastSpace = truncated.lastIndexOf(' ')\n            if (lastSpace > 0) {\n                truncated = truncated.substring(0, lastSpace)\n            }\n        }\n    }\n\n    return truncated.trim() + '…'\n}"
  },
  {
    "path": "lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"
  },
  {
    "path": "next.config.ts",
    "content": "import type { NextConfig } from \"next\";\nimport path from \"node:path\";\n\nconst nextConfig: NextConfig = {\n    reactStrictMode: false,\n    reactCompiler: true,\n    images: {\n        formats: [\"image/avif\", \"image/webp\"],\n        // No remote patterns needed - avatars are proxied through /api/avatar/[hash]\n        remotePatterns: [],\n    },\n    turbopack: {\n        root: path.join(__dirname, '..'),\n    },\n    // webpack: (config) => {\n    //     // Required for Redoc\n    //     config.resolve.fallback = {\n    //         ...config.resolve.fallback,\n    //         fs: false,\n    //         path: false,\n    //     };\n    //\n    //     return config;\n    // },\n    experimental: {\n        // Enable optimized Fast Refresh for faster development\n        optimizePackageImports: ['lucide-react', '@radix-ui/react-icons', '@heroicons/react'],\n        // Turbo mode for faster builds\n    },\n    // output: 'standalone', uses next-start, leave commented-out\n};\n\nexport default nextConfig;"
  },
  {
    "path": "next.openapi.json",
    "content": "{\n  \"openapi\": \"3.0.0\",\n  \"info\": {\n    \"title\": \"API Documentation\",\n    \"version\": \"1.0.0\",\n    \"description\": \"This is the OpenAPI specification for your project.\"\n  },\n  \"servers\": [\n    {\n      \"url\": \"http://localhost:3000/api\",\n      \"description\": \"Local development server\"\n    }\n  ],\n  \"components\": {\n    \"securitySchemes\": {\n      \"BearerAuth\": {\n        \"type\": \"http\",\n        \"scheme\": \"bearer\",\n        \"bearerFormat\": \"JWT\"\n      }\n    }\n  },\n  \"apiDir\": \"../app/api\",\n  \"schemaDir\": \"./\",\n  \"docsUrl\": \"api-docs\",\n  \"ui\": \"swagger\",\n  \"outputFile\": \"swagger.json\",\n  \"includeOpenApiRoutes\": false\n}\n"
  },
  {
    "path": "nginx.conf",
    "content": "# Main nginx configuration for Changerawr\n# Don't set user in Alpine Linux with nginx - it runs as nginx by default\nworker_processes auto;\nerror_log /var/log/nginx/error.log warn;\npid /run/nginx/nginx.pid;\n\nevents {\n    worker_connections 1024;\n}\n\nhttp {\n    include /etc/nginx/mime.types;\n    default_type application/octet-stream;\n\n    log_format main '$remote_addr - $remote_user [$time_local] \"$request\" '\n                    '$status $body_bytes_sent \"$http_referer\" '\n                    '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n\n    access_log /var/log/nginx/access.log main;\n\n    sendfile on;\n    tcp_nopush on;\n    tcp_nodelay on;\n    keepalive_timeout 65;\n    types_hash_max_size 2048;\n    client_max_body_size 50M;\n\n    # Gzip compression\n    gzip on;\n    gzip_vary on;\n    gzip_proxied any;\n    gzip_comp_level 6;\n    gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;\n\n    # Default server block (main Changerawr app)\n    server {\n        listen 80 default_server;\n        listen [::]:80 default_server;\n        server_name _;\n\n        location / {\n            proxy_pass http://127.0.0.1:3000;\n            proxy_http_version 1.1;\n            proxy_set_header Upgrade $http_upgrade;\n            proxy_set_header Connection 'upgrade';\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n            proxy_set_header X-Forwarded-Proto $scheme;\n            proxy_cache_bypass $http_upgrade;\n        }\n\n        # ACME challenge for main domain\n        location ^~ /.well-known/acme-challenge/ {\n            proxy_pass http://127.0.0.1:3000;\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n            proxy_set_header X-Forwarded-Proto $scheme;\n        }\n    }\n\n    # Include custom domain configurations managed by nginx-agent\n    include /etc/nginx/sites-enabled/*.conf;\n}\n"
  },
  {
    "path": "package-lock.json.backup",
    "content": "{\n  \"name\": \"changerawr\",\n  \"version\": \"0.1.0\",\n  \"lockfileVersion\": 3,\n  \"requires\": true,\n  \"packages\": {\n    \"\": {\n      \"name\": \"changerawr\",\n      \"version\": \"0.1.0\",\n      \"dependencies\": {\n        \"@changerawr/markdown\": \"^1.1.5\",\n        \"@faker-js/faker\": \"^9.5.0\",\n        \"@headlessui/react\": \"^2.2.0\",\n        \"@heroicons/react\": \"^2.2.0\",\n        \"@hookform/resolvers\": \"^4.0.0\",\n        \"@icons-pack/react-simple-icons\": \"^13.6.0\",\n        \"@mistralai/mistralai\": \"^1.5.0\",\n        \"@prisma/client\": \"^6.6.0\",\n        \"@radix-ui/react-accordion\": \"^1.2.3\",\n        \"@radix-ui/react-alert-dialog\": \"^1.1.6\",\n        \"@radix-ui/react-avatar\": \"^1.1.3\",\n        \"@radix-ui/react-checkbox\": \"^1.2.2\",\n        \"@radix-ui/react-collapsible\": \"^1.1.7\",\n        \"@radix-ui/react-dialog\": \"^1.1.6\",\n        \"@radix-ui/react-dropdown-menu\": \"^2.1.6\",\n        \"@radix-ui/react-hover-card\": \"^1.1.7\",\n        \"@radix-ui/react-icons\": \"^1.3.2\",\n        \"@radix-ui/react-label\": \"^2.1.2\",\n        \"@radix-ui/react-popover\": \"^1.1.6\",\n        \"@radix-ui/react-progress\": \"^1.1.2\",\n        \"@radix-ui/react-radio-group\": \"^1.2.3\",\n        \"@radix-ui/react-scroll-area\": \"^1.2.3\",\n        \"@radix-ui/react-select\": \"^2.1.6\",\n        \"@radix-ui/react-separator\": \"^1.1.2\",\n        \"@radix-ui/react-slider\": \"^1.3.4\",\n        \"@radix-ui/react-slot\": \"^1.1.2\",\n        \"@radix-ui/react-switch\": \"^1.1.3\",\n        \"@radix-ui/react-tabs\": \"^1.1.3\",\n        \"@radix-ui/react-toast\": \"^1.2.6\",\n        \"@radix-ui/react-tooltip\": \"^1.1.8\",\n        \"@react-email/components\": \"^0.0.36\",\n        \"@react-email/render\": \"^1.0.6\",\n        \"@scalar/nextjs-api-reference\": \"^0.9.0\",\n        \"@simplewebauthn/browser\": \"^9.0.1\",\n        \"@simplewebauthn/server\": \"^9.0.3\",\n        \"@tanstack/react-query\": \"^5.66.0\",\n        \"@tiptap/react\": \"^2.11.5\",\n        \"@tiptap/starter-kit\": \"^2.11.5\",\n        \"@types/bcryptjs\": \"^2.4.6\",\n        \"@types/md5\": \"^2.3.5\",\n        \"@types/react-syntax-highlighter\": \"^15.5.13\",\n        \"babel-plugin-react-compiler\": \"^1.0.0\",\n        \"bcryptjs\": \"^3.0.0\",\n        \"canvas-confetti\": \"^1.9.3\",\n        \"chalk\": \"^5.4.1\",\n        \"child_process\": \"^1.0.2\",\n        \"class-variance-authority\": \"^0.7.1\",\n        \"clsx\": \"^2.1.1\",\n        \"cmdk\": \"^1.1.1\",\n        \"comment-parser\": \"^1.4.1\",\n        \"compare-versions\": \"^6.1.1\",\n        \"cookies-next\": \"^5.1.0\",\n        \"date-fns\": \"^4.1.0\",\n        \"dompurify\": \"^3.2.4\",\n        \"dotenv\": \"^16.4.7\",\n        \"framer-motion\": \"^12.5.0\",\n        \"fs\": \"^0.0.1-security\",\n        \"fs-extra\": \"^11.3.0\",\n        \"glob\": \"^11.0.1\",\n        \"jose\": \"^5.9.6\",\n        \"jsdoc\": \"^4.0.4\",\n        \"jsdoc-api\": \"^9.3.4\",\n        \"lucide-react\": \"^0.475.0\",\n        \"md5\": \"^2.3.0\",\n        \"mobx\": \"^6.13.6\",\n        \"net\": \"^1.0.2\",\n        \"next\": \"^16.1.6\",\n        \"next-themes\": \"^0.4.4\",\n        \"nock\": \"^14.0.1\",\n        \"nodemailer\": \"^7.0.11\",\n        \"openapi-types\": \"^12.1.3\",\n        \"ora\": \"^8.2.0\",\n        \"path\": \"^0.12.7\",\n        \"perfect-freehand\": \"^1.2.2\",\n        \"playwright\": \"^1.53.1\",\n        \"prismjs\": \"^1.30.0\",\n        \"react\": \"^19.2.4\",\n        \"react-csv\": \"^2.2.2\",\n        \"react-day-picker\": \"^9.4.3\",\n        \"react-dnd\": \"^16.0.1\",\n        \"react-dnd-html5-backend\": \"^16.0.1\",\n        \"react-dom\": \"^19.2.4\",\n        \"react-hook-form\": \"^7.54.2\",\n        \"react-markdown\": \"^9.0.3\",\n        \"react-syntax-highlighter\": \"^16.1.0\",\n        \"recharts\": \"^2.15.4\",\n        \"rehype-highlight\": \"^7.0.2\",\n        \"rehype-katex\": \"^7.0.1\",\n        \"rehype-raw\": \"^7.0.0\",\n        \"rehype-sanitize\": \"^6.0.0\",\n        \"rehype-stringify\": \"^10.0.1\",\n        \"remark-emoji\": \"^5.0.1\",\n        \"remark-gfm\": \"^4.0.1\",\n        \"remark-math\": \"^6.0.0\",\n        \"remark-parse\": \"^11.0.0\",\n        \"remark-rehype\": \"^11.1.1\",\n        \"slackify-markdown\": \"^5.0.0\",\n        \"styled-components\": \"^6.1.15\",\n        \"tailwind-merge\": \"^3.0.1\",\n        \"tailwindcss-animate\": \"^1.0.7\",\n        \"tls\": \"^0.0.1\",\n        \"unified\": \"^11.0.5\",\n        \"use-debounce\": \"^10.0.4\",\n        \"zod\": \"^3.24.2\"\n      },\n      \"devDependencies\": {\n        \"@eslint/eslintrc\": \"^3\",\n        \"@simplewebauthn/types\": \"^12.0.0\",\n        \"@tailwindcss/forms\": \"^0.5.10\",\n        \"@types/canvas-confetti\": \"^1.9.0\",\n        \"@types/fs-extra\": \"^11.0.4\",\n        \"@types/node\": \"^20\",\n        \"@types/nodemailer\": \"^6.4.17\",\n        \"@types/react\": \"19.2.2\",\n        \"@types/react-csv\": \"^1.1.10\",\n        \"@types/react-dom\": \"19.2.2\",\n        \"@types/swagger-ui-react\": \"^5.18.0\",\n        \"baseline-browser-mapping\": \"^2.9.19\",\n        \"eslint\": \"^9\",\n        \"eslint-config-next\": \"^16.1.6\",\n        \"postcss\": \"^8\",\n        \"prisma\": \"^6.6.0\",\n        \"tailwindcss\": \"^3.4.1\",\n        \"ts-node\": \"^10.9.2\",\n        \"tsx\": \"^4.19.3\",\n        \"typescript\": \"^5\"\n      }\n    },\n    \"node_modules/@alloc/quick-lru\": {\n      \"version\": \"5.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz\",\n      \"integrity\": \"sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/@babel/code-frame\": {\n      \"version\": \"7.27.1\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz\",\n      \"integrity\": \"sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/helper-validator-identifier\": \"^7.27.1\",\n        \"js-tokens\": \"^4.0.0\",\n        \"picocolors\": \"^1.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@babel/compat-data\": {\n      \"version\": \"7.28.5\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz\",\n      \"integrity\": \"sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@babel/core\": {\n      \"version\": \"7.28.5\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz\",\n      \"integrity\": \"sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@babel/code-frame\": \"^7.27.1\",\n        \"@babel/generator\": \"^7.28.5\",\n        \"@babel/helper-compilation-targets\": \"^7.27.2\",\n        \"@babel/helper-module-transforms\": \"^7.28.3\",\n        \"@babel/helpers\": \"^7.28.4\",\n        \"@babel/parser\": \"^7.28.5\",\n        \"@babel/template\": \"^7.27.2\",\n        \"@babel/traverse\": \"^7.28.5\",\n        \"@babel/types\": \"^7.28.5\",\n        \"@jridgewell/remapping\": \"^2.3.5\",\n        \"convert-source-map\": \"^2.0.0\",\n        \"debug\": \"^4.1.0\",\n        \"gensync\": \"^1.0.0-beta.2\",\n        \"json5\": \"^2.2.3\",\n        \"semver\": \"^6.3.1\"\n      },\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/babel\"\n      }\n    },\n    \"node_modules/@babel/core/node_modules/json5\": {\n      \"version\": \"2.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/json5/-/json5-2.2.3.tgz\",\n      \"integrity\": \"sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"json5\": \"lib/cli.js\"\n      },\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/@babel/core/node_modules/semver\": {\n      \"version\": \"6.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/semver/-/semver-6.3.1.tgz\",\n      \"integrity\": \"sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==\",\n      \"dev\": true,\n      \"license\": \"ISC\",\n      \"bin\": {\n        \"semver\": \"bin/semver.js\"\n      }\n    },\n    \"node_modules/@babel/generator\": {\n      \"version\": \"7.28.5\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz\",\n      \"integrity\": \"sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/parser\": \"^7.28.5\",\n        \"@babel/types\": \"^7.28.5\",\n        \"@jridgewell/gen-mapping\": \"^0.3.12\",\n        \"@jridgewell/trace-mapping\": \"^0.3.28\",\n        \"jsesc\": \"^3.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@babel/helper-compilation-targets\": {\n      \"version\": \"7.27.2\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz\",\n      \"integrity\": \"sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/compat-data\": \"^7.27.2\",\n        \"@babel/helper-validator-option\": \"^7.27.1\",\n        \"browserslist\": \"^4.24.0\",\n        \"lru-cache\": \"^5.1.1\",\n        \"semver\": \"^6.3.1\"\n      },\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@babel/helper-compilation-targets/node_modules/lru-cache\": {\n      \"version\": \"5.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz\",\n      \"integrity\": \"sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==\",\n      \"dev\": true,\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"yallist\": \"^3.0.2\"\n      }\n    },\n    \"node_modules/@babel/helper-compilation-targets/node_modules/semver\": {\n      \"version\": \"6.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/semver/-/semver-6.3.1.tgz\",\n      \"integrity\": \"sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==\",\n      \"dev\": true,\n      \"license\": \"ISC\",\n      \"bin\": {\n        \"semver\": \"bin/semver.js\"\n      }\n    },\n    \"node_modules/@babel/helper-globals\": {\n      \"version\": \"7.28.0\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz\",\n      \"integrity\": \"sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@babel/helper-module-imports\": {\n      \"version\": \"7.27.1\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz\",\n      \"integrity\": \"sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/traverse\": \"^7.27.1\",\n        \"@babel/types\": \"^7.27.1\"\n      },\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@babel/helper-module-transforms\": {\n      \"version\": \"7.28.3\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz\",\n      \"integrity\": \"sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/helper-module-imports\": \"^7.27.1\",\n        \"@babel/helper-validator-identifier\": \"^7.27.1\",\n        \"@babel/traverse\": \"^7.28.3\"\n      },\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      },\n      \"peerDependencies\": {\n        \"@babel/core\": \"^7.0.0\"\n      }\n    },\n    \"node_modules/@babel/helper-string-parser\": {\n      \"version\": \"7.27.1\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz\",\n      \"integrity\": \"sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@babel/helper-validator-identifier\": {\n      \"version\": \"7.28.5\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz\",\n      \"integrity\": \"sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@babel/helper-validator-option\": {\n      \"version\": \"7.27.1\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz\",\n      \"integrity\": \"sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@babel/helpers\": {\n      \"version\": \"7.28.4\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz\",\n      \"integrity\": \"sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/template\": \"^7.27.2\",\n        \"@babel/types\": \"^7.28.4\"\n      },\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@babel/parser\": {\n      \"version\": \"7.28.5\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz\",\n      \"integrity\": \"sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/types\": \"^7.28.5\"\n      },\n      \"bin\": {\n        \"parser\": \"bin/babel-parser.js\"\n      },\n      \"engines\": {\n        \"node\": \">=6.0.0\"\n      }\n    },\n    \"node_modules/@babel/runtime\": {\n      \"version\": \"7.28.4\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz\",\n      \"integrity\": \"sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@babel/template\": {\n      \"version\": \"7.27.2\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz\",\n      \"integrity\": \"sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/code-frame\": \"^7.27.1\",\n        \"@babel/parser\": \"^7.27.2\",\n        \"@babel/types\": \"^7.27.1\"\n      },\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@babel/traverse\": {\n      \"version\": \"7.28.5\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz\",\n      \"integrity\": \"sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/code-frame\": \"^7.27.1\",\n        \"@babel/generator\": \"^7.28.5\",\n        \"@babel/helper-globals\": \"^7.28.0\",\n        \"@babel/parser\": \"^7.28.5\",\n        \"@babel/template\": \"^7.27.2\",\n        \"@babel/types\": \"^7.28.5\",\n        \"debug\": \"^4.3.1\"\n      },\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@babel/types\": {\n      \"version\": \"7.28.5\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz\",\n      \"integrity\": \"sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/helper-string-parser\": \"^7.27.1\",\n        \"@babel/helper-validator-identifier\": \"^7.28.5\"\n      },\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@changerawr/markdown\": {\n      \"version\": \"1.1.10\",\n      \"resolved\": \"https://registry.npmjs.org/@changerawr/markdown/-/markdown-1.1.10.tgz\",\n      \"integrity\": \"sha512-4cgURp8uzmricYS+SK/6Gc4cIPt4UpEFu8xeybVcGwJGxI7t8FEXlIjYIawZDHNTFSI7Vnq/w5rpEGmen8nIyw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@testing-library/react\": \"^16.3.0\",\n        \"dompurify\": \"^3.0.8\"\n      },\n      \"engines\": {\n        \"node\": \">=16.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \">=16.8.0\",\n        \"react-dom\": \">=16.8.0\",\n        \"tailwindcss\": \">=3.0.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"react\": {\n          \"optional\": true\n        },\n        \"react-dom\": {\n          \"optional\": true\n        },\n        \"tailwindcss\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@cspotcode/source-map-support\": {\n      \"version\": \"0.8.1\",\n      \"resolved\": \"https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz\",\n      \"integrity\": \"sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==\",\n      \"devOptional\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@jridgewell/trace-mapping\": \"0.3.9\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping\": {\n      \"version\": \"0.3.9\",\n      \"resolved\": \"https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz\",\n      \"integrity\": \"sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==\",\n      \"devOptional\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@jridgewell/resolve-uri\": \"^3.0.3\",\n        \"@jridgewell/sourcemap-codec\": \"^1.4.10\"\n      }\n    },\n    \"node_modules/@date-fns/tz\": {\n      \"version\": \"1.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz\",\n      \"integrity\": \"sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@emnapi/core\": {\n      \"version\": \"1.4.3\",\n      \"resolved\": \"https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz\",\n      \"integrity\": \"sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"@emnapi/wasi-threads\": \"1.0.2\",\n        \"tslib\": \"^2.4.0\"\n      }\n    },\n    \"node_modules/@emnapi/runtime\": {\n      \"version\": \"1.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz\",\n      \"integrity\": \"sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==\",\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"tslib\": \"^2.4.0\"\n      }\n    },\n    \"node_modules/@emnapi/wasi-threads\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz\",\n      \"integrity\": \"sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"tslib\": \"^2.4.0\"\n      }\n    },\n    \"node_modules/@emotion/is-prop-valid\": {\n      \"version\": \"1.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz\",\n      \"integrity\": \"sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@emotion/memoize\": \"^0.8.1\"\n      }\n    },\n    \"node_modules/@emotion/memoize\": {\n      \"version\": \"0.8.1\",\n      \"resolved\": \"https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz\",\n      \"integrity\": \"sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@emotion/unitless\": {\n      \"version\": \"0.8.1\",\n      \"resolved\": \"https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz\",\n      \"integrity\": \"sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@esbuild/aix-ppc64\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz\",\n      \"integrity\": \"sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==\",\n      \"cpu\": [\n        \"ppc64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"aix\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/android-arm\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz\",\n      \"integrity\": \"sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"android\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/android-arm64\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz\",\n      \"integrity\": \"sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"android\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/android-x64\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz\",\n      \"integrity\": \"sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"android\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/darwin-arm64\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz\",\n      \"integrity\": \"sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/darwin-x64\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz\",\n      \"integrity\": \"sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/freebsd-arm64\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz\",\n      \"integrity\": \"sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"freebsd\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/freebsd-x64\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz\",\n      \"integrity\": \"sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"freebsd\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/linux-arm\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz\",\n      \"integrity\": \"sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/linux-arm64\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz\",\n      \"integrity\": \"sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/linux-ia32\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz\",\n      \"integrity\": \"sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==\",\n      \"cpu\": [\n        \"ia32\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/linux-loong64\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz\",\n      \"integrity\": \"sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==\",\n      \"cpu\": [\n        \"loong64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/linux-mips64el\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz\",\n      \"integrity\": \"sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==\",\n      \"cpu\": [\n        \"mips64el\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/linux-ppc64\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz\",\n      \"integrity\": \"sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==\",\n      \"cpu\": [\n        \"ppc64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/linux-riscv64\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz\",\n      \"integrity\": \"sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==\",\n      \"cpu\": [\n        \"riscv64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/linux-s390x\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz\",\n      \"integrity\": \"sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==\",\n      \"cpu\": [\n        \"s390x\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/linux-x64\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz\",\n      \"integrity\": \"sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/netbsd-arm64\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz\",\n      \"integrity\": \"sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"netbsd\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/netbsd-x64\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz\",\n      \"integrity\": \"sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"netbsd\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/openbsd-arm64\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz\",\n      \"integrity\": \"sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"openbsd\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/openbsd-x64\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz\",\n      \"integrity\": \"sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"openbsd\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/sunos-x64\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz\",\n      \"integrity\": \"sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"sunos\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/win32-arm64\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz\",\n      \"integrity\": \"sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/win32-ia32\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz\",\n      \"integrity\": \"sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==\",\n      \"cpu\": [\n        \"ia32\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/win32-x64\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz\",\n      \"integrity\": \"sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@eslint-community/eslint-utils\": {\n      \"version\": \"4.9.1\",\n      \"resolved\": \"https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz\",\n      \"integrity\": \"sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"eslint-visitor-keys\": \"^3.4.3\"\n      },\n      \"engines\": {\n        \"node\": \"^12.22.0 || ^14.17.0 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^6.0.0 || ^7.0.0 || >=8.0.0\"\n      }\n    },\n    \"node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys\": {\n      \"version\": \"3.4.3\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz\",\n      \"integrity\": \"sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \"^12.22.0 || ^14.17.0 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/@eslint-community/regexpp\": {\n      \"version\": \"4.12.2\",\n      \"resolved\": \"https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz\",\n      \"integrity\": \"sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^12.0.0 || ^14.0.0 || >=16.0.0\"\n      }\n    },\n    \"node_modules/@eslint/config-array\": {\n      \"version\": \"0.21.1\",\n      \"resolved\": \"https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz\",\n      \"integrity\": \"sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@eslint/object-schema\": \"^2.1.7\",\n        \"debug\": \"^4.3.1\",\n        \"minimatch\": \"^3.1.2\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      }\n    },\n    \"node_modules/@eslint/config-helpers\": {\n      \"version\": \"0.4.2\",\n      \"resolved\": \"https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz\",\n      \"integrity\": \"sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@eslint/core\": \"^0.17.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      }\n    },\n    \"node_modules/@eslint/core\": {\n      \"version\": \"0.17.0\",\n      \"resolved\": \"https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz\",\n      \"integrity\": \"sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@types/json-schema\": \"^7.0.15\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      }\n    },\n    \"node_modules/@eslint/eslintrc\": {\n      \"version\": \"3.3.3\",\n      \"resolved\": \"https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz\",\n      \"integrity\": \"sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ajv\": \"^6.12.4\",\n        \"debug\": \"^4.3.2\",\n        \"espree\": \"^10.0.1\",\n        \"globals\": \"^14.0.0\",\n        \"ignore\": \"^5.2.0\",\n        \"import-fresh\": \"^3.2.1\",\n        \"js-yaml\": \"^4.1.1\",\n        \"minimatch\": \"^3.1.2\",\n        \"strip-json-comments\": \"^3.1.1\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/@eslint/js\": {\n      \"version\": \"9.39.3\",\n      \"resolved\": \"https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz\",\n      \"integrity\": \"sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://eslint.org/donate\"\n      }\n    },\n    \"node_modules/@eslint/object-schema\": {\n      \"version\": \"2.1.7\",\n      \"resolved\": \"https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz\",\n      \"integrity\": \"sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      }\n    },\n    \"node_modules/@eslint/plugin-kit\": {\n      \"version\": \"0.4.1\",\n      \"resolved\": \"https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz\",\n      \"integrity\": \"sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@eslint/core\": \"^0.17.0\",\n        \"levn\": \"^0.4.1\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      }\n    },\n    \"node_modules/@faker-js/faker\": {\n      \"version\": \"9.8.0\",\n      \"resolved\": \"https://registry.npmjs.org/@faker-js/faker/-/faker-9.8.0.tgz\",\n      \"integrity\": \"sha512-U9wpuSrJC93jZBxx/Qq2wPjCuYISBueyVUGK7qqdmj7r/nxaxwW8AQDCLeRO7wZnjj94sh3p246cAYjUKuqgfg==\",\n      \"funding\": [\n        {\n          \"type\": \"opencollective\",\n          \"url\": \"https://opencollective.com/fakerjs\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18.0.0\",\n        \"npm\": \">=9.0.0\"\n      }\n    },\n    \"node_modules/@floating-ui/core\": {\n      \"version\": \"1.7.2\",\n      \"resolved\": \"https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz\",\n      \"integrity\": \"sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@floating-ui/utils\": \"^0.2.10\"\n      }\n    },\n    \"node_modules/@floating-ui/dom\": {\n      \"version\": \"1.7.2\",\n      \"resolved\": \"https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz\",\n      \"integrity\": \"sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@floating-ui/core\": \"^1.7.2\",\n        \"@floating-ui/utils\": \"^0.2.10\"\n      }\n    },\n    \"node_modules/@floating-ui/react\": {\n      \"version\": \"0.26.28\",\n      \"resolved\": \"https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz\",\n      \"integrity\": \"sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@floating-ui/react-dom\": \"^2.1.2\",\n        \"@floating-ui/utils\": \"^0.2.8\",\n        \"tabbable\": \"^6.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \">=16.8.0\",\n        \"react-dom\": \">=16.8.0\"\n      }\n    },\n    \"node_modules/@floating-ui/react-dom\": {\n      \"version\": \"2.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz\",\n      \"integrity\": \"sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@floating-ui/dom\": \"^1.7.2\"\n      },\n      \"peerDependencies\": {\n        \"react\": \">=16.8.0\",\n        \"react-dom\": \">=16.8.0\"\n      }\n    },\n    \"node_modules/@floating-ui/utils\": {\n      \"version\": \"0.2.10\",\n      \"resolved\": \"https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz\",\n      \"integrity\": \"sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@headlessui/react\": {\n      \"version\": \"2.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/@headlessui/react/-/react-2.2.4.tgz\",\n      \"integrity\": \"sha512-lz+OGcAH1dK93rgSMzXmm1qKOJkBUqZf1L4M8TWLNplftQD3IkoEDdUFNfAn4ylsN6WOTVtWaLmvmaHOUk1dTA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@floating-ui/react\": \"^0.26.16\",\n        \"@react-aria/focus\": \"^3.20.2\",\n        \"@react-aria/interactions\": \"^3.25.0\",\n        \"@tanstack/react-virtual\": \"^3.13.9\",\n        \"use-sync-external-store\": \"^1.5.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18 || ^19 || ^19.0.0-rc\",\n        \"react-dom\": \"^18 || ^19 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@heroicons/react\": {\n      \"version\": \"2.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz\",\n      \"integrity\": \"sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==\",\n      \"license\": \"MIT\",\n      \"peerDependencies\": {\n        \"react\": \">= 16 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@hexagon/base64\": {\n      \"version\": \"1.1.28\",\n      \"resolved\": \"https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz\",\n      \"integrity\": \"sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@hookform/resolvers\": {\n      \"version\": \"4.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.3.tgz\",\n      \"integrity\": \"sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@standard-schema/utils\": \"^0.3.0\"\n      },\n      \"peerDependencies\": {\n        \"react-hook-form\": \"^7.0.0\"\n      }\n    },\n    \"node_modules/@humanfs/core\": {\n      \"version\": \"0.19.1\",\n      \"resolved\": \"https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz\",\n      \"integrity\": \"sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \">=18.18.0\"\n      }\n    },\n    \"node_modules/@humanfs/node\": {\n      \"version\": \"0.16.6\",\n      \"resolved\": \"https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz\",\n      \"integrity\": \"sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@humanfs/core\": \"^0.19.1\",\n        \"@humanwhocodes/retry\": \"^0.3.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18.18.0\"\n      }\n    },\n    \"node_modules/@humanfs/node/node_modules/@humanwhocodes/retry\": {\n      \"version\": \"0.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz\",\n      \"integrity\": \"sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \">=18.18\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/nzakas\"\n      }\n    },\n    \"node_modules/@humanwhocodes/module-importer\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz\",\n      \"integrity\": \"sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \">=12.22\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/nzakas\"\n      }\n    },\n    \"node_modules/@humanwhocodes/retry\": {\n      \"version\": \"0.4.3\",\n      \"resolved\": \"https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz\",\n      \"integrity\": \"sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \">=18.18\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/nzakas\"\n      }\n    },\n    \"node_modules/@icons-pack/react-simple-icons\": {\n      \"version\": \"13.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/@icons-pack/react-simple-icons/-/react-simple-icons-13.6.0.tgz\",\n      \"integrity\": \"sha512-q4FySfJfM3Mv8jdF+Pq9zUmsiJe1IP4zbwFjsTK7uirqS+yeroadd40mAXphiQq90f5NHLFTfFkRiJDCLZS8/A==\",\n      \"license\": \"MIT\",\n      \"peerDependencies\": {\n        \"react\": \"^16.13 || ^17 || ^18 || ^19\"\n      }\n    },\n    \"node_modules/@img/colour\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz\",\n      \"integrity\": \"sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==\",\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@img/sharp-darwin-arm64\": {\n      \"version\": \"0.34.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz\",\n      \"integrity\": \"sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-libvips-darwin-arm64\": \"1.2.3\"\n      }\n    },\n    \"node_modules/@img/sharp-darwin-x64\": {\n      \"version\": \"0.34.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz\",\n      \"integrity\": \"sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-libvips-darwin-x64\": \"1.2.3\"\n      }\n    },\n    \"node_modules/@img/sharp-libvips-darwin-arm64\": {\n      \"version\": \"1.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz\",\n      \"integrity\": \"sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-libvips-darwin-x64\": {\n      \"version\": \"1.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz\",\n      \"integrity\": \"sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-libvips-linux-arm\": {\n      \"version\": \"1.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz\",\n      \"integrity\": \"sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"license\": \"LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-libvips-linux-arm64\": {\n      \"version\": \"1.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz\",\n      \"integrity\": \"sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-libvips-linux-ppc64\": {\n      \"version\": \"1.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz\",\n      \"integrity\": \"sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==\",\n      \"cpu\": [\n        \"ppc64\"\n      ],\n      \"license\": \"LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-libvips-linux-s390x\": {\n      \"version\": \"1.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz\",\n      \"integrity\": \"sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==\",\n      \"cpu\": [\n        \"s390x\"\n      ],\n      \"license\": \"LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-libvips-linux-x64\": {\n      \"version\": \"1.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz\",\n      \"integrity\": \"sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-libvips-linuxmusl-arm64\": {\n      \"version\": \"1.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz\",\n      \"integrity\": \"sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-libvips-linuxmusl-x64\": {\n      \"version\": \"1.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz\",\n      \"integrity\": \"sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-linux-arm\": {\n      \"version\": \"0.34.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz\",\n      \"integrity\": \"sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-libvips-linux-arm\": \"1.2.3\"\n      }\n    },\n    \"node_modules/@img/sharp-linux-arm64\": {\n      \"version\": \"0.34.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz\",\n      \"integrity\": \"sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-libvips-linux-arm64\": \"1.2.3\"\n      }\n    },\n    \"node_modules/@img/sharp-linux-ppc64\": {\n      \"version\": \"0.34.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz\",\n      \"integrity\": \"sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==\",\n      \"cpu\": [\n        \"ppc64\"\n      ],\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-libvips-linux-ppc64\": \"1.2.3\"\n      }\n    },\n    \"node_modules/@img/sharp-linux-s390x\": {\n      \"version\": \"0.34.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz\",\n      \"integrity\": \"sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==\",\n      \"cpu\": [\n        \"s390x\"\n      ],\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-libvips-linux-s390x\": \"1.2.3\"\n      }\n    },\n    \"node_modules/@img/sharp-linux-x64\": {\n      \"version\": \"0.34.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz\",\n      \"integrity\": \"sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-libvips-linux-x64\": \"1.2.3\"\n      }\n    },\n    \"node_modules/@img/sharp-linuxmusl-arm64\": {\n      \"version\": \"0.34.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz\",\n      \"integrity\": \"sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-libvips-linuxmusl-arm64\": \"1.2.3\"\n      }\n    },\n    \"node_modules/@img/sharp-linuxmusl-x64\": {\n      \"version\": \"0.34.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz\",\n      \"integrity\": \"sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-libvips-linuxmusl-x64\": \"1.2.3\"\n      }\n    },\n    \"node_modules/@img/sharp-wasm32\": {\n      \"version\": \"0.34.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz\",\n      \"integrity\": \"sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==\",\n      \"cpu\": [\n        \"wasm32\"\n      ],\n      \"license\": \"Apache-2.0 AND LGPL-3.0-or-later AND MIT\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"@emnapi/runtime\": \"^1.5.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-win32-arm64\": {\n      \"version\": \"0.34.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz\",\n      \"integrity\": \"sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"Apache-2.0 AND LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-win32-ia32\": {\n      \"version\": \"0.34.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz\",\n      \"integrity\": \"sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==\",\n      \"cpu\": [\n        \"ia32\"\n      ],\n      \"license\": \"Apache-2.0 AND LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-win32-x64\": {\n      \"version\": \"0.34.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz\",\n      \"integrity\": \"sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"Apache-2.0 AND LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@isaacs/cliui\": {\n      \"version\": \"8.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz\",\n      \"integrity\": \"sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"string-width\": \"^5.1.2\",\n        \"string-width-cjs\": \"npm:string-width@^4.2.0\",\n        \"strip-ansi\": \"^7.0.1\",\n        \"strip-ansi-cjs\": \"npm:strip-ansi@^6.0.1\",\n        \"wrap-ansi\": \"^8.1.0\",\n        \"wrap-ansi-cjs\": \"npm:wrap-ansi@^7.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@jridgewell/gen-mapping\": {\n      \"version\": \"0.3.13\",\n      \"resolved\": \"https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz\",\n      \"integrity\": \"sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@jridgewell/sourcemap-codec\": \"^1.5.0\",\n        \"@jridgewell/trace-mapping\": \"^0.3.24\"\n      }\n    },\n    \"node_modules/@jridgewell/remapping\": {\n      \"version\": \"2.3.5\",\n      \"resolved\": \"https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz\",\n      \"integrity\": \"sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@jridgewell/gen-mapping\": \"^0.3.5\",\n        \"@jridgewell/trace-mapping\": \"^0.3.24\"\n      }\n    },\n    \"node_modules/@jridgewell/resolve-uri\": {\n      \"version\": \"3.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz\",\n      \"integrity\": \"sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6.0.0\"\n      }\n    },\n    \"node_modules/@jridgewell/sourcemap-codec\": {\n      \"version\": \"1.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz\",\n      \"integrity\": \"sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@jridgewell/trace-mapping\": {\n      \"version\": \"0.3.31\",\n      \"resolved\": \"https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz\",\n      \"integrity\": \"sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@jridgewell/resolve-uri\": \"^3.1.0\",\n        \"@jridgewell/sourcemap-codec\": \"^1.4.14\"\n      }\n    },\n    \"node_modules/@jsdoc/salty\": {\n      \"version\": \"0.2.9\",\n      \"resolved\": \"https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz\",\n      \"integrity\": \"sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==\",\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"lodash\": \"^4.17.21\"\n      },\n      \"engines\": {\n        \"node\": \">=v12.0.0\"\n      }\n    },\n    \"node_modules/@levischuck/tiny-cbor\": {\n      \"version\": \"0.2.11\",\n      \"resolved\": \"https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz\",\n      \"integrity\": \"sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@mistralai/mistralai\": {\n      \"version\": \"1.7.2\",\n      \"resolved\": \"https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.7.2.tgz\",\n      \"integrity\": \"sha512-lsBFADWVH1RRnAdSof49ZwmI+mBiaWdha9yYj87JMjp/o3d6SDvaEFpk+phDjRxAS+uVFvWD7HXk8ezhTXxRJA==\",\n      \"dependencies\": {\n        \"zod-to-json-schema\": \"^3.24.1\"\n      },\n      \"peerDependencies\": {\n        \"zod\": \">= 3\"\n      }\n    },\n    \"node_modules/@mswjs/interceptors\": {\n      \"version\": \"0.38.7\",\n      \"resolved\": \"https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.38.7.tgz\",\n      \"integrity\": \"sha512-Jkb27iSn7JPdkqlTqKfhncFfnEZsIJVYxsFbUSWEkxdIPdsyngrhoDBk0/BGD2FQcRH99vlRrkHpNTyKqI+0/w==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@open-draft/deferred-promise\": \"^2.2.0\",\n        \"@open-draft/logger\": \"^0.3.0\",\n        \"@open-draft/until\": \"^2.0.0\",\n        \"is-node-process\": \"^1.2.0\",\n        \"outvariant\": \"^1.4.3\",\n        \"strict-event-emitter\": \"^0.5.1\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@napi-rs/wasm-runtime\": {\n      \"version\": \"0.2.11\",\n      \"resolved\": \"https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz\",\n      \"integrity\": \"sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"@emnapi/core\": \"^1.4.3\",\n        \"@emnapi/runtime\": \"^1.4.3\",\n        \"@tybys/wasm-util\": \"^0.9.0\"\n      }\n    },\n    \"node_modules/@next/env\": {\n      \"version\": \"16.1.6\",\n      \"resolved\": \"https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz\",\n      \"integrity\": \"sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@next/eslint-plugin-next\": {\n      \"version\": \"16.1.6\",\n      \"resolved\": \"https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.6.tgz\",\n      \"integrity\": \"sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"fast-glob\": \"3.3.1\"\n      }\n    },\n    \"node_modules/@next/swc-darwin-arm64\": {\n      \"version\": \"16.1.6\",\n      \"resolved\": \"https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz\",\n      \"integrity\": \"sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@next/swc-darwin-x64\": {\n      \"version\": \"16.1.6\",\n      \"resolved\": \"https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz\",\n      \"integrity\": \"sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@next/swc-linux-arm64-gnu\": {\n      \"version\": \"16.1.6\",\n      \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz\",\n      \"integrity\": \"sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@next/swc-linux-arm64-musl\": {\n      \"version\": \"16.1.6\",\n      \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz\",\n      \"integrity\": \"sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@next/swc-linux-x64-gnu\": {\n      \"version\": \"16.1.6\",\n      \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz\",\n      \"integrity\": \"sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@next/swc-linux-x64-musl\": {\n      \"version\": \"16.1.6\",\n      \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz\",\n      \"integrity\": \"sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@next/swc-win32-arm64-msvc\": {\n      \"version\": \"16.1.6\",\n      \"resolved\": \"https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz\",\n      \"integrity\": \"sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@next/swc-win32-x64-msvc\": {\n      \"version\": \"16.1.6\",\n      \"resolved\": \"https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz\",\n      \"integrity\": \"sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@nodelib/fs.scandir\": {\n      \"version\": \"2.1.5\",\n      \"resolved\": \"https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz\",\n      \"integrity\": \"sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@nodelib/fs.stat\": \"2.0.5\",\n        \"run-parallel\": \"^1.1.9\"\n      },\n      \"engines\": {\n        \"node\": \">= 8\"\n      }\n    },\n    \"node_modules/@nodelib/fs.stat\": {\n      \"version\": \"2.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz\",\n      \"integrity\": \"sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 8\"\n      }\n    },\n    \"node_modules/@nodelib/fs.walk\": {\n      \"version\": \"1.2.8\",\n      \"resolved\": \"https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz\",\n      \"integrity\": \"sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@nodelib/fs.scandir\": \"2.1.5\",\n        \"fastq\": \"^1.6.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 8\"\n      }\n    },\n    \"node_modules/@nolyfill/is-core-module\": {\n      \"version\": \"1.0.39\",\n      \"resolved\": \"https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz\",\n      \"integrity\": \"sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12.4.0\"\n      }\n    },\n    \"node_modules/@open-draft/deferred-promise\": {\n      \"version\": \"2.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz\",\n      \"integrity\": \"sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@open-draft/logger\": {\n      \"version\": \"0.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz\",\n      \"integrity\": \"sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"is-node-process\": \"^1.2.0\",\n        \"outvariant\": \"^1.4.0\"\n      }\n    },\n    \"node_modules/@open-draft/until\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz\",\n      \"integrity\": \"sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@peculiar/asn1-android\": {\n      \"version\": \"2.3.16\",\n      \"resolved\": \"https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.3.16.tgz\",\n      \"integrity\": \"sha512-a1viIv3bIahXNssrOIkXZIlI2ePpZaNmR30d4aBL99mu2rO+mT9D6zBsp7H6eROWGtmwv0Ionp5olJurIo09dw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@peculiar/asn1-schema\": \"^2.3.15\",\n        \"asn1js\": \"^3.0.5\",\n        \"tslib\": \"^2.8.1\"\n      }\n    },\n    \"node_modules/@peculiar/asn1-ecc\": {\n      \"version\": \"2.3.15\",\n      \"resolved\": \"https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.15.tgz\",\n      \"integrity\": \"sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@peculiar/asn1-schema\": \"^2.3.15\",\n        \"@peculiar/asn1-x509\": \"^2.3.15\",\n        \"asn1js\": \"^3.0.5\",\n        \"tslib\": \"^2.8.1\"\n      }\n    },\n    \"node_modules/@peculiar/asn1-rsa\": {\n      \"version\": \"2.3.15\",\n      \"resolved\": \"https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.15.tgz\",\n      \"integrity\": \"sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@peculiar/asn1-schema\": \"^2.3.15\",\n        \"@peculiar/asn1-x509\": \"^2.3.15\",\n        \"asn1js\": \"^3.0.5\",\n        \"tslib\": \"^2.8.1\"\n      }\n    },\n    \"node_modules/@peculiar/asn1-schema\": {\n      \"version\": \"2.3.15\",\n      \"resolved\": \"https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.15.tgz\",\n      \"integrity\": \"sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"asn1js\": \"^3.0.5\",\n        \"pvtsutils\": \"^1.3.6\",\n        \"tslib\": \"^2.8.1\"\n      }\n    },\n    \"node_modules/@peculiar/asn1-x509\": {\n      \"version\": \"2.3.15\",\n      \"resolved\": \"https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.15.tgz\",\n      \"integrity\": \"sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@peculiar/asn1-schema\": \"^2.3.15\",\n        \"asn1js\": \"^3.0.5\",\n        \"pvtsutils\": \"^1.3.6\",\n        \"tslib\": \"^2.8.1\"\n      }\n    },\n    \"node_modules/@popperjs/core\": {\n      \"version\": \"2.11.8\",\n      \"resolved\": \"https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz\",\n      \"integrity\": \"sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/popperjs\"\n      }\n    },\n    \"node_modules/@prisma/client\": {\n      \"version\": \"6.10.1\",\n      \"resolved\": \"https://registry.npmjs.org/@prisma/client/-/client-6.10.1.tgz\",\n      \"integrity\": \"sha512-Re4pMlcUsQsUTAYMK7EJ4Bw2kg3WfZAAlr8GjORJaK4VOP6LxRQUQ1TuLnxcF42XqGkWQ36q5CQF1yVadANQ6w==\",\n      \"hasInstallScript\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \">=18.18\"\n      },\n      \"peerDependencies\": {\n        \"prisma\": \"*\",\n        \"typescript\": \">=5.1.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"prisma\": {\n          \"optional\": true\n        },\n        \"typescript\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@prisma/config\": {\n      \"version\": \"6.10.1\",\n      \"resolved\": \"https://registry.npmjs.org/@prisma/config/-/config-6.10.1.tgz\",\n      \"integrity\": \"sha512-kz4/bnqrOrzWo8KzYguN0cden4CzLJJ+2VSpKtF8utHS3l1JS0Lhv6BLwpOX6X9yNreTbZQZwewb+/BMPDCIYQ==\",\n      \"devOptional\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"jiti\": \"2.4.2\"\n      }\n    },\n    \"node_modules/@prisma/debug\": {\n      \"version\": \"6.10.1\",\n      \"resolved\": \"https://registry.npmjs.org/@prisma/debug/-/debug-6.10.1.tgz\",\n      \"integrity\": \"sha512-k2YT53cWxv9OLjW4zSYTZ6Z7j0gPfCzcr2Mj99qsuvlxr8WAKSZ2NcSR0zLf/mP4oxnYG842IMj3utTgcd7CaA==\",\n      \"devOptional\": true,\n      \"license\": \"Apache-2.0\"\n    },\n    \"node_modules/@prisma/engines\": {\n      \"version\": \"6.10.1\",\n      \"resolved\": \"https://registry.npmjs.org/@prisma/engines/-/engines-6.10.1.tgz\",\n      \"integrity\": \"sha512-Q07P5rS2iPwk2IQr/rUQJ42tHjpPyFcbiH7PXZlV81Ryr9NYIgdxcUrwgVOWVm5T7ap02C0dNd1dpnNcSWig8A==\",\n      \"devOptional\": true,\n      \"hasInstallScript\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@prisma/debug\": \"6.10.1\",\n        \"@prisma/engines-version\": \"6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c\",\n        \"@prisma/fetch-engine\": \"6.10.1\",\n        \"@prisma/get-platform\": \"6.10.1\"\n      }\n    },\n    \"node_modules/@prisma/engines-version\": {\n      \"version\": \"6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c\",\n      \"resolved\": \"https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c.tgz\",\n      \"integrity\": \"sha512-ZJFTsEqapiTYVzXya6TUKYDFnSWCNegfUiG5ik9fleQva5Sk3DNyyUi7X1+0ZxWFHwHDr6BZV5Vm+iwP+LlciA==\",\n      \"devOptional\": true,\n      \"license\": \"Apache-2.0\"\n    },\n    \"node_modules/@prisma/fetch-engine\": {\n      \"version\": \"6.10.1\",\n      \"resolved\": \"https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.10.1.tgz\",\n      \"integrity\": \"sha512-clmbG/Jgmrc/n6Y77QcBmAUlq9LrwI9Dbgy4pq5jeEARBpRCWJDJ7PWW1P8p0LfFU0i5fsyO7FqRzRB8mkdS4g==\",\n      \"devOptional\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@prisma/debug\": \"6.10.1\",\n        \"@prisma/engines-version\": \"6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c\",\n        \"@prisma/get-platform\": \"6.10.1\"\n      }\n    },\n    \"node_modules/@prisma/get-platform\": {\n      \"version\": \"6.10.1\",\n      \"resolved\": \"https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.10.1.tgz\",\n      \"integrity\": \"sha512-4CY5ndKylcsce9Mv+VWp5obbR2/86SHOLVV053pwIkhVtT9C9A83yqiqI/5kJM9T1v1u1qco/bYjDKycmei9HA==\",\n      \"devOptional\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@prisma/debug\": \"6.10.1\"\n      }\n    },\n    \"node_modules/@radix-ui/number\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz\",\n      \"integrity\": \"sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@radix-ui/primitive\": {\n      \"version\": \"1.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz\",\n      \"integrity\": \"sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@radix-ui/react-accordion\": {\n      \"version\": \"1.2.11\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.11.tgz\",\n      \"integrity\": \"sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/primitive\": \"1.1.2\",\n        \"@radix-ui/react-collapsible\": \"1.1.11\",\n        \"@radix-ui/react-collection\": \"1.1.7\",\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-direction\": \"1.1.1\",\n        \"@radix-ui/react-id\": \"1.1.1\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-use-controllable-state\": \"1.2.2\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-alert-dialog\": {\n      \"version\": \"1.1.14\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.14.tgz\",\n      \"integrity\": \"sha512-IOZfZ3nPvN6lXpJTBCunFQPRSvK8MDgSc1FB85xnIpUKOw9en0dJj8JmCAxV7BiZdtYlUpmrQjoTFkVYtdoWzQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/primitive\": \"1.1.2\",\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-dialog\": \"1.1.14\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-slot\": \"1.2.3\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-arrow\": {\n      \"version\": \"1.1.7\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz\",\n      \"integrity\": \"sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/react-primitive\": \"2.1.3\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-avatar\": {\n      \"version\": \"1.1.10\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz\",\n      \"integrity\": \"sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-use-callback-ref\": \"1.1.1\",\n        \"@radix-ui/react-use-is-hydrated\": \"0.1.0\",\n        \"@radix-ui/react-use-layout-effect\": \"1.1.1\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-checkbox\": {\n      \"version\": \"1.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz\",\n      \"integrity\": \"sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/primitive\": \"1.1.2\",\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-presence\": \"1.1.4\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-use-controllable-state\": \"1.2.2\",\n        \"@radix-ui/react-use-previous\": \"1.1.1\",\n        \"@radix-ui/react-use-size\": \"1.1.1\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-collapsible\": {\n      \"version\": \"1.1.11\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz\",\n      \"integrity\": \"sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/primitive\": \"1.1.2\",\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-id\": \"1.1.1\",\n        \"@radix-ui/react-presence\": \"1.1.4\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-use-controllable-state\": \"1.2.2\",\n        \"@radix-ui/react-use-layout-effect\": \"1.1.1\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-collection\": {\n      \"version\": \"1.1.7\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz\",\n      \"integrity\": \"sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-slot\": \"1.2.3\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-compose-refs\": {\n      \"version\": \"1.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz\",\n      \"integrity\": \"sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==\",\n      \"license\": \"MIT\",\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-context\": {\n      \"version\": \"1.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz\",\n      \"integrity\": \"sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==\",\n      \"license\": \"MIT\",\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-dialog\": {\n      \"version\": \"1.1.14\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz\",\n      \"integrity\": \"sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/primitive\": \"1.1.2\",\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-dismissable-layer\": \"1.1.10\",\n        \"@radix-ui/react-focus-guards\": \"1.1.2\",\n        \"@radix-ui/react-focus-scope\": \"1.1.7\",\n        \"@radix-ui/react-id\": \"1.1.1\",\n        \"@radix-ui/react-portal\": \"1.1.9\",\n        \"@radix-ui/react-presence\": \"1.1.4\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-slot\": \"1.2.3\",\n        \"@radix-ui/react-use-controllable-state\": \"1.2.2\",\n        \"aria-hidden\": \"^1.2.4\",\n        \"react-remove-scroll\": \"^2.6.3\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-direction\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz\",\n      \"integrity\": \"sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==\",\n      \"license\": \"MIT\",\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-dismissable-layer\": {\n      \"version\": \"1.1.10\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz\",\n      \"integrity\": \"sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/primitive\": \"1.1.2\",\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-use-callback-ref\": \"1.1.1\",\n        \"@radix-ui/react-use-escape-keydown\": \"1.1.1\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-dropdown-menu\": {\n      \"version\": \"2.1.15\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz\",\n      \"integrity\": \"sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/primitive\": \"1.1.2\",\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-id\": \"1.1.1\",\n        \"@radix-ui/react-menu\": \"2.1.15\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-use-controllable-state\": \"1.2.2\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-focus-guards\": {\n      \"version\": \"1.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz\",\n      \"integrity\": \"sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==\",\n      \"license\": \"MIT\",\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-focus-scope\": {\n      \"version\": \"1.1.7\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz\",\n      \"integrity\": \"sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-use-callback-ref\": \"1.1.1\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-hover-card\": {\n      \"version\": \"1.1.14\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.14.tgz\",\n      \"integrity\": \"sha512-CPYZ24Mhirm+g6D8jArmLzjYu4Eyg3TTUHswR26QgzXBHBe64BO/RHOJKzmF/Dxb4y4f9PKyJdwm/O/AhNkb+Q==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/primitive\": \"1.1.2\",\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-dismissable-layer\": \"1.1.10\",\n        \"@radix-ui/react-popper\": \"1.2.7\",\n        \"@radix-ui/react-portal\": \"1.1.9\",\n        \"@radix-ui/react-presence\": \"1.1.4\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-use-controllable-state\": \"1.2.2\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-icons\": {\n      \"version\": \"1.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz\",\n      \"integrity\": \"sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==\",\n      \"license\": \"MIT\",\n      \"peerDependencies\": {\n        \"react\": \"^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@radix-ui/react-id\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz\",\n      \"integrity\": \"sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/react-use-layout-effect\": \"1.1.1\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-label\": {\n      \"version\": \"2.1.7\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz\",\n      \"integrity\": \"sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/react-primitive\": \"2.1.3\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-menu\": {\n      \"version\": \"2.1.15\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz\",\n      \"integrity\": \"sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/primitive\": \"1.1.2\",\n        \"@radix-ui/react-collection\": \"1.1.7\",\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-direction\": \"1.1.1\",\n        \"@radix-ui/react-dismissable-layer\": \"1.1.10\",\n        \"@radix-ui/react-focus-guards\": \"1.1.2\",\n        \"@radix-ui/react-focus-scope\": \"1.1.7\",\n        \"@radix-ui/react-id\": \"1.1.1\",\n        \"@radix-ui/react-popper\": \"1.2.7\",\n        \"@radix-ui/react-portal\": \"1.1.9\",\n        \"@radix-ui/react-presence\": \"1.1.4\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-roving-focus\": \"1.1.10\",\n        \"@radix-ui/react-slot\": \"1.2.3\",\n        \"@radix-ui/react-use-callback-ref\": \"1.1.1\",\n        \"aria-hidden\": \"^1.2.4\",\n        \"react-remove-scroll\": \"^2.6.3\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-popover\": {\n      \"version\": \"1.1.14\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz\",\n      \"integrity\": \"sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/primitive\": \"1.1.2\",\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-dismissable-layer\": \"1.1.10\",\n        \"@radix-ui/react-focus-guards\": \"1.1.2\",\n        \"@radix-ui/react-focus-scope\": \"1.1.7\",\n        \"@radix-ui/react-id\": \"1.1.1\",\n        \"@radix-ui/react-popper\": \"1.2.7\",\n        \"@radix-ui/react-portal\": \"1.1.9\",\n        \"@radix-ui/react-presence\": \"1.1.4\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-slot\": \"1.2.3\",\n        \"@radix-ui/react-use-controllable-state\": \"1.2.2\",\n        \"aria-hidden\": \"^1.2.4\",\n        \"react-remove-scroll\": \"^2.6.3\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-popper\": {\n      \"version\": \"1.2.7\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz\",\n      \"integrity\": \"sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@floating-ui/react-dom\": \"^2.0.0\",\n        \"@radix-ui/react-arrow\": \"1.1.7\",\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-use-callback-ref\": \"1.1.1\",\n        \"@radix-ui/react-use-layout-effect\": \"1.1.1\",\n        \"@radix-ui/react-use-rect\": \"1.1.1\",\n        \"@radix-ui/react-use-size\": \"1.1.1\",\n        \"@radix-ui/rect\": \"1.1.1\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-portal\": {\n      \"version\": \"1.1.9\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz\",\n      \"integrity\": \"sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-use-layout-effect\": \"1.1.1\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-presence\": {\n      \"version\": \"1.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz\",\n      \"integrity\": \"sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-use-layout-effect\": \"1.1.1\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-primitive\": {\n      \"version\": \"2.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz\",\n      \"integrity\": \"sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/react-slot\": \"1.2.3\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-progress\": {\n      \"version\": \"1.1.7\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz\",\n      \"integrity\": \"sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-primitive\": \"2.1.3\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-radio-group\": {\n      \"version\": \"1.3.7\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.7.tgz\",\n      \"integrity\": \"sha512-9w5XhD0KPOrm92OTTE0SysH3sYzHsSTHNvZgUBo/VZ80VdYyB5RneDbc0dKpURS24IxkoFRu/hI0i4XyfFwY6g==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/primitive\": \"1.1.2\",\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-direction\": \"1.1.1\",\n        \"@radix-ui/react-presence\": \"1.1.4\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-roving-focus\": \"1.1.10\",\n        \"@radix-ui/react-use-controllable-state\": \"1.2.2\",\n        \"@radix-ui/react-use-previous\": \"1.1.1\",\n        \"@radix-ui/react-use-size\": \"1.1.1\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-roving-focus\": {\n      \"version\": \"1.1.10\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz\",\n      \"integrity\": \"sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/primitive\": \"1.1.2\",\n        \"@radix-ui/react-collection\": \"1.1.7\",\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-direction\": \"1.1.1\",\n        \"@radix-ui/react-id\": \"1.1.1\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-use-callback-ref\": \"1.1.1\",\n        \"@radix-ui/react-use-controllable-state\": \"1.2.2\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-scroll-area\": {\n      \"version\": \"1.2.9\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.9.tgz\",\n      \"integrity\": \"sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/number\": \"1.1.1\",\n        \"@radix-ui/primitive\": \"1.1.2\",\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-direction\": \"1.1.1\",\n        \"@radix-ui/react-presence\": \"1.1.4\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-use-callback-ref\": \"1.1.1\",\n        \"@radix-ui/react-use-layout-effect\": \"1.1.1\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-select\": {\n      \"version\": \"2.2.5\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz\",\n      \"integrity\": \"sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/number\": \"1.1.1\",\n        \"@radix-ui/primitive\": \"1.1.2\",\n        \"@radix-ui/react-collection\": \"1.1.7\",\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-direction\": \"1.1.1\",\n        \"@radix-ui/react-dismissable-layer\": \"1.1.10\",\n        \"@radix-ui/react-focus-guards\": \"1.1.2\",\n        \"@radix-ui/react-focus-scope\": \"1.1.7\",\n        \"@radix-ui/react-id\": \"1.1.1\",\n        \"@radix-ui/react-popper\": \"1.2.7\",\n        \"@radix-ui/react-portal\": \"1.1.9\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-slot\": \"1.2.3\",\n        \"@radix-ui/react-use-callback-ref\": \"1.1.1\",\n        \"@radix-ui/react-use-controllable-state\": \"1.2.2\",\n        \"@radix-ui/react-use-layout-effect\": \"1.1.1\",\n        \"@radix-ui/react-use-previous\": \"1.1.1\",\n        \"@radix-ui/react-visually-hidden\": \"1.2.3\",\n        \"aria-hidden\": \"^1.2.4\",\n        \"react-remove-scroll\": \"^2.6.3\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-separator\": {\n      \"version\": \"1.1.7\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz\",\n      \"integrity\": \"sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/react-primitive\": \"2.1.3\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-slider\": {\n      \"version\": \"1.3.5\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.5.tgz\",\n      \"integrity\": \"sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/number\": \"1.1.1\",\n        \"@radix-ui/primitive\": \"1.1.2\",\n        \"@radix-ui/react-collection\": \"1.1.7\",\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-direction\": \"1.1.1\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-use-controllable-state\": \"1.2.2\",\n        \"@radix-ui/react-use-layout-effect\": \"1.1.1\",\n        \"@radix-ui/react-use-previous\": \"1.1.1\",\n        \"@radix-ui/react-use-size\": \"1.1.1\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-slot\": {\n      \"version\": \"1.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz\",\n      \"integrity\": \"sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/react-compose-refs\": \"1.1.2\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-switch\": {\n      \"version\": \"1.2.5\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.5.tgz\",\n      \"integrity\": \"sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/primitive\": \"1.1.2\",\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-use-controllable-state\": \"1.2.2\",\n        \"@radix-ui/react-use-previous\": \"1.1.1\",\n        \"@radix-ui/react-use-size\": \"1.1.1\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-tabs\": {\n      \"version\": \"1.1.12\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz\",\n      \"integrity\": \"sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/primitive\": \"1.1.2\",\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-direction\": \"1.1.1\",\n        \"@radix-ui/react-id\": \"1.1.1\",\n        \"@radix-ui/react-presence\": \"1.1.4\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-roving-focus\": \"1.1.10\",\n        \"@radix-ui/react-use-controllable-state\": \"1.2.2\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-toast\": {\n      \"version\": \"1.2.14\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.14.tgz\",\n      \"integrity\": \"sha512-nAP5FBxBJGQ/YfUB+r+O6USFVkWq3gAInkxyEnmvEV5jtSbfDhfa4hwX8CraCnbjMLsE7XSf/K75l9xXY7joWg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/primitive\": \"1.1.2\",\n        \"@radix-ui/react-collection\": \"1.1.7\",\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-dismissable-layer\": \"1.1.10\",\n        \"@radix-ui/react-portal\": \"1.1.9\",\n        \"@radix-ui/react-presence\": \"1.1.4\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-use-callback-ref\": \"1.1.1\",\n        \"@radix-ui/react-use-controllable-state\": \"1.2.2\",\n        \"@radix-ui/react-use-layout-effect\": \"1.1.1\",\n        \"@radix-ui/react-visually-hidden\": \"1.2.3\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-tooltip\": {\n      \"version\": \"1.2.7\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz\",\n      \"integrity\": \"sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/primitive\": \"1.1.2\",\n        \"@radix-ui/react-compose-refs\": \"1.1.2\",\n        \"@radix-ui/react-context\": \"1.1.2\",\n        \"@radix-ui/react-dismissable-layer\": \"1.1.10\",\n        \"@radix-ui/react-id\": \"1.1.1\",\n        \"@radix-ui/react-popper\": \"1.2.7\",\n        \"@radix-ui/react-portal\": \"1.1.9\",\n        \"@radix-ui/react-presence\": \"1.1.4\",\n        \"@radix-ui/react-primitive\": \"2.1.3\",\n        \"@radix-ui/react-slot\": \"1.2.3\",\n        \"@radix-ui/react-use-controllable-state\": \"1.2.2\",\n        \"@radix-ui/react-visually-hidden\": \"1.2.3\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-use-callback-ref\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz\",\n      \"integrity\": \"sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==\",\n      \"license\": \"MIT\",\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-use-controllable-state\": {\n      \"version\": \"1.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz\",\n      \"integrity\": \"sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/react-use-effect-event\": \"0.0.2\",\n        \"@radix-ui/react-use-layout-effect\": \"1.1.1\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-use-effect-event\": {\n      \"version\": \"0.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz\",\n      \"integrity\": \"sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/react-use-layout-effect\": \"1.1.1\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-use-escape-keydown\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz\",\n      \"integrity\": \"sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/react-use-callback-ref\": \"1.1.1\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-use-is-hydrated\": {\n      \"version\": \"0.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz\",\n      \"integrity\": \"sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"use-sync-external-store\": \"^1.5.0\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-use-layout-effect\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz\",\n      \"integrity\": \"sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==\",\n      \"license\": \"MIT\",\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-use-previous\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz\",\n      \"integrity\": \"sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==\",\n      \"license\": \"MIT\",\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-use-rect\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz\",\n      \"integrity\": \"sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/rect\": \"1.1.1\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-use-size\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz\",\n      \"integrity\": \"sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/react-use-layout-effect\": \"1.1.1\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/react-visually-hidden\": {\n      \"version\": \"1.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz\",\n      \"integrity\": \"sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/react-primitive\": \"2.1.3\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"@types/react-dom\": \"*\",\n        \"react\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@radix-ui/rect\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz\",\n      \"integrity\": \"sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@react-aria/focus\": {\n      \"version\": \"3.20.5\",\n      \"resolved\": \"https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.5.tgz\",\n      \"integrity\": \"sha512-JpFtXmWQ0Oca7FcvkqgjSyo6xEP7v3oQOLUId6o0xTvm4AD5W0mU2r3lYrbhsJ+XxdUUX4AVR5473sZZ85kU4A==\",\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@react-aria/interactions\": \"^3.25.3\",\n        \"@react-aria/utils\": \"^3.29.1\",\n        \"@react-types/shared\": \"^3.30.0\",\n        \"@swc/helpers\": \"^0.5.0\",\n        \"clsx\": \"^2.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1\",\n        \"react-dom\": \"^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1\"\n      }\n    },\n    \"node_modules/@react-aria/interactions\": {\n      \"version\": \"3.25.3\",\n      \"resolved\": \"https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.3.tgz\",\n      \"integrity\": \"sha512-J1bhlrNtjPS/fe5uJQ+0c7/jiXniwa4RQlP+Emjfc/iuqpW2RhbF9ou5vROcLzWIyaW8tVMZ468J68rAs/aZ5A==\",\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@react-aria/ssr\": \"^3.9.9\",\n        \"@react-aria/utils\": \"^3.29.1\",\n        \"@react-stately/flags\": \"^3.1.2\",\n        \"@react-types/shared\": \"^3.30.0\",\n        \"@swc/helpers\": \"^0.5.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1\",\n        \"react-dom\": \"^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1\"\n      }\n    },\n    \"node_modules/@react-aria/ssr\": {\n      \"version\": \"3.9.9\",\n      \"resolved\": \"https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.9.tgz\",\n      \"integrity\": \"sha512-2P5thfjfPy/np18e5wD4WPt8ydNXhij1jwA8oehxZTFqlgVMGXzcWKxTb4RtJrLFsqPO7RUQTiY8QJk0M4Vy2g==\",\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@swc/helpers\": \"^0.5.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 12\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1\"\n      }\n    },\n    \"node_modules/@react-aria/utils\": {\n      \"version\": \"3.29.1\",\n      \"resolved\": \"https://registry.npmjs.org/@react-aria/utils/-/utils-3.29.1.tgz\",\n      \"integrity\": \"sha512-yXMFVJ73rbQ/yYE/49n5Uidjw7kh192WNN9PNQGV0Xoc7EJUlSOxqhnpHmYTyO0EotJ8fdM1fMH8durHjUSI8g==\",\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@react-aria/ssr\": \"^3.9.9\",\n        \"@react-stately/flags\": \"^3.1.2\",\n        \"@react-stately/utils\": \"^3.10.7\",\n        \"@react-types/shared\": \"^3.30.0\",\n        \"@swc/helpers\": \"^0.5.0\",\n        \"clsx\": \"^2.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1\",\n        \"react-dom\": \"^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1\"\n      }\n    },\n    \"node_modules/@react-dnd/asap\": {\n      \"version\": \"5.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz\",\n      \"integrity\": \"sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@react-dnd/invariant\": {\n      \"version\": \"4.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz\",\n      \"integrity\": \"sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@react-dnd/shallowequal\": {\n      \"version\": \"4.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz\",\n      \"integrity\": \"sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@react-email/body\": {\n      \"version\": \"0.0.11\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/body/-/body-0.0.11.tgz\",\n      \"integrity\": \"sha512-ZSD2SxVSgUjHGrB0Wi+4tu3MEpB4fYSbezsFNEJk2xCWDBkFiOeEsjTmR5dvi+CxTK691hQTQlHv0XWuP7ENTg==\",\n      \"license\": \"MIT\",\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/button\": {\n      \"version\": \"0.0.19\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/button/-/button-0.0.19.tgz\",\n      \"integrity\": \"sha512-HYHrhyVGt7rdM/ls6FuuD6XE7fa7bjZTJqB2byn6/oGsfiEZaogY77OtoLL/mrQHjHjZiJadtAMSik9XLcm7+A==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/code-block\": {\n      \"version\": \"0.0.12\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.12.tgz\",\n      \"integrity\": \"sha512-Faw3Ij9+/Qwq6moWaeHnV8Hn7ekc/EqyAzPi6yUar21dhcqYugCC4Da1x4d9nA9zC0H9KU3lYVJczh8D3cA+Eg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"prismjs\": \"1.30.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/code-inline\": {\n      \"version\": \"0.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.5.tgz\",\n      \"integrity\": \"sha512-MmAsOzdJpzsnY2cZoPHFPk6uDO/Ncpb4Kh1hAt9UZc1xOW3fIzpe1Pi9y9p6wwUmpaeeDalJxAxH6/fnTquinA==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/column\": {\n      \"version\": \"0.0.13\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/column/-/column-0.0.13.tgz\",\n      \"integrity\": \"sha512-Lqq17l7ShzJG/d3b1w/+lVO+gp2FM05ZUo/nW0rjxB8xBICXOVv6PqjDnn3FXKssvhO5qAV20lHM6S+spRhEwQ==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/components\": {\n      \"version\": \"0.0.36\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/components/-/components-0.0.36.tgz\",\n      \"integrity\": \"sha512-VMh+OQplAnG8JMLlJjdnjt+ThJZ+JVkp0q2YMS2NEz+T88N22bLD2p7DZO0QgtNaKgumOhJI/0a2Q7VzCrwu5g==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@react-email/body\": \"0.0.11\",\n        \"@react-email/button\": \"0.0.19\",\n        \"@react-email/code-block\": \"0.0.12\",\n        \"@react-email/code-inline\": \"0.0.5\",\n        \"@react-email/column\": \"0.0.13\",\n        \"@react-email/container\": \"0.0.15\",\n        \"@react-email/font\": \"0.0.9\",\n        \"@react-email/head\": \"0.0.12\",\n        \"@react-email/heading\": \"0.0.15\",\n        \"@react-email/hr\": \"0.0.11\",\n        \"@react-email/html\": \"0.0.11\",\n        \"@react-email/img\": \"0.0.11\",\n        \"@react-email/link\": \"0.0.12\",\n        \"@react-email/markdown\": \"0.0.14\",\n        \"@react-email/preview\": \"0.0.12\",\n        \"@react-email/render\": \"1.0.6\",\n        \"@react-email/row\": \"0.0.12\",\n        \"@react-email/section\": \"0.0.16\",\n        \"@react-email/tailwind\": \"1.0.4\",\n        \"@react-email/text\": \"0.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/components/node_modules/@react-email/render\": {\n      \"version\": \"1.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/render/-/render-1.0.6.tgz\",\n      \"integrity\": \"sha512-zNueW5Wn/4jNC1c5LFgXzbUdv5Lhms+FWjOvWAhal7gx5YVf0q6dPJ0dnR70+ifo59gcMLwCZEaTS9EEuUhKvQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"html-to-text\": \"9.0.5\",\n        \"prettier\": \"3.5.3\",\n        \"react-promise-suspense\": \"0.3.4\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/components/node_modules/prettier\": {\n      \"version\": \"3.5.3\",\n      \"resolved\": \"https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz\",\n      \"integrity\": \"sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==\",\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"prettier\": \"bin/prettier.cjs\"\n      },\n      \"engines\": {\n        \"node\": \">=14\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/prettier/prettier?sponsor=1\"\n      }\n    },\n    \"node_modules/@react-email/container\": {\n      \"version\": \"0.0.15\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/container/-/container-0.0.15.tgz\",\n      \"integrity\": \"sha512-Qo2IQo0ru2kZq47REmHW3iXjAQaKu4tpeq/M8m1zHIVwKduL2vYOBQWbC2oDnMtWPmkBjej6XxgtZByxM6cCFg==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/font\": {\n      \"version\": \"0.0.9\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/font/-/font-0.0.9.tgz\",\n      \"integrity\": \"sha512-4zjq23oT9APXkerqeslPH3OZWuh5X4crHK6nx82mVHV2SrLba8+8dPEnWbaACWTNjOCbcLIzaC9unk7Wq2MIXw==\",\n      \"license\": \"MIT\",\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/head\": {\n      \"version\": \"0.0.12\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/head/-/head-0.0.12.tgz\",\n      \"integrity\": \"sha512-X2Ii6dDFMF+D4niNwMAHbTkeCjlYYnMsd7edXOsi0JByxt9wNyZ9EnhFiBoQdqkE+SMDcu8TlNNttMrf5sJeMA==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/heading\": {\n      \"version\": \"0.0.15\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/heading/-/heading-0.0.15.tgz\",\n      \"integrity\": \"sha512-xF2GqsvBrp/HbRHWEfOgSfRFX+Q8I5KBEIG5+Lv3Vb2R/NYr0s8A5JhHHGf2pWBMJdbP4B2WHgj/VUrhy8dkIg==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/hr\": {\n      \"version\": \"0.0.11\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/hr/-/hr-0.0.11.tgz\",\n      \"integrity\": \"sha512-S1gZHVhwOsd1Iad5IFhpfICwNPMGPJidG/Uysy1AwmspyoAP5a4Iw3OWEpINFdgh9MHladbxcLKO2AJO+cA9Lw==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/html\": {\n      \"version\": \"0.0.11\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/html/-/html-0.0.11.tgz\",\n      \"integrity\": \"sha512-qJhbOQy5VW5qzU74AimjAR9FRFQfrMa7dn4gkEXKMB/S9xZN8e1yC1uA9C15jkXI/PzmJ0muDIWmFwatm5/+VA==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/img\": {\n      \"version\": \"0.0.11\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/img/-/img-0.0.11.tgz\",\n      \"integrity\": \"sha512-aGc8Y6U5C3igoMaqAJKsCpkbm1XjguQ09Acd+YcTKwjnC2+0w3yGUJkjWB2vTx4tN8dCqQCXO8FmdJpMfOA9EQ==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/link\": {\n      \"version\": \"0.0.12\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/link/-/link-0.0.12.tgz\",\n      \"integrity\": \"sha512-vF+xxQk2fGS1CN7UPQDbzvcBGfffr+GjTPNiWM38fhBfsLv6A/YUfaqxWlmL7zLzVmo0K2cvvV9wxlSyNba1aQ==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/markdown\": {\n      \"version\": \"0.0.14\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.14.tgz\",\n      \"integrity\": \"sha512-5IsobCyPkb4XwnQO8uFfGcNOxnsg3311GRXhJ3uKv51P7Jxme4ycC/MITnwIZ10w2zx7HIyTiqVzTj4XbuIHbg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"md-to-react-email\": \"5.0.5\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/preview\": {\n      \"version\": \"0.0.12\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/preview/-/preview-0.0.12.tgz\",\n      \"integrity\": \"sha512-g/H5fa9PQPDK6WUEG7iTlC19sAktI23qyoiJtMLqQiXFCfWeQMhqjLGKeLSKkfzszqmfJCjZtpSiKtBoOdxp3Q==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/render\": {\n      \"version\": \"1.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/render/-/render-1.1.3.tgz\",\n      \"integrity\": \"sha512-TjjF1tdTmOqYEIWWg9wMx5q9JbQRbWmnG7owQbSGEHkNfc/c/vBu7hjfrki907lgQEAkYac9KPTyIjOKhvhJCg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"html-to-text\": \"^9.0.5\",\n        \"prettier\": \"^3.5.3\",\n        \"react-promise-suspense\": \"^0.3.4\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\",\n        \"react-dom\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/row\": {\n      \"version\": \"0.0.12\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/row/-/row-0.0.12.tgz\",\n      \"integrity\": \"sha512-HkCdnEjvK3o+n0y0tZKXYhIXUNPDx+2vq1dJTmqappVHXS5tXS6W5JOPZr5j+eoZ8gY3PShI2LWj5rWF7ZEtIQ==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/section\": {\n      \"version\": \"0.0.16\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/section/-/section-0.0.16.tgz\",\n      \"integrity\": \"sha512-FjqF9xQ8FoeUZYKSdt8sMIKvoT9XF8BrzhT3xiFKdEMwYNbsDflcjfErJe3jb7Wj/es/lKTbV5QR1dnLzGpL3w==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/tailwind\": {\n      \"version\": \"1.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/tailwind/-/tailwind-1.0.4.tgz\",\n      \"integrity\": \"sha512-tJdcusncdqgvTUYZIuhNC6LYTfL9vNTSQpwWdTCQhQ1lsrNCEE4OKCSdzSV3S9F32pi0i0xQ+YPJHKIzGjdTSA==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-email/text\": {\n      \"version\": \"0.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/@react-email/text/-/text-0.1.1.tgz\",\n      \"integrity\": \"sha512-Zo9tSEzkO3fODLVH1yVhzVCiwETfeEL5wU93jXKWo2DHoMuiZ9Iabaso3T0D0UjhrCB1PBMeq2YiejqeToTyIQ==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/@react-stately/flags\": {\n      \"version\": \"3.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz\",\n      \"integrity\": \"sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==\",\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@swc/helpers\": \"^0.5.0\"\n      }\n    },\n    \"node_modules/@react-stately/utils\": {\n      \"version\": \"3.10.7\",\n      \"resolved\": \"https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.7.tgz\",\n      \"integrity\": \"sha512-cWvjGAocvy4abO9zbr6PW6taHgF24Mwy/LbQ4TC4Aq3tKdKDntxyD+sh7AkSRfJRT2ccMVaHVv2+FfHThd3PKQ==\",\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@swc/helpers\": \"^0.5.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1\"\n      }\n    },\n    \"node_modules/@react-types/shared\": {\n      \"version\": \"3.30.0\",\n      \"resolved\": \"https://registry.npmjs.org/@react-types/shared/-/shared-3.30.0.tgz\",\n      \"integrity\": \"sha512-COIazDAx1ncDg046cTJ8SFYsX8aS3lB/08LDnbkH/SkdYrFPWDlXMrO/sUam8j1WWM+PJ+4d1mj7tODIKNiFog==\",\n      \"license\": \"Apache-2.0\",\n      \"peerDependencies\": {\n        \"react\": \"^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1\"\n      }\n    },\n    \"node_modules/@remirror/core-constants\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz\",\n      \"integrity\": \"sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@rtsao/scc\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz\",\n      \"integrity\": \"sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@scalar/core\": {\n      \"version\": \"0.3.23\",\n      \"resolved\": \"https://registry.npmjs.org/@scalar/core/-/core-0.3.23.tgz\",\n      \"integrity\": \"sha512-hop7LVR3MKB2VpS8dly3gmmbB3lBGxQRtL0pBaC77zFMRHoBv1DuB2bj8l4gxd5grzitJ1LsYduvywLAMY9F6g==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@scalar/types\": \"0.5.0\"\n      },\n      \"engines\": {\n        \"node\": \">=20\"\n      }\n    },\n    \"node_modules/@scalar/nextjs-api-reference\": {\n      \"version\": \"0.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/@scalar/nextjs-api-reference/-/nextjs-api-reference-0.9.2.tgz\",\n      \"integrity\": \"sha512-yzCKZ6vRjJb9ZnApEsN1Yp9Oh89CqdnY1uGiu/IAOKlMlwOvTdORKwmhF+/sdEuf/6//rKFzA8g+8LAk0HpvyQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@scalar/core\": \"0.3.23\"\n      },\n      \"engines\": {\n        \"node\": \">=20\"\n      },\n      \"peerDependencies\": {\n        \"next\": \"^15.0.0 || ^16.0.0\",\n        \"react\": \"^19.0.0\"\n      }\n    },\n    \"node_modules/@scalar/types\": {\n      \"version\": \"0.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/@scalar/types/-/types-0.5.0.tgz\",\n      \"integrity\": \"sha512-imDMuTieOc5kHM9/Kt/1lmiI5ZtusuaYlzsXTP99IsWvD8mJ7ivF73lPBRj4PKtg4vY+ta5CO/vJpvnCYandRg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"nanoid\": \"5.1.5\",\n        \"type-fest\": \"5.0.0\",\n        \"zod\": \"4.1.11\"\n      },\n      \"engines\": {\n        \"node\": \">=20\"\n      }\n    },\n    \"node_modules/@scalar/types/node_modules/zod\": {\n      \"version\": \"4.1.11\",\n      \"resolved\": \"https://registry.npmjs.org/zod/-/zod-4.1.11.tgz\",\n      \"integrity\": \"sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/colinhacks\"\n      }\n    },\n    \"node_modules/@selderee/plugin-htmlparser2\": {\n      \"version\": \"0.11.0\",\n      \"resolved\": \"https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz\",\n      \"integrity\": \"sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"domhandler\": \"^5.0.3\",\n        \"selderee\": \"^0.11.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://ko-fi.com/killymxi\"\n      }\n    },\n    \"node_modules/@simplewebauthn/browser\": {\n      \"version\": \"9.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/@simplewebauthn/browser/-/browser-9.0.1.tgz\",\n      \"integrity\": \"sha512-wD2WpbkaEP4170s13/HUxPcAV5y4ZXaKo1TfNklS5zDefPinIgXOpgz1kpEvobAsaLPa2KeH7AKKX/od1mrBJw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@simplewebauthn/types\": \"^9.0.1\"\n      }\n    },\n    \"node_modules/@simplewebauthn/browser/node_modules/@simplewebauthn/types\": {\n      \"version\": \"9.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/@simplewebauthn/types/-/types-9.0.1.tgz\",\n      \"integrity\": \"sha512-tGSRP1QvsAvsJmnOlRQyw/mvK9gnPtjEc5fg2+m8n+QUa+D7rvrKkOYyfpy42GTs90X3RDOnqJgfHt+qO67/+w==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@simplewebauthn/server\": {\n      \"version\": \"9.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/@simplewebauthn/server/-/server-9.0.3.tgz\",\n      \"integrity\": \"sha512-FMZieoBosrVLFxCnxPFD9Enhd1U7D8nidVDT4MsHc6l4fdVcjoeHjDueeXCloO1k5O/fZg1fsSXXPKbY2XTzDA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@hexagon/base64\": \"^1.1.27\",\n        \"@levischuck/tiny-cbor\": \"^0.2.2\",\n        \"@peculiar/asn1-android\": \"^2.3.10\",\n        \"@peculiar/asn1-ecc\": \"^2.3.8\",\n        \"@peculiar/asn1-rsa\": \"^2.3.8\",\n        \"@peculiar/asn1-schema\": \"^2.3.8\",\n        \"@peculiar/asn1-x509\": \"^2.3.8\",\n        \"@simplewebauthn/types\": \"^9.0.1\",\n        \"cross-fetch\": \"^4.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=16.0.0\"\n      }\n    },\n    \"node_modules/@simplewebauthn/server/node_modules/@simplewebauthn/types\": {\n      \"version\": \"9.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/@simplewebauthn/types/-/types-9.0.1.tgz\",\n      \"integrity\": \"sha512-tGSRP1QvsAvsJmnOlRQyw/mvK9gnPtjEc5fg2+m8n+QUa+D7rvrKkOYyfpy42GTs90X3RDOnqJgfHt+qO67/+w==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@simplewebauthn/types\": {\n      \"version\": \"12.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/@simplewebauthn/types/-/types-12.0.0.tgz\",\n      \"integrity\": \"sha512-q6y8MkoV8V8jB4zzp18Uyj2I7oFp2/ONL8c3j8uT06AOWu3cIChc1au71QYHrP2b+xDapkGTiv+9lX7xkTlAsA==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@sindresorhus/is\": {\n      \"version\": \"4.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz\",\n      \"integrity\": \"sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sindresorhus/is?sponsor=1\"\n      }\n    },\n    \"node_modules/@standard-schema/utils\": {\n      \"version\": \"0.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz\",\n      \"integrity\": \"sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@swc/helpers\": {\n      \"version\": \"0.5.17\",\n      \"resolved\": \"https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz\",\n      \"integrity\": \"sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==\",\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"tslib\": \"^2.8.0\"\n      }\n    },\n    \"node_modules/@tailwindcss/forms\": {\n      \"version\": \"0.5.10\",\n      \"resolved\": \"https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz\",\n      \"integrity\": \"sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"mini-svg-data-uri\": \"^1.2.3\"\n      },\n      \"peerDependencies\": {\n        \"tailwindcss\": \">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1\"\n      }\n    },\n    \"node_modules/@tanstack/query-core\": {\n      \"version\": \"5.81.5\",\n      \"resolved\": \"https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.81.5.tgz\",\n      \"integrity\": \"sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/tannerlinsley\"\n      }\n    },\n    \"node_modules/@tanstack/react-query\": {\n      \"version\": \"5.81.5\",\n      \"resolved\": \"https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.81.5.tgz\",\n      \"integrity\": \"sha512-lOf2KqRRiYWpQT86eeeftAGnjuTR35myTP8MXyvHa81VlomoAWNEd8x5vkcAfQefu0qtYCvyqLropFZqgI2EQw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@tanstack/query-core\": \"5.81.5\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/tannerlinsley\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18 || ^19\"\n      }\n    },\n    \"node_modules/@tanstack/react-virtual\": {\n      \"version\": \"3.13.12\",\n      \"resolved\": \"https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz\",\n      \"integrity\": \"sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@tanstack/virtual-core\": \"3.13.12\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/tannerlinsley\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0\",\n        \"react-dom\": \"^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0\"\n      }\n    },\n    \"node_modules/@tanstack/virtual-core\": {\n      \"version\": \"3.13.12\",\n      \"resolved\": \"https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz\",\n      \"integrity\": \"sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/tannerlinsley\"\n      }\n    },\n    \"node_modules/@testing-library/dom\": {\n      \"version\": \"10.4.1\",\n      \"resolved\": \"https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz\",\n      \"integrity\": \"sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/code-frame\": \"^7.10.4\",\n        \"@babel/runtime\": \"^7.12.5\",\n        \"@types/aria-query\": \"^5.0.1\",\n        \"aria-query\": \"5.3.0\",\n        \"dom-accessibility-api\": \"^0.5.9\",\n        \"lz-string\": \"^1.5.0\",\n        \"picocolors\": \"1.1.1\",\n        \"pretty-format\": \"^27.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@testing-library/dom/node_modules/aria-query\": {\n      \"version\": \"5.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz\",\n      \"integrity\": \"sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==\",\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"dequal\": \"^2.0.3\"\n      }\n    },\n    \"node_modules/@testing-library/react\": {\n      \"version\": \"16.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz\",\n      \"integrity\": \"sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/runtime\": \"^7.12.5\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"peerDependencies\": {\n        \"@testing-library/dom\": \"^10.0.0\",\n        \"@types/react\": \"^18.0.0 || ^19.0.0\",\n        \"@types/react-dom\": \"^18.0.0 || ^19.0.0\",\n        \"react\": \"^18.0.0 || ^19.0.0\",\n        \"react-dom\": \"^18.0.0 || ^19.0.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        },\n        \"@types/react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@tiptap/core\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/core/-/core-2.23.0.tgz\",\n      \"integrity\": \"sha512-Cdfhd0Po1cKMYqHtyv/3XATXpf2Kjo8fuau/QJwrml0NpM18/XX9mAgp2NJ/QaiQ3vi8vDandg7RmZ5OrApglQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/pm\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-blockquote\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.23.0.tgz\",\n      \"integrity\": \"sha512-EBWzvqM39K07oAtxD/bZr/TKqTvMZ9pQtBkimyFgHBhkd/PsQvu0r0k1wmheSsizlwDVkO4O8St0zkUimqyevQ==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-bold\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.23.0.tgz\",\n      \"integrity\": \"sha512-OY1xlt1yXpj9+Mzdv+YC6a3okr39xDkeCJyvmLG5OShYUx6fxTT19uXbKF7Y3aAS0BHet5rcrTXEMRlp7N3Qaw==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-bubble-menu\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.23.0.tgz\",\n      \"integrity\": \"sha512-4CZxcVj/0ZetEiWgiP31xTHgaQ7Hr3Ad36cAEza/nGYifaztuPjLO2Y9qdnC1iJHIxttnX6GVRnCMRmZMfhgHg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"tippy.js\": \"^6.3.7\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\",\n        \"@tiptap/pm\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-bullet-list\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.23.0.tgz\",\n      \"integrity\": \"sha512-YrmH5AVSkpCQ7k1Orm8hlzDeUO7rxpQkS51sr2w+ruruKIun/X6V0phuLee+f7DBrzHenBcuU6gBtU6vgGPYFw==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-code\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.23.0.tgz\",\n      \"integrity\": \"sha512-Ip/5+kNoqrxYPHLnZMf7i6wfjjRuR5QgfC3IR3Mk1WQM1JGXCLL+uUjTUxKXFUj28hjSJfsmVbTUhoVvgZEWfw==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-code-block\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.23.0.tgz\",\n      \"integrity\": \"sha512-p8iizp5nQBBhYPrIgBVwEqcDnc2fFRAZCXy/xjmAk2kKOhB7NYe3+1yrbFcQKVAdaUFxG+BRj2WxNDeeRt5tJA==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\",\n        \"@tiptap/pm\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-document\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.23.0.tgz\",\n      \"integrity\": \"sha512-kuRPqH0UdjZ4RcnpPELsu1N8LqeixEin+mv5eaQJI/aZ6rFq+kcY4UZF3C7q56Rat5r9CgHBiZbD0t5l6E3gdA==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-dropcursor\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.23.0.tgz\",\n      \"integrity\": \"sha512-m2LzkJpipHLPEllD3MXZQMssu7Xng7YJOJ8ZNDkF0uUkXljwh7G0ROjGNKUlV8/dqoCVmJIZIyF6t9saQwTTbA==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\",\n        \"@tiptap/pm\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-floating-menu\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.23.0.tgz\",\n      \"integrity\": \"sha512-MvwDMhO5o5NciE+wc6B9dQgTFzmPjtB1o3S+HTdlGzGFGgx9PsNikK5BkqMit9j2NnrqyHnOf88QK/wZR5fqGA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"tippy.js\": \"^6.3.7\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\",\n        \"@tiptap/pm\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-gapcursor\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.23.0.tgz\",\n      \"integrity\": \"sha512-SpYsDtMiVwqcSB84g714PrnHo985R5UiIaGngef6iMNy/0xjKcO0tj/feu0WwJDuSj22Opzlnb/Ld/D4Va27Ng==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\",\n        \"@tiptap/pm\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-hard-break\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.23.0.tgz\",\n      \"integrity\": \"sha512-OpNBEYv9HDUPo8SgvmI5oPd0b+xmdadtFyL7t4lxhYar8n5NDYubaXYgbKcdJfXvUxEeGwdc3ePnTFpsF0mrYw==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-heading\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.23.0.tgz\",\n      \"integrity\": \"sha512-ZbombU/zc42QiqIBVq5bn/I0Y+eiie/0Nax/bdFCDPIKLp8GCp2BDRg46e3kcCanTyZMXw2HmkWrkG3sQNHLWw==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-history\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.23.0.tgz\",\n      \"integrity\": \"sha512-W+2bZ/02nm56g/wmEaSx9QcdZ8mHjoFyc8MKf54Mrzi+nIdNjsNreKrn1yCp683CGEPd8DLadDFkz0o13N+rxA==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\",\n        \"@tiptap/pm\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-horizontal-rule\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.23.0.tgz\",\n      \"integrity\": \"sha512-i/gml9PMQ6uNeq2CCNIWkkYDbafx6XMH4xPSHW4SAG02Exa64iIZLWy57Vb4MR5INSZ6lM/OzU7sdfzHSOb44g==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\",\n        \"@tiptap/pm\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-italic\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.23.0.tgz\",\n      \"integrity\": \"sha512-hX3oacTUloWM8Xu8IapcU2onMWmSkJi8mNAJiIFMiAYcERfTfxPsT3u2yO2gvpoh1iqtZWFM2gc+3x6BnXek8g==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-list-item\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.23.0.tgz\",\n      \"integrity\": \"sha512-tYhLqCaQRjX2S6ICt8FJ+eCAxBMVtXWth6dWt3w7wpkoCVU6n0Dva/2Z3x5lNJPZxUKrsqXc1oYOgvY1pUYyAA==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-ordered-list\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.23.0.tgz\",\n      \"integrity\": \"sha512-IMlPpAPuiFl5L5QwP0aFb8jmJtOceNy4E4tUZulvqARnrzFv//wSuHBZKJziygvm/XK7VcV/clk4fCk/ca5r4g==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-paragraph\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.23.0.tgz\",\n      \"integrity\": \"sha512-MXhRkb741UOcJp2evG/H0MY3WJQnX7z8PsejmJbJXOHBrS/Esxq0AlrDAjuFhbfAnJwYiWQ1lk6ucvKV6DhFuQ==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-strike\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.23.0.tgz\",\n      \"integrity\": \"sha512-zdYO4xdg15BE8gmPYFgA5Xn5+hPA6NAiDBWxv5KNWD9cJ5OhsJx2OsfSCWc0CxYQaIIbHhGM9EGzqH5lF+UnwQ==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-text\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.23.0.tgz\",\n      \"integrity\": \"sha512-hF+CU1H4B4UgqjBXXPPaACVZdSGuMH0TDYTd7h403qUAIBKkYbjuan7laQpiT0qnF0Dg+sGgvmGcd4H1tTBM8g==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/extension-text-style\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.23.0.tgz\",\n      \"integrity\": \"sha512-SFSKm0fHgBiJHwv8nZFeNToBqTDWxuTBr1vWHu/QKmP4H3D8xKRJZlCJ4zrtTEhMyKSLmFxG41fuC0r233SE3Q==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/@tiptap/pm\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/pm/-/pm-2.23.0.tgz\",\n      \"integrity\": \"sha512-PQFi8H+OrcaNXNGxbXSjJmZFh1wxiFMbUg25LjOX148d7i+21uWKy6avsr5rsBQNBAKIIMB6PQY61Lhv5r61uA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"prosemirror-changeset\": \"^2.3.0\",\n        \"prosemirror-collab\": \"^1.3.1\",\n        \"prosemirror-commands\": \"^1.6.2\",\n        \"prosemirror-dropcursor\": \"^1.8.1\",\n        \"prosemirror-gapcursor\": \"^1.3.2\",\n        \"prosemirror-history\": \"^1.4.1\",\n        \"prosemirror-inputrules\": \"^1.4.0\",\n        \"prosemirror-keymap\": \"^1.2.2\",\n        \"prosemirror-markdown\": \"^1.13.1\",\n        \"prosemirror-menu\": \"^1.2.4\",\n        \"prosemirror-model\": \"^1.23.0\",\n        \"prosemirror-schema-basic\": \"^1.2.3\",\n        \"prosemirror-schema-list\": \"^1.4.1\",\n        \"prosemirror-state\": \"^1.4.3\",\n        \"prosemirror-tables\": \"^1.6.4\",\n        \"prosemirror-trailing-node\": \"^3.0.0\",\n        \"prosemirror-transform\": \"^1.10.2\",\n        \"prosemirror-view\": \"^1.37.0\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      }\n    },\n    \"node_modules/@tiptap/react\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/react/-/react-2.23.0.tgz\",\n      \"integrity\": \"sha512-HiEIMYXa4/JWJq1USm0BWXDX0nrXUyG3T1UwZXwFCWZWaRBXmBRv+X5GkyP7fYuuiTTcqmTO3bBhb9CGNDr+AA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@tiptap/extension-bubble-menu\": \"^2.23.0\",\n        \"@tiptap/extension-floating-menu\": \"^2.23.0\",\n        \"@types/use-sync-external-store\": \"^0.0.6\",\n        \"fast-deep-equal\": \"^3\",\n        \"use-sync-external-store\": \"^1\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      },\n      \"peerDependencies\": {\n        \"@tiptap/core\": \"^2.7.0\",\n        \"@tiptap/pm\": \"^2.7.0\",\n        \"react\": \"^17.0.0 || ^18.0.0 || ^19.0.0\",\n        \"react-dom\": \"^17.0.0 || ^18.0.0 || ^19.0.0\"\n      }\n    },\n    \"node_modules/@tiptap/starter-kit\": {\n      \"version\": \"2.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.23.0.tgz\",\n      \"integrity\": \"sha512-mmSrWQB57dHQOZLE9Q4vlgsYYMfihVHDMHshgTSy7nsmw3cFgYGcmzdBKVkO6Ekxk4vElzZIzjSTa0cbOOtfKQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@tiptap/core\": \"^2.23.0\",\n        \"@tiptap/extension-blockquote\": \"^2.23.0\",\n        \"@tiptap/extension-bold\": \"^2.23.0\",\n        \"@tiptap/extension-bullet-list\": \"^2.23.0\",\n        \"@tiptap/extension-code\": \"^2.23.0\",\n        \"@tiptap/extension-code-block\": \"^2.23.0\",\n        \"@tiptap/extension-document\": \"^2.23.0\",\n        \"@tiptap/extension-dropcursor\": \"^2.23.0\",\n        \"@tiptap/extension-gapcursor\": \"^2.23.0\",\n        \"@tiptap/extension-hard-break\": \"^2.23.0\",\n        \"@tiptap/extension-heading\": \"^2.23.0\",\n        \"@tiptap/extension-history\": \"^2.23.0\",\n        \"@tiptap/extension-horizontal-rule\": \"^2.23.0\",\n        \"@tiptap/extension-italic\": \"^2.23.0\",\n        \"@tiptap/extension-list-item\": \"^2.23.0\",\n        \"@tiptap/extension-ordered-list\": \"^2.23.0\",\n        \"@tiptap/extension-paragraph\": \"^2.23.0\",\n        \"@tiptap/extension-strike\": \"^2.23.0\",\n        \"@tiptap/extension-text\": \"^2.23.0\",\n        \"@tiptap/extension-text-style\": \"^2.23.0\",\n        \"@tiptap/pm\": \"^2.23.0\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/ueberdosis\"\n      }\n    },\n    \"node_modules/@tsconfig/node10\": {\n      \"version\": \"1.0.11\",\n      \"resolved\": \"https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz\",\n      \"integrity\": \"sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==\",\n      \"devOptional\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@tsconfig/node12\": {\n      \"version\": \"1.0.11\",\n      \"resolved\": \"https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz\",\n      \"integrity\": \"sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==\",\n      \"devOptional\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@tsconfig/node14\": {\n      \"version\": \"1.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz\",\n      \"integrity\": \"sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==\",\n      \"devOptional\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@tsconfig/node16\": {\n      \"version\": \"1.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz\",\n      \"integrity\": \"sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==\",\n      \"devOptional\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@tybys/wasm-util\": {\n      \"version\": \"0.9.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz\",\n      \"integrity\": \"sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"tslib\": \"^2.4.0\"\n      }\n    },\n    \"node_modules/@types/aria-query\": {\n      \"version\": \"5.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz\",\n      \"integrity\": \"sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/bcryptjs\": {\n      \"version\": \"2.4.6\",\n      \"resolved\": \"https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz\",\n      \"integrity\": \"sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/canvas-confetti\": {\n      \"version\": \"1.9.0\",\n      \"resolved\": \"https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz\",\n      \"integrity\": \"sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/d3-array\": {\n      \"version\": \"3.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz\",\n      \"integrity\": \"sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/d3-color\": {\n      \"version\": \"3.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz\",\n      \"integrity\": \"sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/d3-ease\": {\n      \"version\": \"3.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz\",\n      \"integrity\": \"sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/d3-interpolate\": {\n      \"version\": \"3.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz\",\n      \"integrity\": \"sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/d3-color\": \"*\"\n      }\n    },\n    \"node_modules/@types/d3-path\": {\n      \"version\": \"3.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz\",\n      \"integrity\": \"sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/d3-scale\": {\n      \"version\": \"4.0.9\",\n      \"resolved\": \"https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz\",\n      \"integrity\": \"sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/d3-time\": \"*\"\n      }\n    },\n    \"node_modules/@types/d3-shape\": {\n      \"version\": \"3.1.7\",\n      \"resolved\": \"https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz\",\n      \"integrity\": \"sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/d3-path\": \"*\"\n      }\n    },\n    \"node_modules/@types/d3-time\": {\n      \"version\": \"3.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz\",\n      \"integrity\": \"sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/d3-timer\": {\n      \"version\": \"3.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz\",\n      \"integrity\": \"sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/debug\": {\n      \"version\": \"4.1.12\",\n      \"resolved\": \"https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz\",\n      \"integrity\": \"sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/ms\": \"*\"\n      }\n    },\n    \"node_modules/@types/estree\": {\n      \"version\": \"1.0.8\",\n      \"resolved\": \"https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz\",\n      \"integrity\": \"sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/estree-jsx\": {\n      \"version\": \"1.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz\",\n      \"integrity\": \"sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/estree\": \"*\"\n      }\n    },\n    \"node_modules/@types/fs-extra\": {\n      \"version\": \"11.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz\",\n      \"integrity\": \"sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/jsonfile\": \"*\",\n        \"@types/node\": \"*\"\n      }\n    },\n    \"node_modules/@types/hast\": {\n      \"version\": \"3.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz\",\n      \"integrity\": \"sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/unist\": \"*\"\n      }\n    },\n    \"node_modules/@types/json-schema\": {\n      \"version\": \"7.0.15\",\n      \"resolved\": \"https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz\",\n      \"integrity\": \"sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/json5\": {\n      \"version\": \"0.0.29\",\n      \"resolved\": \"https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz\",\n      \"integrity\": \"sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/jsonfile\": {\n      \"version\": \"6.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz\",\n      \"integrity\": \"sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/node\": \"*\"\n      }\n    },\n    \"node_modules/@types/katex\": {\n      \"version\": \"0.16.7\",\n      \"resolved\": \"https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz\",\n      \"integrity\": \"sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/linkify-it\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz\",\n      \"integrity\": \"sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/markdown-it\": {\n      \"version\": \"14.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz\",\n      \"integrity\": \"sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@types/linkify-it\": \"^5\",\n        \"@types/mdurl\": \"^2\"\n      }\n    },\n    \"node_modules/@types/md5\": {\n      \"version\": \"2.3.5\",\n      \"resolved\": \"https://registry.npmjs.org/@types/md5/-/md5-2.3.5.tgz\",\n      \"integrity\": \"sha512-/i42wjYNgE6wf0j2bcTX6kuowmdL/6PE4IVitMpm2eYKBUuYCprdcWVK+xEF0gcV6ufMCRhtxmReGfc6hIK7Jw==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/mdast\": {\n      \"version\": \"4.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz\",\n      \"integrity\": \"sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/unist\": \"*\"\n      }\n    },\n    \"node_modules/@types/mdurl\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz\",\n      \"integrity\": \"sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/ms\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz\",\n      \"integrity\": \"sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/node\": {\n      \"version\": \"20.19.2\",\n      \"resolved\": \"https://registry.npmjs.org/@types/node/-/node-20.19.2.tgz\",\n      \"integrity\": \"sha512-9pLGGwdzOUBDYi0GNjM97FIA+f92fqSke6joWeBjWXllfNxZBs7qeMF7tvtOIsbY45xkWkxrdwUfUf3MnQa9gA==\",\n      \"devOptional\": true,\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"undici-types\": \"~6.21.0\"\n      }\n    },\n    \"node_modules/@types/nodemailer\": {\n      \"version\": \"6.4.17\",\n      \"resolved\": \"https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz\",\n      \"integrity\": \"sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/node\": \"*\"\n      }\n    },\n    \"node_modules/@types/prismjs\": {\n      \"version\": \"1.26.5\",\n      \"resolved\": \"https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz\",\n      \"integrity\": \"sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/react\": {\n      \"version\": \"19.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz\",\n      \"integrity\": \"sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"csstype\": \"^3.0.2\"\n      }\n    },\n    \"node_modules/@types/react-csv\": {\n      \"version\": \"1.1.10\",\n      \"resolved\": \"https://registry.npmjs.org/@types/react-csv/-/react-csv-1.1.10.tgz\",\n      \"integrity\": \"sha512-PESAyASL7Nfi/IyBR3ufd8qZkyoS+7jOylKmJxRZUZLFASLo4NZaRsJ8rNP8pCcbIziADyWBbLPD1nPddhsL4g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/react\": \"*\"\n      }\n    },\n    \"node_modules/@types/react-dom\": {\n      \"version\": \"19.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz\",\n      \"integrity\": \"sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==\",\n      \"devOptional\": true,\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"peerDependencies\": {\n        \"@types/react\": \"^19.2.0\"\n      }\n    },\n    \"node_modules/@types/react-syntax-highlighter\": {\n      \"version\": \"15.5.13\",\n      \"resolved\": \"https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz\",\n      \"integrity\": \"sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/react\": \"*\"\n      }\n    },\n    \"node_modules/@types/stylis\": {\n      \"version\": \"4.2.5\",\n      \"resolved\": \"https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz\",\n      \"integrity\": \"sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/swagger-ui-react\": {\n      \"version\": \"5.18.0\",\n      \"resolved\": \"https://registry.npmjs.org/@types/swagger-ui-react/-/swagger-ui-react-5.18.0.tgz\",\n      \"integrity\": \"sha512-c2M9adVG7t28t1pq19K9Jt20VLQf0P/fwJwnfcmsVVsdkwCWhRmbKDu+tIs0/NGwJ/7GY8lBx+iKZxuDI5gDbw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/react\": \"*\"\n      }\n    },\n    \"node_modules/@types/trusted-types\": {\n      \"version\": \"2.0.7\",\n      \"resolved\": \"https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz\",\n      \"integrity\": \"sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==\",\n      \"license\": \"MIT\",\n      \"optional\": true\n    },\n    \"node_modules/@types/unist\": {\n      \"version\": \"3.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz\",\n      \"integrity\": \"sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/use-sync-external-store\": {\n      \"version\": \"0.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz\",\n      \"integrity\": \"sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@typescript-eslint/eslint-plugin\": {\n      \"version\": \"8.56.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz\",\n      \"integrity\": \"sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@eslint-community/regexpp\": \"^4.12.2\",\n        \"@typescript-eslint/scope-manager\": \"8.56.0\",\n        \"@typescript-eslint/type-utils\": \"8.56.0\",\n        \"@typescript-eslint/utils\": \"8.56.0\",\n        \"@typescript-eslint/visitor-keys\": \"8.56.0\",\n        \"ignore\": \"^7.0.5\",\n        \"natural-compare\": \"^1.4.0\",\n        \"ts-api-utils\": \"^2.4.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"@typescript-eslint/parser\": \"^8.56.0\",\n        \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore\": {\n      \"version\": \"7.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz\",\n      \"integrity\": \"sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 4\"\n      }\n    },\n    \"node_modules/@typescript-eslint/parser\": {\n      \"version\": \"8.56.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz\",\n      \"integrity\": \"sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@typescript-eslint/scope-manager\": \"8.56.0\",\n        \"@typescript-eslint/types\": \"8.56.0\",\n        \"@typescript-eslint/typescript-estree\": \"8.56.0\",\n        \"@typescript-eslint/visitor-keys\": \"8.56.0\",\n        \"debug\": \"^4.4.3\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/project-service\": {\n      \"version\": \"8.56.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz\",\n      \"integrity\": \"sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/tsconfig-utils\": \"^8.56.0\",\n        \"@typescript-eslint/types\": \"^8.56.0\",\n        \"debug\": \"^4.4.3\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/scope-manager\": {\n      \"version\": \"8.56.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz\",\n      \"integrity\": \"sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/types\": \"8.56.0\",\n        \"@typescript-eslint/visitor-keys\": \"8.56.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/tsconfig-utils\": {\n      \"version\": \"8.56.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz\",\n      \"integrity\": \"sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/type-utils\": {\n      \"version\": \"8.56.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz\",\n      \"integrity\": \"sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/types\": \"8.56.0\",\n        \"@typescript-eslint/typescript-estree\": \"8.56.0\",\n        \"@typescript-eslint/utils\": \"8.56.0\",\n        \"debug\": \"^4.4.3\",\n        \"ts-api-utils\": \"^2.4.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/types\": {\n      \"version\": \"8.56.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz\",\n      \"integrity\": \"sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/typescript-estree\": {\n      \"version\": \"8.56.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz\",\n      \"integrity\": \"sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/project-service\": \"8.56.0\",\n        \"@typescript-eslint/tsconfig-utils\": \"8.56.0\",\n        \"@typescript-eslint/types\": \"8.56.0\",\n        \"@typescript-eslint/visitor-keys\": \"8.56.0\",\n        \"debug\": \"^4.4.3\",\n        \"minimatch\": \"^9.0.5\",\n        \"semver\": \"^7.7.3\",\n        \"tinyglobby\": \"^0.2.15\",\n        \"ts-api-utils\": \"^2.4.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/utils\": {\n      \"version\": \"8.56.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz\",\n      \"integrity\": \"sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@eslint-community/eslint-utils\": \"^4.9.1\",\n        \"@typescript-eslint/scope-manager\": \"8.56.0\",\n        \"@typescript-eslint/types\": \"8.56.0\",\n        \"@typescript-eslint/typescript-estree\": \"8.56.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/visitor-keys\": {\n      \"version\": \"8.56.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz\",\n      \"integrity\": \"sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/types\": \"8.56.0\",\n        \"eslint-visitor-keys\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz\",\n      \"integrity\": \"sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \"^20.19.0 || ^22.13.0 || >=24\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/@ungap/structured-clone\": {\n      \"version\": \"1.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz\",\n      \"integrity\": \"sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==\",\n      \"license\": \"ISC\"\n    },\n    \"node_modules/@unrs/resolver-binding-android-arm-eabi\": {\n      \"version\": \"1.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.9.2.tgz\",\n      \"integrity\": \"sha512-tS+lqTU3N0kkthU+rYp0spAYq15DU8ld9kXkaKg9sbQqJNF+WPMuNHZQGCgdxrUOEO0j22RKMwRVhF1HTl+X8A==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"android\"\n      ]\n    },\n    \"node_modules/@unrs/resolver-binding-android-arm64\": {\n      \"version\": \"1.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.9.2.tgz\",\n      \"integrity\": \"sha512-MffGiZULa/KmkNjHeuuflLVqfhqLv1vZLm8lWIyeADvlElJ/GLSOkoUX+5jf4/EGtfwrNFcEaB8BRas03KT0/Q==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"android\"\n      ]\n    },\n    \"node_modules/@unrs/resolver-binding-darwin-arm64\": {\n      \"version\": \"1.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.9.2.tgz\",\n      \"integrity\": \"sha512-dzJYK5rohS1sYl1DHdJ3mwfwClJj5BClQnQSyAgEfggbUwA9RlROQSSbKBLqrGfsiC/VyrDPtbO8hh56fnkbsQ==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ]\n    },\n    \"node_modules/@unrs/resolver-binding-darwin-x64\": {\n      \"version\": \"1.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.9.2.tgz\",\n      \"integrity\": \"sha512-gaIMWK+CWtXcg9gUyznkdV54LzQ90S3X3dn8zlh+QR5Xy7Y+Efqw4Rs4im61K1juy4YNb67vmJsCDAGOnIeffQ==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ]\n    },\n    \"node_modules/@unrs/resolver-binding-freebsd-x64\": {\n      \"version\": \"1.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.9.2.tgz\",\n      \"integrity\": \"sha512-S7QpkMbVoVJb0xwHFwujnwCAEDe/596xqY603rpi/ioTn9VDgBHnCCxh+UFrr5yxuMH+dliHfjwCZJXOPJGPnw==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"freebsd\"\n      ]\n    },\n    \"node_modules/@unrs/resolver-binding-linux-arm-gnueabihf\": {\n      \"version\": \"1.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.9.2.tgz\",\n      \"integrity\": \"sha512-+XPUMCuCCI80I46nCDFbGum0ZODP5NWGiwS3Pj8fOgsG5/ctz+/zzuBlq/WmGa+EjWZdue6CF0aWWNv84sE1uw==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@unrs/resolver-binding-linux-arm-musleabihf\": {\n      \"version\": \"1.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.9.2.tgz\",\n      \"integrity\": \"sha512-sqvUyAd1JUpwbz33Ce2tuTLJKM+ucSsYpPGl2vuFwZnEIg0CmdxiZ01MHQ3j6ExuRqEDUCy8yvkDKvjYFPb8Zg==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@unrs/resolver-binding-linux-arm64-gnu\": {\n      \"version\": \"1.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.9.2.tgz\",\n      \"integrity\": \"sha512-UYA0MA8ajkEDCFRQdng/FVx3F6szBvk3EPnkTTQuuO9lV1kPGuTB+V9TmbDxy5ikaEgyWKxa4CI3ySjklZ9lFA==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@unrs/resolver-binding-linux-arm64-musl\": {\n      \"version\": \"1.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.9.2.tgz\",\n      \"integrity\": \"sha512-P/CO3ODU9YJIHFqAkHbquKtFst0COxdphc8TKGL5yCX75GOiVpGqd1d15ahpqu8xXVsqP4MGFP2C3LRZnnL5MA==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@unrs/resolver-binding-linux-ppc64-gnu\": {\n      \"version\": \"1.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.9.2.tgz\",\n      \"integrity\": \"sha512-uKStFlOELBxBum2s1hODPtgJhY4NxYJE9pAeyBgNEzHgTqTiVBPjfTlPFJkfxyTjQEuxZbbJlJnMCrRgD7ubzw==\",\n      \"cpu\": [\n        \"ppc64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@unrs/resolver-binding-linux-riscv64-gnu\": {\n      \"version\": \"1.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.9.2.tgz\",\n      \"integrity\": \"sha512-LkbNnZlhINfY9gK30AHs26IIVEZ9PEl9qOScYdmY2o81imJYI4IMnJiW0vJVtXaDHvBvxeAgEy5CflwJFIl3tQ==\",\n      \"cpu\": [\n        \"riscv64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@unrs/resolver-binding-linux-riscv64-musl\": {\n      \"version\": \"1.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.9.2.tgz\",\n      \"integrity\": \"sha512-vI+e6FzLyZHSLFNomPi+nT+qUWN4YSj8pFtQZSFTtmgFoxqB6NyjxSjAxEC1m93qn6hUXhIsh8WMp+fGgxCoRg==\",\n      \"cpu\": [\n        \"riscv64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@unrs/resolver-binding-linux-s390x-gnu\": {\n      \"version\": \"1.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.9.2.tgz\",\n      \"integrity\": \"sha512-sSO4AlAYhSM2RAzBsRpahcJB1msc6uYLAtP6pesPbZtptF8OU/CbCPhSRW6cnYOGuVmEmWVW5xVboAqCnWTeHQ==\",\n      \"cpu\": [\n        \"s390x\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@unrs/resolver-binding-linux-x64-gnu\": {\n      \"version\": \"1.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.9.2.tgz\",\n      \"integrity\": \"sha512-jkSkwch0uPFva20Mdu8orbQjv2A3G88NExTN2oPTI1AJ+7mZfYW3cDCTyoH6OnctBKbBVeJCEqh0U02lTkqD5w==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@unrs/resolver-binding-linux-x64-musl\": {\n      \"version\": \"1.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.9.2.tgz\",\n      \"integrity\": \"sha512-Uk64NoiTpQbkpl+bXsbeyOPRpUoMdcUqa+hDC1KhMW7aN1lfW8PBlBH4mJ3n3Y47dYE8qi0XTxy1mBACruYBaw==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@unrs/resolver-binding-wasm32-wasi\": {\n      \"version\": \"1.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.9.2.tgz\",\n      \"integrity\": \"sha512-EpBGwkcjDicjR/ybC0g8wO5adPNdVuMrNalVgYcWi+gYtC1XYNuxe3rufcO7dA76OHGeVabcO6cSkPJKVcbCXQ==\",\n      \"cpu\": [\n        \"wasm32\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"@napi-rs/wasm-runtime\": \"^0.2.11\"\n      },\n      \"engines\": {\n        \"node\": \">=14.0.0\"\n      }\n    },\n    \"node_modules/@unrs/resolver-binding-win32-arm64-msvc\": {\n      \"version\": \"1.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.9.2.tgz\",\n      \"integrity\": \"sha512-EdFbGn7o1SxGmN6aZw9wAkehZJetFPao0VGZ9OMBwKx6TkvDuj6cNeLimF/Psi6ts9lMOe+Dt6z19fZQ9Ye2fw==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ]\n    },\n    \"node_modules/@unrs/resolver-binding-win32-ia32-msvc\": {\n      \"version\": \"1.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.9.2.tgz\",\n      \"integrity\": \"sha512-JY9hi1p7AG+5c/dMU8o2kWemM8I6VZxfGwn1GCtf3c5i+IKcMo2NQ8OjZ4Z3/itvY/Si3K10jOBQn7qsD/whUA==\",\n      \"cpu\": [\n        \"ia32\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ]\n    },\n    \"node_modules/@unrs/resolver-binding-win32-x64-msvc\": {\n      \"version\": \"1.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.9.2.tgz\",\n      \"integrity\": \"sha512-ryoo+EB19lMxAd80ln9BVf8pdOAxLb97amrQ3SFN9OCRn/5M5wvwDgAe4i8ZjhpbiHoDeP8yavcTEnpKBo7lZg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ]\n    },\n    \"node_modules/acorn\": {\n      \"version\": \"8.15.0\",\n      \"resolved\": \"https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz\",\n      \"integrity\": \"sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==\",\n      \"devOptional\": true,\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"bin\": {\n        \"acorn\": \"bin/acorn\"\n      },\n      \"engines\": {\n        \"node\": \">=0.4.0\"\n      }\n    },\n    \"node_modules/acorn-jsx\": {\n      \"version\": \"5.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz\",\n      \"integrity\": \"sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"peerDependencies\": {\n        \"acorn\": \"^6.0.0 || ^7.0.0 || ^8.0.0\"\n      }\n    },\n    \"node_modules/acorn-walk\": {\n      \"version\": \"8.3.4\",\n      \"resolved\": \"https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz\",\n      \"integrity\": \"sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==\",\n      \"devOptional\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"acorn\": \"^8.11.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.4.0\"\n      }\n    },\n    \"node_modules/ajv\": {\n      \"version\": \"6.14.0\",\n      \"resolved\": \"https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz\",\n      \"integrity\": \"sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"fast-deep-equal\": \"^3.1.1\",\n        \"fast-json-stable-stringify\": \"^2.0.0\",\n        \"json-schema-traverse\": \"^0.4.1\",\n        \"uri-js\": \"^4.2.2\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/epoberezkin\"\n      }\n    },\n    \"node_modules/ansi-regex\": {\n      \"version\": \"6.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz\",\n      \"integrity\": \"sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/ansi-regex?sponsor=1\"\n      }\n    },\n    \"node_modules/ansi-styles\": {\n      \"version\": \"6.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz\",\n      \"integrity\": \"sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/ansi-styles?sponsor=1\"\n      }\n    },\n    \"node_modules/any-promise\": {\n      \"version\": \"1.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz\",\n      \"integrity\": \"sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/anymatch\": {\n      \"version\": \"3.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz\",\n      \"integrity\": \"sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"normalize-path\": \"^3.0.0\",\n        \"picomatch\": \"^2.0.4\"\n      },\n      \"engines\": {\n        \"node\": \">= 8\"\n      }\n    },\n    \"node_modules/arg\": {\n      \"version\": \"5.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/arg/-/arg-5.0.2.tgz\",\n      \"integrity\": \"sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/argparse\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz\",\n      \"integrity\": \"sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==\",\n      \"license\": \"Python-2.0\"\n    },\n    \"node_modules/aria-hidden\": {\n      \"version\": \"1.2.6\",\n      \"resolved\": \"https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz\",\n      \"integrity\": \"sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"tslib\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      }\n    },\n    \"node_modules/aria-query\": {\n      \"version\": \"5.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz\",\n      \"integrity\": \"sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/array-back\": {\n      \"version\": \"6.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz\",\n      \"integrity\": \"sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12.17\"\n      }\n    },\n    \"node_modules/array-buffer-byte-length\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz\",\n      \"integrity\": \"sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.3\",\n        \"is-array-buffer\": \"^3.0.5\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/array-includes\": {\n      \"version\": \"3.1.9\",\n      \"resolved\": \"https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz\",\n      \"integrity\": \"sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.8\",\n        \"call-bound\": \"^1.0.4\",\n        \"define-properties\": \"^1.2.1\",\n        \"es-abstract\": \"^1.24.0\",\n        \"es-object-atoms\": \"^1.1.1\",\n        \"get-intrinsic\": \"^1.3.0\",\n        \"is-string\": \"^1.1.1\",\n        \"math-intrinsics\": \"^1.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/array.prototype.findlast\": {\n      \"version\": \"1.2.5\",\n      \"resolved\": \"https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz\",\n      \"integrity\": \"sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.7\",\n        \"define-properties\": \"^1.2.1\",\n        \"es-abstract\": \"^1.23.2\",\n        \"es-errors\": \"^1.3.0\",\n        \"es-object-atoms\": \"^1.0.0\",\n        \"es-shim-unscopables\": \"^1.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/array.prototype.findlastindex\": {\n      \"version\": \"1.2.6\",\n      \"resolved\": \"https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz\",\n      \"integrity\": \"sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.8\",\n        \"call-bound\": \"^1.0.4\",\n        \"define-properties\": \"^1.2.1\",\n        \"es-abstract\": \"^1.23.9\",\n        \"es-errors\": \"^1.3.0\",\n        \"es-object-atoms\": \"^1.1.1\",\n        \"es-shim-unscopables\": \"^1.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/array.prototype.flat\": {\n      \"version\": \"1.3.3\",\n      \"resolved\": \"https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz\",\n      \"integrity\": \"sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.8\",\n        \"define-properties\": \"^1.2.1\",\n        \"es-abstract\": \"^1.23.5\",\n        \"es-shim-unscopables\": \"^1.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/array.prototype.flatmap\": {\n      \"version\": \"1.3.3\",\n      \"resolved\": \"https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz\",\n      \"integrity\": \"sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.8\",\n        \"define-properties\": \"^1.2.1\",\n        \"es-abstract\": \"^1.23.5\",\n        \"es-shim-unscopables\": \"^1.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/array.prototype.tosorted\": {\n      \"version\": \"1.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz\",\n      \"integrity\": \"sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.7\",\n        \"define-properties\": \"^1.2.1\",\n        \"es-abstract\": \"^1.23.3\",\n        \"es-errors\": \"^1.3.0\",\n        \"es-shim-unscopables\": \"^1.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/arraybuffer.prototype.slice\": {\n      \"version\": \"1.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz\",\n      \"integrity\": \"sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"array-buffer-byte-length\": \"^1.0.1\",\n        \"call-bind\": \"^1.0.8\",\n        \"define-properties\": \"^1.2.1\",\n        \"es-abstract\": \"^1.23.5\",\n        \"es-errors\": \"^1.3.0\",\n        \"get-intrinsic\": \"^1.2.6\",\n        \"is-array-buffer\": \"^3.0.4\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/asn1js\": {\n      \"version\": \"3.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz\",\n      \"integrity\": \"sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==\",\n      \"license\": \"BSD-3-Clause\",\n      \"dependencies\": {\n        \"pvtsutils\": \"^1.3.6\",\n        \"pvutils\": \"^1.1.3\",\n        \"tslib\": \"^2.8.1\"\n      },\n      \"engines\": {\n        \"node\": \">=12.0.0\"\n      }\n    },\n    \"node_modules/ast-types-flow\": {\n      \"version\": \"0.0.8\",\n      \"resolved\": \"https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz\",\n      \"integrity\": \"sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/async-function\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz\",\n      \"integrity\": \"sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/available-typed-arrays\": {\n      \"version\": \"1.0.7\",\n      \"resolved\": \"https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz\",\n      \"integrity\": \"sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"possible-typed-array-names\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/axe-core\": {\n      \"version\": \"4.10.3\",\n      \"resolved\": \"https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz\",\n      \"integrity\": \"sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==\",\n      \"dev\": true,\n      \"license\": \"MPL-2.0\",\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/axobject-query\": {\n      \"version\": \"4.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz\",\n      \"integrity\": \"sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/babel-plugin-react-compiler\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz\",\n      \"integrity\": \"sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@babel/types\": \"^7.26.0\"\n      }\n    },\n    \"node_modules/bail\": {\n      \"version\": \"2.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/bail/-/bail-2.0.2.tgz\",\n      \"integrity\": \"sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/balanced-match\": {\n      \"version\": \"4.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz\",\n      \"integrity\": \"sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"20 || >=22\"\n      }\n    },\n    \"node_modules/baseline-browser-mapping\": {\n      \"version\": \"2.9.19\",\n      \"resolved\": \"https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz\",\n      \"integrity\": \"sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==\",\n      \"license\": \"Apache-2.0\",\n      \"bin\": {\n        \"baseline-browser-mapping\": \"dist/cli.js\"\n      }\n    },\n    \"node_modules/bcryptjs\": {\n      \"version\": \"3.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz\",\n      \"integrity\": \"sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==\",\n      \"license\": \"BSD-3-Clause\",\n      \"bin\": {\n        \"bcrypt\": \"bin/bcrypt\"\n      }\n    },\n    \"node_modules/binary-extensions\": {\n      \"version\": \"2.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz\",\n      \"integrity\": \"sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/bluebird\": {\n      \"version\": \"3.7.2\",\n      \"resolved\": \"https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz\",\n      \"integrity\": \"sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/brace-expansion\": {\n      \"version\": \"5.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz\",\n      \"integrity\": \"sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"balanced-match\": \"^4.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"20 || >=22\"\n      }\n    },\n    \"node_modules/braces\": {\n      \"version\": \"3.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/braces/-/braces-3.0.3.tgz\",\n      \"integrity\": \"sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"fill-range\": \"^7.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/browserslist\": {\n      \"version\": \"4.27.0\",\n      \"resolved\": \"https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz\",\n      \"integrity\": \"sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==\",\n      \"dev\": true,\n      \"funding\": [\n        {\n          \"type\": \"opencollective\",\n          \"url\": \"https://opencollective.com/browserslist\"\n        },\n        {\n          \"type\": \"tidelift\",\n          \"url\": \"https://tidelift.com/funding/github/npm/browserslist\"\n        },\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/ai\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"baseline-browser-mapping\": \"^2.8.19\",\n        \"caniuse-lite\": \"^1.0.30001751\",\n        \"electron-to-chromium\": \"^1.5.238\",\n        \"node-releases\": \"^2.0.26\",\n        \"update-browserslist-db\": \"^1.1.4\"\n      },\n      \"bin\": {\n        \"browserslist\": \"cli.js\"\n      },\n      \"engines\": {\n        \"node\": \"^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7\"\n      }\n    },\n    \"node_modules/cache-point\": {\n      \"version\": \"3.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/cache-point/-/cache-point-3.0.1.tgz\",\n      \"integrity\": \"sha512-itTIMLEKbh6Dw5DruXbxAgcyLnh/oPGVLBfTPqBOftASxHe8bAeXy7JkO4F0LvHqht7XqP5O/09h5UcHS2w0FA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"array-back\": \"^6.2.2\"\n      },\n      \"engines\": {\n        \"node\": \">=12.17\"\n      },\n      \"peerDependencies\": {\n        \"@75lb/nature\": \"latest\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@75lb/nature\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/call-bind\": {\n      \"version\": \"1.0.8\",\n      \"resolved\": \"https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz\",\n      \"integrity\": \"sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind-apply-helpers\": \"^1.0.0\",\n        \"es-define-property\": \"^1.0.0\",\n        \"get-intrinsic\": \"^1.2.4\",\n        \"set-function-length\": \"^1.2.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/call-bind-apply-helpers\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz\",\n      \"integrity\": \"sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"es-errors\": \"^1.3.0\",\n        \"function-bind\": \"^1.1.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/call-bound\": {\n      \"version\": \"1.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz\",\n      \"integrity\": \"sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind-apply-helpers\": \"^1.0.2\",\n        \"get-intrinsic\": \"^1.3.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/callsites\": {\n      \"version\": \"3.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz\",\n      \"integrity\": \"sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/camelcase-css\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz\",\n      \"integrity\": \"sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 6\"\n      }\n    },\n    \"node_modules/camelize\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz\",\n      \"integrity\": \"sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/caniuse-lite\": {\n      \"version\": \"1.0.30001754\",\n      \"resolved\": \"https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz\",\n      \"integrity\": \"sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==\",\n      \"funding\": [\n        {\n          \"type\": \"opencollective\",\n          \"url\": \"https://opencollective.com/browserslist\"\n        },\n        {\n          \"type\": \"tidelift\",\n          \"url\": \"https://tidelift.com/funding/github/npm/caniuse-lite\"\n        },\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/ai\"\n        }\n      ],\n      \"license\": \"CC-BY-4.0\"\n    },\n    \"node_modules/canvas-confetti\": {\n      \"version\": \"1.9.3\",\n      \"resolved\": \"https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.3.tgz\",\n      \"integrity\": \"sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==\",\n      \"license\": \"ISC\",\n      \"funding\": {\n        \"type\": \"donate\",\n        \"url\": \"https://www.paypal.me/kirilvatev\"\n      }\n    },\n    \"node_modules/catharsis\": {\n      \"version\": \"0.9.0\",\n      \"resolved\": \"https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz\",\n      \"integrity\": \"sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"lodash\": \"^4.17.15\"\n      },\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/ccount\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz\",\n      \"integrity\": \"sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/chalk\": {\n      \"version\": \"5.4.1\",\n      \"resolved\": \"https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz\",\n      \"integrity\": \"sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^12.17.0 || ^14.13 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/chalk?sponsor=1\"\n      }\n    },\n    \"node_modules/char-regex\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz\",\n      \"integrity\": \"sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=10\"\n      }\n    },\n    \"node_modules/character-entities\": {\n      \"version\": \"2.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz\",\n      \"integrity\": \"sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/character-entities-html4\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz\",\n      \"integrity\": \"sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/character-entities-legacy\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz\",\n      \"integrity\": \"sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/character-reference-invalid\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz\",\n      \"integrity\": \"sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/charenc\": {\n      \"version\": \"0.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz\",\n      \"integrity\": \"sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==\",\n      \"license\": \"BSD-3-Clause\",\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/child_process\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz\",\n      \"integrity\": \"sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==\",\n      \"license\": \"ISC\"\n    },\n    \"node_modules/chokidar\": {\n      \"version\": \"3.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz\",\n      \"integrity\": \"sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"anymatch\": \"~3.1.2\",\n        \"braces\": \"~3.0.2\",\n        \"glob-parent\": \"~5.1.2\",\n        \"is-binary-path\": \"~2.1.0\",\n        \"is-glob\": \"~4.0.1\",\n        \"normalize-path\": \"~3.0.0\",\n        \"readdirp\": \"~3.6.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 8.10.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://paulmillr.com/funding/\"\n      },\n      \"optionalDependencies\": {\n        \"fsevents\": \"~2.3.2\"\n      }\n    },\n    \"node_modules/chokidar/node_modules/glob-parent\": {\n      \"version\": \"5.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz\",\n      \"integrity\": \"sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"is-glob\": \"^4.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 6\"\n      }\n    },\n    \"node_modules/class-variance-authority\": {\n      \"version\": \"0.7.1\",\n      \"resolved\": \"https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz\",\n      \"integrity\": \"sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==\",\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"clsx\": \"^2.1.1\"\n      },\n      \"funding\": {\n        \"url\": \"https://polar.sh/cva\"\n      }\n    },\n    \"node_modules/cli-cursor\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz\",\n      \"integrity\": \"sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"restore-cursor\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/cli-spinners\": {\n      \"version\": \"2.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz\",\n      \"integrity\": \"sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/client-only\": {\n      \"version\": \"0.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz\",\n      \"integrity\": \"sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/clsx\": {\n      \"version\": \"2.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz\",\n      \"integrity\": \"sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/cmdk\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz\",\n      \"integrity\": \"sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@radix-ui/react-compose-refs\": \"^1.1.1\",\n        \"@radix-ui/react-dialog\": \"^1.1.6\",\n        \"@radix-ui/react-id\": \"^1.1.0\",\n        \"@radix-ui/react-primitive\": \"^2.0.2\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18 || ^19 || ^19.0.0-rc\",\n        \"react-dom\": \"^18 || ^19 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/color-convert\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz\",\n      \"integrity\": \"sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"color-name\": \"~1.1.4\"\n      },\n      \"engines\": {\n        \"node\": \">=7.0.0\"\n      }\n    },\n    \"node_modules/color-name\": {\n      \"version\": \"1.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz\",\n      \"integrity\": \"sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/comma-separated-tokens\": {\n      \"version\": \"2.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz\",\n      \"integrity\": \"sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/comment-parser\": {\n      \"version\": \"1.4.1\",\n      \"resolved\": \"https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz\",\n      \"integrity\": \"sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 12.0.0\"\n      }\n    },\n    \"node_modules/compare-versions\": {\n      \"version\": \"6.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz\",\n      \"integrity\": \"sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/convert-source-map\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz\",\n      \"integrity\": \"sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/cookie\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz\",\n      \"integrity\": \"sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/cookies-next\": {\n      \"version\": \"5.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/cookies-next/-/cookies-next-5.1.0.tgz\",\n      \"integrity\": \"sha512-9Ekne+q8hfziJtnT9c1yDUBqT0eDMGgPrfPl4bpR3xwQHLTd/8gbSf6+IEkP/pjGsDZt1TGbC6emYmFYRbIXwQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"cookie\": \"^1.0.1\"\n      },\n      \"peerDependencies\": {\n        \"next\": \">=15.0.0\",\n        \"react\": \">= 16.8.0\"\n      }\n    },\n    \"node_modules/create-require\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz\",\n      \"integrity\": \"sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==\",\n      \"devOptional\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/crelt\": {\n      \"version\": \"1.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz\",\n      \"integrity\": \"sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/cross-fetch\": {\n      \"version\": \"4.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz\",\n      \"integrity\": \"sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"node-fetch\": \"^2.7.0\"\n      }\n    },\n    \"node_modules/cross-spawn\": {\n      \"version\": \"7.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz\",\n      \"integrity\": \"sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"path-key\": \"^3.1.0\",\n        \"shebang-command\": \"^2.0.0\",\n        \"which\": \"^2.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 8\"\n      }\n    },\n    \"node_modules/crypt\": {\n      \"version\": \"0.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz\",\n      \"integrity\": \"sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==\",\n      \"license\": \"BSD-3-Clause\",\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/css-color-keywords\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz\",\n      \"integrity\": \"sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==\",\n      \"license\": \"ISC\",\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/css-to-react-native\": {\n      \"version\": \"3.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz\",\n      \"integrity\": \"sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"camelize\": \"^1.0.0\",\n        \"css-color-keywords\": \"^1.0.0\",\n        \"postcss-value-parser\": \"^4.0.2\"\n      }\n    },\n    \"node_modules/cssesc\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz\",\n      \"integrity\": \"sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==\",\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"cssesc\": \"bin/cssesc\"\n      },\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/csstype\": {\n      \"version\": \"3.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz\",\n      \"integrity\": \"sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/current-module-paths\": {\n      \"version\": \"1.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/current-module-paths/-/current-module-paths-1.1.2.tgz\",\n      \"integrity\": \"sha512-H4s4arcLx/ugbu1XkkgSvcUZax0L6tXUqnppGniQb8l5VjUKGHoayXE5RiriiPhYDd+kjZnaok1Uig13PKtKYQ==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12.17\"\n      }\n    },\n    \"node_modules/d3-array\": {\n      \"version\": \"3.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz\",\n      \"integrity\": \"sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"internmap\": \"1 - 2\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/d3-color\": {\n      \"version\": \"3.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz\",\n      \"integrity\": \"sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==\",\n      \"license\": \"ISC\",\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/d3-ease\": {\n      \"version\": \"3.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz\",\n      \"integrity\": \"sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==\",\n      \"license\": \"BSD-3-Clause\",\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/d3-format\": {\n      \"version\": \"3.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz\",\n      \"integrity\": \"sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==\",\n      \"license\": \"ISC\",\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/d3-interpolate\": {\n      \"version\": \"3.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz\",\n      \"integrity\": \"sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"d3-color\": \"1 - 3\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/d3-path\": {\n      \"version\": \"3.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz\",\n      \"integrity\": \"sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==\",\n      \"license\": \"ISC\",\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/d3-scale\": {\n      \"version\": \"4.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz\",\n      \"integrity\": \"sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"d3-array\": \"2.10.0 - 3\",\n        \"d3-format\": \"1 - 3\",\n        \"d3-interpolate\": \"1.2.0 - 3\",\n        \"d3-time\": \"2.1.1 - 3\",\n        \"d3-time-format\": \"2 - 4\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/d3-shape\": {\n      \"version\": \"3.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz\",\n      \"integrity\": \"sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"d3-path\": \"^3.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/d3-time\": {\n      \"version\": \"3.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz\",\n      \"integrity\": \"sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"d3-array\": \"2 - 3\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/d3-time-format\": {\n      \"version\": \"4.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz\",\n      \"integrity\": \"sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"d3-time\": \"1 - 3\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/d3-timer\": {\n      \"version\": \"3.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz\",\n      \"integrity\": \"sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==\",\n      \"license\": \"ISC\",\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/damerau-levenshtein\": {\n      \"version\": \"1.0.8\",\n      \"resolved\": \"https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz\",\n      \"integrity\": \"sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==\",\n      \"dev\": true,\n      \"license\": \"BSD-2-Clause\"\n    },\n    \"node_modules/data-view-buffer\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz\",\n      \"integrity\": \"sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.3\",\n        \"es-errors\": \"^1.3.0\",\n        \"is-data-view\": \"^1.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/data-view-byte-length\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz\",\n      \"integrity\": \"sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.3\",\n        \"es-errors\": \"^1.3.0\",\n        \"is-data-view\": \"^1.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/inspect-js\"\n      }\n    },\n    \"node_modules/data-view-byte-offset\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz\",\n      \"integrity\": \"sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.2\",\n        \"es-errors\": \"^1.3.0\",\n        \"is-data-view\": \"^1.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/date-fns\": {\n      \"version\": \"4.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz\",\n      \"integrity\": \"sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/kossnocorp\"\n      }\n    },\n    \"node_modules/date-fns-jalali\": {\n      \"version\": \"4.1.0-0\",\n      \"resolved\": \"https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz\",\n      \"integrity\": \"sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/debug\": {\n      \"version\": \"4.4.3\",\n      \"resolved\": \"https://registry.npmjs.org/debug/-/debug-4.4.3.tgz\",\n      \"integrity\": \"sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ms\": \"^2.1.3\"\n      },\n      \"engines\": {\n        \"node\": \">=6.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"supports-color\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/decimal.js-light\": {\n      \"version\": \"2.5.1\",\n      \"resolved\": \"https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz\",\n      \"integrity\": \"sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/decode-named-character-reference\": {\n      \"version\": \"1.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz\",\n      \"integrity\": \"sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"character-entities\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/deep-is\": {\n      \"version\": \"0.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz\",\n      \"integrity\": \"sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/deepmerge\": {\n      \"version\": \"4.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz\",\n      \"integrity\": \"sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/define-data-property\": {\n      \"version\": \"1.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz\",\n      \"integrity\": \"sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"es-define-property\": \"^1.0.0\",\n        \"es-errors\": \"^1.3.0\",\n        \"gopd\": \"^1.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/define-properties\": {\n      \"version\": \"1.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz\",\n      \"integrity\": \"sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"define-data-property\": \"^1.0.1\",\n        \"has-property-descriptors\": \"^1.0.0\",\n        \"object-keys\": \"^1.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/dequal\": {\n      \"version\": \"2.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz\",\n      \"integrity\": \"sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/detect-libc\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz\",\n      \"integrity\": \"sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==\",\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/detect-node-es\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz\",\n      \"integrity\": \"sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/devlop\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz\",\n      \"integrity\": \"sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"dequal\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/didyoumean\": {\n      \"version\": \"1.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz\",\n      \"integrity\": \"sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==\",\n      \"license\": \"Apache-2.0\"\n    },\n    \"node_modules/diff\": {\n      \"version\": \"4.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/diff/-/diff-4.0.4.tgz\",\n      \"integrity\": \"sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==\",\n      \"devOptional\": true,\n      \"license\": \"BSD-3-Clause\",\n      \"engines\": {\n        \"node\": \">=0.3.1\"\n      }\n    },\n    \"node_modules/dlv\": {\n      \"version\": \"1.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz\",\n      \"integrity\": \"sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/dnd-core\": {\n      \"version\": \"16.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz\",\n      \"integrity\": \"sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@react-dnd/asap\": \"^5.0.1\",\n        \"@react-dnd/invariant\": \"^4.0.1\",\n        \"redux\": \"^4.2.0\"\n      }\n    },\n    \"node_modules/doctrine\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz\",\n      \"integrity\": \"sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"esutils\": \"^2.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/dom-accessibility-api\": {\n      \"version\": \"0.5.16\",\n      \"resolved\": \"https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz\",\n      \"integrity\": \"sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/dom-helpers\": {\n      \"version\": \"5.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz\",\n      \"integrity\": \"sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/runtime\": \"^7.8.7\",\n        \"csstype\": \"^3.0.2\"\n      }\n    },\n    \"node_modules/dom-serializer\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz\",\n      \"integrity\": \"sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"domelementtype\": \"^2.3.0\",\n        \"domhandler\": \"^5.0.2\",\n        \"entities\": \"^4.2.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/cheeriojs/dom-serializer?sponsor=1\"\n      }\n    },\n    \"node_modules/domelementtype\": {\n      \"version\": \"2.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz\",\n      \"integrity\": \"sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/fb55\"\n        }\n      ],\n      \"license\": \"BSD-2-Clause\"\n    },\n    \"node_modules/domhandler\": {\n      \"version\": \"5.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz\",\n      \"integrity\": \"sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==\",\n      \"license\": \"BSD-2-Clause\",\n      \"dependencies\": {\n        \"domelementtype\": \"^2.3.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/fb55/domhandler?sponsor=1\"\n      }\n    },\n    \"node_modules/dompurify\": {\n      \"version\": \"3.2.6\",\n      \"resolved\": \"https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz\",\n      \"integrity\": \"sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==\",\n      \"license\": \"(MPL-2.0 OR Apache-2.0)\",\n      \"optionalDependencies\": {\n        \"@types/trusted-types\": \"^2.0.7\"\n      }\n    },\n    \"node_modules/domutils\": {\n      \"version\": \"3.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz\",\n      \"integrity\": \"sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==\",\n      \"license\": \"BSD-2-Clause\",\n      \"dependencies\": {\n        \"dom-serializer\": \"^2.0.0\",\n        \"domelementtype\": \"^2.3.0\",\n        \"domhandler\": \"^5.0.3\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/fb55/domutils?sponsor=1\"\n      }\n    },\n    \"node_modules/dotenv\": {\n      \"version\": \"16.6.1\",\n      \"resolved\": \"https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz\",\n      \"integrity\": \"sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==\",\n      \"license\": \"BSD-2-Clause\",\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://dotenvx.com\"\n      }\n    },\n    \"node_modules/dunder-proto\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz\",\n      \"integrity\": \"sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind-apply-helpers\": \"^1.0.1\",\n        \"es-errors\": \"^1.3.0\",\n        \"gopd\": \"^1.2.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/eastasianwidth\": {\n      \"version\": \"0.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz\",\n      \"integrity\": \"sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/electron-to-chromium\": {\n      \"version\": \"1.5.249\",\n      \"resolved\": \"https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.249.tgz\",\n      \"integrity\": \"sha512-5vcfL3BBe++qZ5kuFhD/p8WOM1N9m3nwvJPULJx+4xf2usSlZFJ0qoNYO2fOX4hi3ocuDcmDobtA+5SFr4OmBg==\",\n      \"dev\": true,\n      \"license\": \"ISC\"\n    },\n    \"node_modules/emoji-regex\": {\n      \"version\": \"9.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz\",\n      \"integrity\": \"sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/emojilib\": {\n      \"version\": \"2.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz\",\n      \"integrity\": \"sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/emoticon\": {\n      \"version\": \"4.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/emoticon/-/emoticon-4.1.0.tgz\",\n      \"integrity\": \"sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/entities\": {\n      \"version\": \"4.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/entities/-/entities-4.5.0.tgz\",\n      \"integrity\": \"sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==\",\n      \"license\": \"BSD-2-Clause\",\n      \"engines\": {\n        \"node\": \">=0.12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/fb55/entities?sponsor=1\"\n      }\n    },\n    \"node_modules/es-abstract\": {\n      \"version\": \"1.24.0\",\n      \"resolved\": \"https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz\",\n      \"integrity\": \"sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"array-buffer-byte-length\": \"^1.0.2\",\n        \"arraybuffer.prototype.slice\": \"^1.0.4\",\n        \"available-typed-arrays\": \"^1.0.7\",\n        \"call-bind\": \"^1.0.8\",\n        \"call-bound\": \"^1.0.4\",\n        \"data-view-buffer\": \"^1.0.2\",\n        \"data-view-byte-length\": \"^1.0.2\",\n        \"data-view-byte-offset\": \"^1.0.1\",\n        \"es-define-property\": \"^1.0.1\",\n        \"es-errors\": \"^1.3.0\",\n        \"es-object-atoms\": \"^1.1.1\",\n        \"es-set-tostringtag\": \"^2.1.0\",\n        \"es-to-primitive\": \"^1.3.0\",\n        \"function.prototype.name\": \"^1.1.8\",\n        \"get-intrinsic\": \"^1.3.0\",\n        \"get-proto\": \"^1.0.1\",\n        \"get-symbol-description\": \"^1.1.0\",\n        \"globalthis\": \"^1.0.4\",\n        \"gopd\": \"^1.2.0\",\n        \"has-property-descriptors\": \"^1.0.2\",\n        \"has-proto\": \"^1.2.0\",\n        \"has-symbols\": \"^1.1.0\",\n        \"hasown\": \"^2.0.2\",\n        \"internal-slot\": \"^1.1.0\",\n        \"is-array-buffer\": \"^3.0.5\",\n        \"is-callable\": \"^1.2.7\",\n        \"is-data-view\": \"^1.0.2\",\n        \"is-negative-zero\": \"^2.0.3\",\n        \"is-regex\": \"^1.2.1\",\n        \"is-set\": \"^2.0.3\",\n        \"is-shared-array-buffer\": \"^1.0.4\",\n        \"is-string\": \"^1.1.1\",\n        \"is-typed-array\": \"^1.1.15\",\n        \"is-weakref\": \"^1.1.1\",\n        \"math-intrinsics\": \"^1.1.0\",\n        \"object-inspect\": \"^1.13.4\",\n        \"object-keys\": \"^1.1.1\",\n        \"object.assign\": \"^4.1.7\",\n        \"own-keys\": \"^1.0.1\",\n        \"regexp.prototype.flags\": \"^1.5.4\",\n        \"safe-array-concat\": \"^1.1.3\",\n        \"safe-push-apply\": \"^1.0.0\",\n        \"safe-regex-test\": \"^1.1.0\",\n        \"set-proto\": \"^1.0.0\",\n        \"stop-iteration-iterator\": \"^1.1.0\",\n        \"string.prototype.trim\": \"^1.2.10\",\n        \"string.prototype.trimend\": \"^1.0.9\",\n        \"string.prototype.trimstart\": \"^1.0.8\",\n        \"typed-array-buffer\": \"^1.0.3\",\n        \"typed-array-byte-length\": \"^1.0.3\",\n        \"typed-array-byte-offset\": \"^1.0.4\",\n        \"typed-array-length\": \"^1.0.7\",\n        \"unbox-primitive\": \"^1.1.0\",\n        \"which-typed-array\": \"^1.1.19\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/es-define-property\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz\",\n      \"integrity\": \"sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/es-errors\": {\n      \"version\": \"1.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz\",\n      \"integrity\": \"sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/es-iterator-helpers\": {\n      \"version\": \"1.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz\",\n      \"integrity\": \"sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.8\",\n        \"call-bound\": \"^1.0.3\",\n        \"define-properties\": \"^1.2.1\",\n        \"es-abstract\": \"^1.23.6\",\n        \"es-errors\": \"^1.3.0\",\n        \"es-set-tostringtag\": \"^2.0.3\",\n        \"function-bind\": \"^1.1.2\",\n        \"get-intrinsic\": \"^1.2.6\",\n        \"globalthis\": \"^1.0.4\",\n        \"gopd\": \"^1.2.0\",\n        \"has-property-descriptors\": \"^1.0.2\",\n        \"has-proto\": \"^1.2.0\",\n        \"has-symbols\": \"^1.1.0\",\n        \"internal-slot\": \"^1.1.0\",\n        \"iterator.prototype\": \"^1.1.4\",\n        \"safe-array-concat\": \"^1.1.3\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/es-object-atoms\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz\",\n      \"integrity\": \"sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"es-errors\": \"^1.3.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/es-set-tostringtag\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz\",\n      \"integrity\": \"sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"es-errors\": \"^1.3.0\",\n        \"get-intrinsic\": \"^1.2.6\",\n        \"has-tostringtag\": \"^1.0.2\",\n        \"hasown\": \"^2.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/es-shim-unscopables\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz\",\n      \"integrity\": \"sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"hasown\": \"^2.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/es-to-primitive\": {\n      \"version\": \"1.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz\",\n      \"integrity\": \"sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"is-callable\": \"^1.2.7\",\n        \"is-date-object\": \"^1.0.5\",\n        \"is-symbol\": \"^1.0.4\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/esbuild\": {\n      \"version\": \"0.25.5\",\n      \"resolved\": \"https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz\",\n      \"integrity\": \"sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==\",\n      \"dev\": true,\n      \"hasInstallScript\": true,\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"esbuild\": \"bin/esbuild\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"optionalDependencies\": {\n        \"@esbuild/aix-ppc64\": \"0.25.5\",\n        \"@esbuild/android-arm\": \"0.25.5\",\n        \"@esbuild/android-arm64\": \"0.25.5\",\n        \"@esbuild/android-x64\": \"0.25.5\",\n        \"@esbuild/darwin-arm64\": \"0.25.5\",\n        \"@esbuild/darwin-x64\": \"0.25.5\",\n        \"@esbuild/freebsd-arm64\": \"0.25.5\",\n        \"@esbuild/freebsd-x64\": \"0.25.5\",\n        \"@esbuild/linux-arm\": \"0.25.5\",\n        \"@esbuild/linux-arm64\": \"0.25.5\",\n        \"@esbuild/linux-ia32\": \"0.25.5\",\n        \"@esbuild/linux-loong64\": \"0.25.5\",\n        \"@esbuild/linux-mips64el\": \"0.25.5\",\n        \"@esbuild/linux-ppc64\": \"0.25.5\",\n        \"@esbuild/linux-riscv64\": \"0.25.5\",\n        \"@esbuild/linux-s390x\": \"0.25.5\",\n        \"@esbuild/linux-x64\": \"0.25.5\",\n        \"@esbuild/netbsd-arm64\": \"0.25.5\",\n        \"@esbuild/netbsd-x64\": \"0.25.5\",\n        \"@esbuild/openbsd-arm64\": \"0.25.5\",\n        \"@esbuild/openbsd-x64\": \"0.25.5\",\n        \"@esbuild/sunos-x64\": \"0.25.5\",\n        \"@esbuild/win32-arm64\": \"0.25.5\",\n        \"@esbuild/win32-ia32\": \"0.25.5\",\n        \"@esbuild/win32-x64\": \"0.25.5\"\n      }\n    },\n    \"node_modules/escalade\": {\n      \"version\": \"3.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz\",\n      \"integrity\": \"sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/escape-string-regexp\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz\",\n      \"integrity\": \"sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/eslint\": {\n      \"version\": \"9.39.3\",\n      \"resolved\": \"https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz\",\n      \"integrity\": \"sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@eslint-community/eslint-utils\": \"^4.8.0\",\n        \"@eslint-community/regexpp\": \"^4.12.1\",\n        \"@eslint/config-array\": \"^0.21.1\",\n        \"@eslint/config-helpers\": \"^0.4.2\",\n        \"@eslint/core\": \"^0.17.0\",\n        \"@eslint/eslintrc\": \"^3.3.1\",\n        \"@eslint/js\": \"9.39.3\",\n        \"@eslint/plugin-kit\": \"^0.4.1\",\n        \"@humanfs/node\": \"^0.16.6\",\n        \"@humanwhocodes/module-importer\": \"^1.0.1\",\n        \"@humanwhocodes/retry\": \"^0.4.2\",\n        \"@types/estree\": \"^1.0.6\",\n        \"ajv\": \"^6.12.4\",\n        \"chalk\": \"^4.0.0\",\n        \"cross-spawn\": \"^7.0.6\",\n        \"debug\": \"^4.3.2\",\n        \"escape-string-regexp\": \"^4.0.0\",\n        \"eslint-scope\": \"^8.4.0\",\n        \"eslint-visitor-keys\": \"^4.2.1\",\n        \"espree\": \"^10.4.0\",\n        \"esquery\": \"^1.5.0\",\n        \"esutils\": \"^2.0.2\",\n        \"fast-deep-equal\": \"^3.1.3\",\n        \"file-entry-cache\": \"^8.0.0\",\n        \"find-up\": \"^5.0.0\",\n        \"glob-parent\": \"^6.0.2\",\n        \"ignore\": \"^5.2.0\",\n        \"imurmurhash\": \"^0.1.4\",\n        \"is-glob\": \"^4.0.0\",\n        \"json-stable-stringify-without-jsonify\": \"^1.0.1\",\n        \"lodash.merge\": \"^4.6.2\",\n        \"minimatch\": \"^3.1.2\",\n        \"natural-compare\": \"^1.4.0\",\n        \"optionator\": \"^0.9.3\"\n      },\n      \"bin\": {\n        \"eslint\": \"bin/eslint.js\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://eslint.org/donate\"\n      },\n      \"peerDependencies\": {\n        \"jiti\": \"*\"\n      },\n      \"peerDependenciesMeta\": {\n        \"jiti\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/eslint-config-next\": {\n      \"version\": \"16.1.6\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.6.tgz\",\n      \"integrity\": \"sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@next/eslint-plugin-next\": \"16.1.6\",\n        \"eslint-import-resolver-node\": \"^0.3.6\",\n        \"eslint-import-resolver-typescript\": \"^3.5.2\",\n        \"eslint-plugin-import\": \"^2.32.0\",\n        \"eslint-plugin-jsx-a11y\": \"^6.10.0\",\n        \"eslint-plugin-react\": \"^7.37.0\",\n        \"eslint-plugin-react-hooks\": \"^7.0.0\",\n        \"globals\": \"16.4.0\",\n        \"typescript-eslint\": \"^8.46.0\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \">=9.0.0\",\n        \"typescript\": \">=3.3.1\"\n      },\n      \"peerDependenciesMeta\": {\n        \"typescript\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/eslint-config-next/node_modules/globals\": {\n      \"version\": \"16.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/globals/-/globals-16.4.0.tgz\",\n      \"integrity\": \"sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/eslint-import-resolver-node\": {\n      \"version\": \"0.3.9\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz\",\n      \"integrity\": \"sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"debug\": \"^3.2.7\",\n        \"is-core-module\": \"^2.13.0\",\n        \"resolve\": \"^1.22.4\"\n      }\n    },\n    \"node_modules/eslint-import-resolver-node/node_modules/debug\": {\n      \"version\": \"3.2.7\",\n      \"resolved\": \"https://registry.npmjs.org/debug/-/debug-3.2.7.tgz\",\n      \"integrity\": \"sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ms\": \"^2.1.1\"\n      }\n    },\n    \"node_modules/eslint-import-resolver-typescript\": {\n      \"version\": \"3.10.1\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz\",\n      \"integrity\": \"sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==\",\n      \"dev\": true,\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"@nolyfill/is-core-module\": \"1.0.39\",\n        \"debug\": \"^4.4.0\",\n        \"get-tsconfig\": \"^4.10.0\",\n        \"is-bun-module\": \"^2.0.0\",\n        \"stable-hash\": \"^0.0.5\",\n        \"tinyglobby\": \"^0.2.13\",\n        \"unrs-resolver\": \"^1.6.2\"\n      },\n      \"engines\": {\n        \"node\": \"^14.18.0 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint-import-resolver-typescript\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"*\",\n        \"eslint-plugin-import\": \"*\",\n        \"eslint-plugin-import-x\": \"*\"\n      },\n      \"peerDependenciesMeta\": {\n        \"eslint-plugin-import\": {\n          \"optional\": true\n        },\n        \"eslint-plugin-import-x\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/eslint-module-utils\": {\n      \"version\": \"2.12.1\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz\",\n      \"integrity\": \"sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"debug\": \"^3.2.7\"\n      },\n      \"engines\": {\n        \"node\": \">=4\"\n      },\n      \"peerDependenciesMeta\": {\n        \"eslint\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/eslint-module-utils/node_modules/debug\": {\n      \"version\": \"3.2.7\",\n      \"resolved\": \"https://registry.npmjs.org/debug/-/debug-3.2.7.tgz\",\n      \"integrity\": \"sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ms\": \"^2.1.1\"\n      }\n    },\n    \"node_modules/eslint-plugin-import\": {\n      \"version\": \"2.32.0\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz\",\n      \"integrity\": \"sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@rtsao/scc\": \"^1.1.0\",\n        \"array-includes\": \"^3.1.9\",\n        \"array.prototype.findlastindex\": \"^1.2.6\",\n        \"array.prototype.flat\": \"^1.3.3\",\n        \"array.prototype.flatmap\": \"^1.3.3\",\n        \"debug\": \"^3.2.7\",\n        \"doctrine\": \"^2.1.0\",\n        \"eslint-import-resolver-node\": \"^0.3.9\",\n        \"eslint-module-utils\": \"^2.12.1\",\n        \"hasown\": \"^2.0.2\",\n        \"is-core-module\": \"^2.16.1\",\n        \"is-glob\": \"^4.0.3\",\n        \"minimatch\": \"^3.1.2\",\n        \"object.fromentries\": \"^2.0.8\",\n        \"object.groupby\": \"^1.0.3\",\n        \"object.values\": \"^1.2.1\",\n        \"semver\": \"^6.3.1\",\n        \"string.prototype.trimend\": \"^1.0.9\",\n        \"tsconfig-paths\": \"^3.15.0\"\n      },\n      \"engines\": {\n        \"node\": \">=4\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9\"\n      }\n    },\n    \"node_modules/eslint-plugin-import/node_modules/debug\": {\n      \"version\": \"3.2.7\",\n      \"resolved\": \"https://registry.npmjs.org/debug/-/debug-3.2.7.tgz\",\n      \"integrity\": \"sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ms\": \"^2.1.1\"\n      }\n    },\n    \"node_modules/eslint-plugin-import/node_modules/semver\": {\n      \"version\": \"6.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/semver/-/semver-6.3.1.tgz\",\n      \"integrity\": \"sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==\",\n      \"dev\": true,\n      \"license\": \"ISC\",\n      \"bin\": {\n        \"semver\": \"bin/semver.js\"\n      }\n    },\n    \"node_modules/eslint-plugin-jsx-a11y\": {\n      \"version\": \"6.10.2\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz\",\n      \"integrity\": \"sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"aria-query\": \"^5.3.2\",\n        \"array-includes\": \"^3.1.8\",\n        \"array.prototype.flatmap\": \"^1.3.2\",\n        \"ast-types-flow\": \"^0.0.8\",\n        \"axe-core\": \"^4.10.0\",\n        \"axobject-query\": \"^4.1.0\",\n        \"damerau-levenshtein\": \"^1.0.8\",\n        \"emoji-regex\": \"^9.2.2\",\n        \"hasown\": \"^2.0.2\",\n        \"jsx-ast-utils\": \"^3.3.5\",\n        \"language-tags\": \"^1.0.9\",\n        \"minimatch\": \"^3.1.2\",\n        \"object.fromentries\": \"^2.0.8\",\n        \"safe-regex-test\": \"^1.0.3\",\n        \"string.prototype.includes\": \"^2.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=4.0\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9\"\n      }\n    },\n    \"node_modules/eslint-plugin-react\": {\n      \"version\": \"7.37.5\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz\",\n      \"integrity\": \"sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"array-includes\": \"^3.1.8\",\n        \"array.prototype.findlast\": \"^1.2.5\",\n        \"array.prototype.flatmap\": \"^1.3.3\",\n        \"array.prototype.tosorted\": \"^1.1.4\",\n        \"doctrine\": \"^2.1.0\",\n        \"es-iterator-helpers\": \"^1.2.1\",\n        \"estraverse\": \"^5.3.0\",\n        \"hasown\": \"^2.0.2\",\n        \"jsx-ast-utils\": \"^2.4.1 || ^3.0.0\",\n        \"minimatch\": \"^3.1.2\",\n        \"object.entries\": \"^1.1.9\",\n        \"object.fromentries\": \"^2.0.8\",\n        \"object.values\": \"^1.2.1\",\n        \"prop-types\": \"^15.8.1\",\n        \"resolve\": \"^2.0.0-next.5\",\n        \"semver\": \"^6.3.1\",\n        \"string.prototype.matchall\": \"^4.0.12\",\n        \"string.prototype.repeat\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=4\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7\"\n      }\n    },\n    \"node_modules/eslint-plugin-react-hooks\": {\n      \"version\": \"7.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz\",\n      \"integrity\": \"sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/core\": \"^7.24.4\",\n        \"@babel/parser\": \"^7.24.4\",\n        \"hermes-parser\": \"^0.25.1\",\n        \"zod\": \"^3.25.0 || ^4.0.0\",\n        \"zod-validation-error\": \"^3.5.0 || ^4.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0\"\n      }\n    },\n    \"node_modules/eslint-plugin-react/node_modules/resolve\": {\n      \"version\": \"2.0.0-next.5\",\n      \"resolved\": \"https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz\",\n      \"integrity\": \"sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"is-core-module\": \"^2.13.0\",\n        \"path-parse\": \"^1.0.7\",\n        \"supports-preserve-symlinks-flag\": \"^1.0.0\"\n      },\n      \"bin\": {\n        \"resolve\": \"bin/resolve\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/eslint-plugin-react/node_modules/semver\": {\n      \"version\": \"6.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/semver/-/semver-6.3.1.tgz\",\n      \"integrity\": \"sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==\",\n      \"dev\": true,\n      \"license\": \"ISC\",\n      \"bin\": {\n        \"semver\": \"bin/semver.js\"\n      }\n    },\n    \"node_modules/eslint-scope\": {\n      \"version\": \"8.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz\",\n      \"integrity\": \"sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==\",\n      \"dev\": true,\n      \"license\": \"BSD-2-Clause\",\n      \"dependencies\": {\n        \"esrecurse\": \"^4.3.0\",\n        \"estraverse\": \"^5.2.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/eslint-visitor-keys\": {\n      \"version\": \"4.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz\",\n      \"integrity\": \"sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/eslint/node_modules/ansi-styles\": {\n      \"version\": \"4.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz\",\n      \"integrity\": \"sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"color-convert\": \"^2.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/ansi-styles?sponsor=1\"\n      }\n    },\n    \"node_modules/eslint/node_modules/chalk\": {\n      \"version\": \"4.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz\",\n      \"integrity\": \"sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ansi-styles\": \"^4.1.0\",\n        \"supports-color\": \"^7.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/chalk?sponsor=1\"\n      }\n    },\n    \"node_modules/espree\": {\n      \"version\": \"10.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/espree/-/espree-10.4.0.tgz\",\n      \"integrity\": \"sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==\",\n      \"dev\": true,\n      \"license\": \"BSD-2-Clause\",\n      \"dependencies\": {\n        \"acorn\": \"^8.15.0\",\n        \"acorn-jsx\": \"^5.3.2\",\n        \"eslint-visitor-keys\": \"^4.2.1\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/esquery\": {\n      \"version\": \"1.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz\",\n      \"integrity\": \"sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==\",\n      \"dev\": true,\n      \"license\": \"BSD-3-Clause\",\n      \"dependencies\": {\n        \"estraverse\": \"^5.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10\"\n      }\n    },\n    \"node_modules/esrecurse\": {\n      \"version\": \"4.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz\",\n      \"integrity\": \"sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==\",\n      \"dev\": true,\n      \"license\": \"BSD-2-Clause\",\n      \"dependencies\": {\n        \"estraverse\": \"^5.2.0\"\n      },\n      \"engines\": {\n        \"node\": \">=4.0\"\n      }\n    },\n    \"node_modules/estraverse\": {\n      \"version\": \"5.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz\",\n      \"integrity\": \"sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==\",\n      \"dev\": true,\n      \"license\": \"BSD-2-Clause\",\n      \"engines\": {\n        \"node\": \">=4.0\"\n      }\n    },\n    \"node_modules/estree-util-is-identifier-name\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz\",\n      \"integrity\": \"sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/esutils\": {\n      \"version\": \"2.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz\",\n      \"integrity\": \"sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==\",\n      \"dev\": true,\n      \"license\": \"BSD-2-Clause\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/eventemitter3\": {\n      \"version\": \"4.0.7\",\n      \"resolved\": \"https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz\",\n      \"integrity\": \"sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/extend\": {\n      \"version\": \"3.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/extend/-/extend-3.0.2.tgz\",\n      \"integrity\": \"sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/fast-deep-equal\": {\n      \"version\": \"3.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz\",\n      \"integrity\": \"sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/fast-equals\": {\n      \"version\": \"5.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz\",\n      \"integrity\": \"sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6.0.0\"\n      }\n    },\n    \"node_modules/fast-glob\": {\n      \"version\": \"3.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz\",\n      \"integrity\": \"sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@nodelib/fs.stat\": \"^2.0.2\",\n        \"@nodelib/fs.walk\": \"^1.2.3\",\n        \"glob-parent\": \"^5.1.2\",\n        \"merge2\": \"^1.3.0\",\n        \"micromatch\": \"^4.0.4\"\n      },\n      \"engines\": {\n        \"node\": \">=8.6.0\"\n      }\n    },\n    \"node_modules/fast-glob/node_modules/glob-parent\": {\n      \"version\": \"5.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz\",\n      \"integrity\": \"sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==\",\n      \"dev\": true,\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"is-glob\": \"^4.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 6\"\n      }\n    },\n    \"node_modules/fast-json-stable-stringify\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz\",\n      \"integrity\": \"sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/fast-levenshtein\": {\n      \"version\": \"2.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz\",\n      \"integrity\": \"sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/fastq\": {\n      \"version\": \"1.19.1\",\n      \"resolved\": \"https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz\",\n      \"integrity\": \"sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"reusify\": \"^1.0.4\"\n      }\n    },\n    \"node_modules/fault\": {\n      \"version\": \"1.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/fault/-/fault-1.0.4.tgz\",\n      \"integrity\": \"sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"format\": \"^0.2.0\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/file-entry-cache\": {\n      \"version\": \"8.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz\",\n      \"integrity\": \"sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"flat-cache\": \"^4.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=16.0.0\"\n      }\n    },\n    \"node_modules/file-set\": {\n      \"version\": \"5.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/file-set/-/file-set-5.2.2.tgz\",\n      \"integrity\": \"sha512-/KgJI1V/QaDK4enOk/E2xMFk1cTWJghEr7UmWiRZfZ6upt6gQCfMn4jJ7aOm64OKurj4TaVnSSgSDqv5ZKYA3A==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"array-back\": \"^6.2.2\",\n        \"fast-glob\": \"^3.3.2\"\n      },\n      \"engines\": {\n        \"node\": \">=12.17\"\n      },\n      \"peerDependencies\": {\n        \"@75lb/nature\": \"latest\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@75lb/nature\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/file-set/node_modules/fast-glob\": {\n      \"version\": \"3.3.3\",\n      \"resolved\": \"https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz\",\n      \"integrity\": \"sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@nodelib/fs.stat\": \"^2.0.2\",\n        \"@nodelib/fs.walk\": \"^1.2.3\",\n        \"glob-parent\": \"^5.1.2\",\n        \"merge2\": \"^1.3.0\",\n        \"micromatch\": \"^4.0.8\"\n      },\n      \"engines\": {\n        \"node\": \">=8.6.0\"\n      }\n    },\n    \"node_modules/file-set/node_modules/glob-parent\": {\n      \"version\": \"5.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz\",\n      \"integrity\": \"sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"is-glob\": \"^4.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 6\"\n      }\n    },\n    \"node_modules/fill-range\": {\n      \"version\": \"7.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz\",\n      \"integrity\": \"sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"to-regex-range\": \"^5.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/find-up\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz\",\n      \"integrity\": \"sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"locate-path\": \"^6.0.0\",\n        \"path-exists\": \"^4.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/flat-cache\": {\n      \"version\": \"4.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz\",\n      \"integrity\": \"sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"flatted\": \"^3.2.9\",\n        \"keyv\": \"^4.5.4\"\n      },\n      \"engines\": {\n        \"node\": \">=16\"\n      }\n    },\n    \"node_modules/flatted\": {\n      \"version\": \"3.3.3\",\n      \"resolved\": \"https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz\",\n      \"integrity\": \"sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==\",\n      \"dev\": true,\n      \"license\": \"ISC\"\n    },\n    \"node_modules/for-each\": {\n      \"version\": \"0.3.5\",\n      \"resolved\": \"https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz\",\n      \"integrity\": \"sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"is-callable\": \"^1.2.7\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/foreground-child\": {\n      \"version\": \"3.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz\",\n      \"integrity\": \"sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"cross-spawn\": \"^7.0.6\",\n        \"signal-exit\": \"^4.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=14\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/format\": {\n      \"version\": \"0.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/format/-/format-0.2.2.tgz\",\n      \"integrity\": \"sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==\",\n      \"engines\": {\n        \"node\": \">=0.4.x\"\n      }\n    },\n    \"node_modules/framer-motion\": {\n      \"version\": \"12.19.2\",\n      \"resolved\": \"https://registry.npmjs.org/framer-motion/-/framer-motion-12.19.2.tgz\",\n      \"integrity\": \"sha512-0cWMLkYr+i0emeXC4hkLF+5aYpzo32nRdQ0D/5DI460B3O7biQ3l2BpDzIGsAHYuZ0fpBP0DC8XBkVf6RPAlZw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"motion-dom\": \"^12.19.0\",\n        \"motion-utils\": \"^12.19.0\",\n        \"tslib\": \"^2.4.0\"\n      },\n      \"peerDependencies\": {\n        \"@emotion/is-prop-valid\": \"*\",\n        \"react\": \"^18.0.0 || ^19.0.0\",\n        \"react-dom\": \"^18.0.0 || ^19.0.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@emotion/is-prop-valid\": {\n          \"optional\": true\n        },\n        \"react\": {\n          \"optional\": true\n        },\n        \"react-dom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/fs\": {\n      \"version\": \"0.0.1-security\",\n      \"resolved\": \"https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz\",\n      \"integrity\": \"sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==\",\n      \"license\": \"ISC\"\n    },\n    \"node_modules/fs-extra\": {\n      \"version\": \"11.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz\",\n      \"integrity\": \"sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"graceful-fs\": \"^4.2.0\",\n        \"jsonfile\": \"^6.0.1\",\n        \"universalify\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=14.14\"\n      }\n    },\n    \"node_modules/fsevents\": {\n      \"version\": \"2.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz\",\n      \"integrity\": \"sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==\",\n      \"hasInstallScript\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"engines\": {\n        \"node\": \"^8.16.0 || ^10.6.0 || >=11.0.0\"\n      }\n    },\n    \"node_modules/function-bind\": {\n      \"version\": \"1.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz\",\n      \"integrity\": \"sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/function.prototype.name\": {\n      \"version\": \"1.1.8\",\n      \"resolved\": \"https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz\",\n      \"integrity\": \"sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.8\",\n        \"call-bound\": \"^1.0.3\",\n        \"define-properties\": \"^1.2.1\",\n        \"functions-have-names\": \"^1.2.3\",\n        \"hasown\": \"^2.0.2\",\n        \"is-callable\": \"^1.2.7\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/functions-have-names\": {\n      \"version\": \"1.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz\",\n      \"integrity\": \"sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/gensync\": {\n      \"version\": \"1.0.0-beta.2\",\n      \"resolved\": \"https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz\",\n      \"integrity\": \"sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/get-east-asian-width\": {\n      \"version\": \"1.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz\",\n      \"integrity\": \"sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/get-intrinsic\": {\n      \"version\": \"1.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz\",\n      \"integrity\": \"sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind-apply-helpers\": \"^1.0.2\",\n        \"es-define-property\": \"^1.0.1\",\n        \"es-errors\": \"^1.3.0\",\n        \"es-object-atoms\": \"^1.1.1\",\n        \"function-bind\": \"^1.1.2\",\n        \"get-proto\": \"^1.0.1\",\n        \"gopd\": \"^1.2.0\",\n        \"has-symbols\": \"^1.1.0\",\n        \"hasown\": \"^2.0.2\",\n        \"math-intrinsics\": \"^1.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/get-nonce\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz\",\n      \"integrity\": \"sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/get-proto\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz\",\n      \"integrity\": \"sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"dunder-proto\": \"^1.0.1\",\n        \"es-object-atoms\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/get-symbol-description\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz\",\n      \"integrity\": \"sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.3\",\n        \"es-errors\": \"^1.3.0\",\n        \"get-intrinsic\": \"^1.2.6\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/get-tsconfig\": {\n      \"version\": \"4.10.1\",\n      \"resolved\": \"https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz\",\n      \"integrity\": \"sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"resolve-pkg-maps\": \"^1.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/privatenumber/get-tsconfig?sponsor=1\"\n      }\n    },\n    \"node_modules/glob\": {\n      \"version\": \"11.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/glob/-/glob-11.1.0.tgz\",\n      \"integrity\": \"sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==\",\n      \"license\": \"BlueOak-1.0.0\",\n      \"dependencies\": {\n        \"foreground-child\": \"^3.3.1\",\n        \"jackspeak\": \"^4.1.1\",\n        \"minimatch\": \"^10.1.1\",\n        \"minipass\": \"^7.1.2\",\n        \"package-json-from-dist\": \"^1.0.0\",\n        \"path-scurry\": \"^2.0.0\"\n      },\n      \"bin\": {\n        \"glob\": \"dist/esm/bin.mjs\"\n      },\n      \"engines\": {\n        \"node\": \"20 || >=22\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/glob-parent\": {\n      \"version\": \"6.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz\",\n      \"integrity\": \"sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"is-glob\": \"^4.0.3\"\n      },\n      \"engines\": {\n        \"node\": \">=10.13.0\"\n      }\n    },\n    \"node_modules/globals\": {\n      \"version\": \"14.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/globals/-/globals-14.0.0.tgz\",\n      \"integrity\": \"sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/globalthis\": {\n      \"version\": \"1.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz\",\n      \"integrity\": \"sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"define-properties\": \"^1.2.1\",\n        \"gopd\": \"^1.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/gopd\": {\n      \"version\": \"1.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz\",\n      \"integrity\": \"sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/graceful-fs\": {\n      \"version\": \"4.2.11\",\n      \"resolved\": \"https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz\",\n      \"integrity\": \"sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==\",\n      \"license\": \"ISC\"\n    },\n    \"node_modules/has-bigints\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz\",\n      \"integrity\": \"sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/has-flag\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz\",\n      \"integrity\": \"sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/has-property-descriptors\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz\",\n      \"integrity\": \"sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"es-define-property\": \"^1.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/has-proto\": {\n      \"version\": \"1.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz\",\n      \"integrity\": \"sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"dunder-proto\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/has-symbols\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz\",\n      \"integrity\": \"sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/has-tostringtag\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz\",\n      \"integrity\": \"sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"has-symbols\": \"^1.0.3\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/hasown\": {\n      \"version\": \"2.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz\",\n      \"integrity\": \"sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"function-bind\": \"^1.1.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/hast-util-from-dom\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz\",\n      \"integrity\": \"sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"hastscript\": \"^9.0.0\",\n        \"web-namespaces\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/hast-util-from-html\": {\n      \"version\": \"2.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz\",\n      \"integrity\": \"sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"devlop\": \"^1.1.0\",\n        \"hast-util-from-parse5\": \"^8.0.0\",\n        \"parse5\": \"^7.0.0\",\n        \"vfile\": \"^6.0.0\",\n        \"vfile-message\": \"^4.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/hast-util-from-html-isomorphic\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz\",\n      \"integrity\": \"sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"hast-util-from-dom\": \"^5.0.0\",\n        \"hast-util-from-html\": \"^2.0.0\",\n        \"unist-util-remove-position\": \"^5.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/hast-util-from-parse5\": {\n      \"version\": \"8.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz\",\n      \"integrity\": \"sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"@types/unist\": \"^3.0.0\",\n        \"devlop\": \"^1.0.0\",\n        \"hastscript\": \"^9.0.0\",\n        \"property-information\": \"^7.0.0\",\n        \"vfile\": \"^6.0.0\",\n        \"vfile-location\": \"^5.0.0\",\n        \"web-namespaces\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/hast-util-is-element\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz\",\n      \"integrity\": \"sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/hast-util-parse-selector\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz\",\n      \"integrity\": \"sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/hast-util-raw\": {\n      \"version\": \"9.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz\",\n      \"integrity\": \"sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"@types/unist\": \"^3.0.0\",\n        \"@ungap/structured-clone\": \"^1.0.0\",\n        \"hast-util-from-parse5\": \"^8.0.0\",\n        \"hast-util-to-parse5\": \"^8.0.0\",\n        \"html-void-elements\": \"^3.0.0\",\n        \"mdast-util-to-hast\": \"^13.0.0\",\n        \"parse5\": \"^7.0.0\",\n        \"unist-util-position\": \"^5.0.0\",\n        \"unist-util-visit\": \"^5.0.0\",\n        \"vfile\": \"^6.0.0\",\n        \"web-namespaces\": \"^2.0.0\",\n        \"zwitch\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/hast-util-sanitize\": {\n      \"version\": \"5.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz\",\n      \"integrity\": \"sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"@ungap/structured-clone\": \"^1.0.0\",\n        \"unist-util-position\": \"^5.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/hast-util-to-html\": {\n      \"version\": \"9.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz\",\n      \"integrity\": \"sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"@types/unist\": \"^3.0.0\",\n        \"ccount\": \"^2.0.0\",\n        \"comma-separated-tokens\": \"^2.0.0\",\n        \"hast-util-whitespace\": \"^3.0.0\",\n        \"html-void-elements\": \"^3.0.0\",\n        \"mdast-util-to-hast\": \"^13.0.0\",\n        \"property-information\": \"^7.0.0\",\n        \"space-separated-tokens\": \"^2.0.0\",\n        \"stringify-entities\": \"^4.0.0\",\n        \"zwitch\": \"^2.0.4\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/hast-util-to-jsx-runtime\": {\n      \"version\": \"2.3.6\",\n      \"resolved\": \"https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz\",\n      \"integrity\": \"sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/estree\": \"^1.0.0\",\n        \"@types/hast\": \"^3.0.0\",\n        \"@types/unist\": \"^3.0.0\",\n        \"comma-separated-tokens\": \"^2.0.0\",\n        \"devlop\": \"^1.0.0\",\n        \"estree-util-is-identifier-name\": \"^3.0.0\",\n        \"hast-util-whitespace\": \"^3.0.0\",\n        \"mdast-util-mdx-expression\": \"^2.0.0\",\n        \"mdast-util-mdx-jsx\": \"^3.0.0\",\n        \"mdast-util-mdxjs-esm\": \"^2.0.0\",\n        \"property-information\": \"^7.0.0\",\n        \"space-separated-tokens\": \"^2.0.0\",\n        \"style-to-js\": \"^1.0.0\",\n        \"unist-util-position\": \"^5.0.0\",\n        \"vfile-message\": \"^4.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/hast-util-to-parse5\": {\n      \"version\": \"8.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz\",\n      \"integrity\": \"sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"comma-separated-tokens\": \"^2.0.0\",\n        \"devlop\": \"^1.0.0\",\n        \"property-information\": \"^6.0.0\",\n        \"space-separated-tokens\": \"^2.0.0\",\n        \"web-namespaces\": \"^2.0.0\",\n        \"zwitch\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/hast-util-to-parse5/node_modules/property-information\": {\n      \"version\": \"6.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz\",\n      \"integrity\": \"sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/hast-util-to-text\": {\n      \"version\": \"4.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz\",\n      \"integrity\": \"sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"@types/unist\": \"^3.0.0\",\n        \"hast-util-is-element\": \"^3.0.0\",\n        \"unist-util-find-after\": \"^5.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/hast-util-whitespace\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz\",\n      \"integrity\": \"sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/hastscript\": {\n      \"version\": \"9.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz\",\n      \"integrity\": \"sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"comma-separated-tokens\": \"^2.0.0\",\n        \"hast-util-parse-selector\": \"^4.0.0\",\n        \"property-information\": \"^7.0.0\",\n        \"space-separated-tokens\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/hermes-estree\": {\n      \"version\": \"0.25.1\",\n      \"resolved\": \"https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz\",\n      \"integrity\": \"sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/hermes-parser\": {\n      \"version\": \"0.25.1\",\n      \"resolved\": \"https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz\",\n      \"integrity\": \"sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"hermes-estree\": \"0.25.1\"\n      }\n    },\n    \"node_modules/highlight.js\": {\n      \"version\": \"10.7.3\",\n      \"resolved\": \"https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz\",\n      \"integrity\": \"sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==\",\n      \"license\": \"BSD-3-Clause\",\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/highlightjs-vue\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz\",\n      \"integrity\": \"sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==\",\n      \"license\": \"CC0-1.0\"\n    },\n    \"node_modules/hoist-non-react-statics\": {\n      \"version\": \"3.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz\",\n      \"integrity\": \"sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==\",\n      \"license\": \"BSD-3-Clause\",\n      \"dependencies\": {\n        \"react-is\": \"^16.7.0\"\n      }\n    },\n    \"node_modules/html-to-text\": {\n      \"version\": \"9.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz\",\n      \"integrity\": \"sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@selderee/plugin-htmlparser2\": \"^0.11.0\",\n        \"deepmerge\": \"^4.3.1\",\n        \"dom-serializer\": \"^2.0.0\",\n        \"htmlparser2\": \"^8.0.2\",\n        \"selderee\": \"^0.11.0\"\n      },\n      \"engines\": {\n        \"node\": \">=14\"\n      }\n    },\n    \"node_modules/html-url-attributes\": {\n      \"version\": \"3.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz\",\n      \"integrity\": \"sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/html-void-elements\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz\",\n      \"integrity\": \"sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/htmlparser2\": {\n      \"version\": \"8.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz\",\n      \"integrity\": \"sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==\",\n      \"funding\": [\n        \"https://github.com/fb55/htmlparser2?sponsor=1\",\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/fb55\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"domelementtype\": \"^2.3.0\",\n        \"domhandler\": \"^5.0.3\",\n        \"domutils\": \"^3.0.1\",\n        \"entities\": \"^4.4.0\"\n      }\n    },\n    \"node_modules/ignore\": {\n      \"version\": \"5.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz\",\n      \"integrity\": \"sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 4\"\n      }\n    },\n    \"node_modules/import-fresh\": {\n      \"version\": \"3.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz\",\n      \"integrity\": \"sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"parent-module\": \"^1.0.0\",\n        \"resolve-from\": \"^4.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=6\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/imurmurhash\": {\n      \"version\": \"0.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz\",\n      \"integrity\": \"sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=0.8.19\"\n      }\n    },\n    \"node_modules/inherits\": {\n      \"version\": \"2.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz\",\n      \"integrity\": \"sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==\",\n      \"license\": \"ISC\"\n    },\n    \"node_modules/inline-style-parser\": {\n      \"version\": \"0.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz\",\n      \"integrity\": \"sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/internal-slot\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz\",\n      \"integrity\": \"sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"es-errors\": \"^1.3.0\",\n        \"hasown\": \"^2.0.2\",\n        \"side-channel\": \"^1.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/internmap\": {\n      \"version\": \"2.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz\",\n      \"integrity\": \"sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==\",\n      \"license\": \"ISC\",\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/is-alphabetical\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz\",\n      \"integrity\": \"sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/is-alphanumerical\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz\",\n      \"integrity\": \"sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"is-alphabetical\": \"^2.0.0\",\n        \"is-decimal\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/is-array-buffer\": {\n      \"version\": \"3.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz\",\n      \"integrity\": \"sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.8\",\n        \"call-bound\": \"^1.0.3\",\n        \"get-intrinsic\": \"^1.2.6\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-async-function\": {\n      \"version\": \"2.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz\",\n      \"integrity\": \"sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"async-function\": \"^1.0.0\",\n        \"call-bound\": \"^1.0.3\",\n        \"get-proto\": \"^1.0.1\",\n        \"has-tostringtag\": \"^1.0.2\",\n        \"safe-regex-test\": \"^1.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-bigint\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz\",\n      \"integrity\": \"sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"has-bigints\": \"^1.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-binary-path\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz\",\n      \"integrity\": \"sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"binary-extensions\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/is-boolean-object\": {\n      \"version\": \"1.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz\",\n      \"integrity\": \"sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.3\",\n        \"has-tostringtag\": \"^1.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-buffer\": {\n      \"version\": \"1.1.6\",\n      \"resolved\": \"https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz\",\n      \"integrity\": \"sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/is-bun-module\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz\",\n      \"integrity\": \"sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"semver\": \"^7.7.1\"\n      }\n    },\n    \"node_modules/is-callable\": {\n      \"version\": \"1.2.7\",\n      \"resolved\": \"https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz\",\n      \"integrity\": \"sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-core-module\": {\n      \"version\": \"2.16.1\",\n      \"resolved\": \"https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz\",\n      \"integrity\": \"sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"hasown\": \"^2.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-data-view\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz\",\n      \"integrity\": \"sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.2\",\n        \"get-intrinsic\": \"^1.2.6\",\n        \"is-typed-array\": \"^1.1.13\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-date-object\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz\",\n      \"integrity\": \"sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.2\",\n        \"has-tostringtag\": \"^1.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-decimal\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz\",\n      \"integrity\": \"sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/is-extglob\": {\n      \"version\": \"2.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz\",\n      \"integrity\": \"sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/is-finalizationregistry\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz\",\n      \"integrity\": \"sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.3\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-fullwidth-code-point\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz\",\n      \"integrity\": \"sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/is-generator-function\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz\",\n      \"integrity\": \"sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.3\",\n        \"get-proto\": \"^1.0.0\",\n        \"has-tostringtag\": \"^1.0.2\",\n        \"safe-regex-test\": \"^1.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-glob\": {\n      \"version\": \"4.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz\",\n      \"integrity\": \"sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"is-extglob\": \"^2.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/is-hexadecimal\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz\",\n      \"integrity\": \"sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/is-interactive\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz\",\n      \"integrity\": \"sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/is-map\": {\n      \"version\": \"2.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz\",\n      \"integrity\": \"sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-negative-zero\": {\n      \"version\": \"2.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz\",\n      \"integrity\": \"sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-node-process\": {\n      \"version\": \"1.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz\",\n      \"integrity\": \"sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/is-number\": {\n      \"version\": \"7.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz\",\n      \"integrity\": \"sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=0.12.0\"\n      }\n    },\n    \"node_modules/is-number-object\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz\",\n      \"integrity\": \"sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.3\",\n        \"has-tostringtag\": \"^1.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-plain-obj\": {\n      \"version\": \"4.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz\",\n      \"integrity\": \"sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/is-regex\": {\n      \"version\": \"1.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz\",\n      \"integrity\": \"sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.2\",\n        \"gopd\": \"^1.2.0\",\n        \"has-tostringtag\": \"^1.0.2\",\n        \"hasown\": \"^2.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-set\": {\n      \"version\": \"2.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz\",\n      \"integrity\": \"sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-shared-array-buffer\": {\n      \"version\": \"1.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz\",\n      \"integrity\": \"sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.3\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-string\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz\",\n      \"integrity\": \"sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.3\",\n        \"has-tostringtag\": \"^1.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-symbol\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz\",\n      \"integrity\": \"sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.2\",\n        \"has-symbols\": \"^1.1.0\",\n        \"safe-regex-test\": \"^1.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-typed-array\": {\n      \"version\": \"1.1.15\",\n      \"resolved\": \"https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz\",\n      \"integrity\": \"sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"which-typed-array\": \"^1.1.16\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-unicode-supported\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz\",\n      \"integrity\": \"sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/is-weakmap\": {\n      \"version\": \"2.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz\",\n      \"integrity\": \"sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-weakref\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz\",\n      \"integrity\": \"sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.3\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-weakset\": {\n      \"version\": \"2.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz\",\n      \"integrity\": \"sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.3\",\n        \"get-intrinsic\": \"^1.2.6\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/isarray\": {\n      \"version\": \"2.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz\",\n      \"integrity\": \"sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/isexe\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz\",\n      \"integrity\": \"sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==\",\n      \"license\": \"ISC\"\n    },\n    \"node_modules/iterator.prototype\": {\n      \"version\": \"1.1.5\",\n      \"resolved\": \"https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz\",\n      \"integrity\": \"sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"define-data-property\": \"^1.1.4\",\n        \"es-object-atoms\": \"^1.0.0\",\n        \"get-intrinsic\": \"^1.2.6\",\n        \"get-proto\": \"^1.0.0\",\n        \"has-symbols\": \"^1.1.0\",\n        \"set-function-name\": \"^2.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/jackspeak\": {\n      \"version\": \"4.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz\",\n      \"integrity\": \"sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==\",\n      \"license\": \"BlueOak-1.0.0\",\n      \"dependencies\": {\n        \"@isaacs/cliui\": \"^8.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"20 || >=22\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/jiti\": {\n      \"version\": \"2.4.2\",\n      \"resolved\": \"https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz\",\n      \"integrity\": \"sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==\",\n      \"devOptional\": true,\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"bin\": {\n        \"jiti\": \"lib/jiti-cli.mjs\"\n      }\n    },\n    \"node_modules/jose\": {\n      \"version\": \"5.10.0\",\n      \"resolved\": \"https://registry.npmjs.org/jose/-/jose-5.10.0.tgz\",\n      \"integrity\": \"sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/panva\"\n      }\n    },\n    \"node_modules/js-tokens\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz\",\n      \"integrity\": \"sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/js-yaml\": {\n      \"version\": \"4.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz\",\n      \"integrity\": \"sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"argparse\": \"^2.0.1\"\n      },\n      \"bin\": {\n        \"js-yaml\": \"bin/js-yaml.js\"\n      }\n    },\n    \"node_modules/js2xmlparser\": {\n      \"version\": \"4.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz\",\n      \"integrity\": \"sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==\",\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"xmlcreate\": \"^2.0.4\"\n      }\n    },\n    \"node_modules/jsdoc\": {\n      \"version\": \"4.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.4.tgz\",\n      \"integrity\": \"sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw==\",\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@babel/parser\": \"^7.20.15\",\n        \"@jsdoc/salty\": \"^0.2.1\",\n        \"@types/markdown-it\": \"^14.1.1\",\n        \"bluebird\": \"^3.7.2\",\n        \"catharsis\": \"^0.9.0\",\n        \"escape-string-regexp\": \"^2.0.0\",\n        \"js2xmlparser\": \"^4.0.2\",\n        \"klaw\": \"^3.0.0\",\n        \"markdown-it\": \"^14.1.0\",\n        \"markdown-it-anchor\": \"^8.6.7\",\n        \"marked\": \"^4.0.10\",\n        \"mkdirp\": \"^1.0.4\",\n        \"requizzle\": \"^0.2.3\",\n        \"strip-json-comments\": \"^3.1.0\",\n        \"underscore\": \"~1.13.2\"\n      },\n      \"bin\": {\n        \"jsdoc\": \"jsdoc.js\"\n      },\n      \"engines\": {\n        \"node\": \">=12.0.0\"\n      }\n    },\n    \"node_modules/jsdoc-api\": {\n      \"version\": \"9.3.4\",\n      \"resolved\": \"https://registry.npmjs.org/jsdoc-api/-/jsdoc-api-9.3.4.tgz\",\n      \"integrity\": \"sha512-di8lggLACEttpyAZ6WjKKafUP4wC4prAGjt40nMl7quDpp2nD7GmLt6/WxhRu9Q6IYoAAySsNeidBXYVAMwlqg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"array-back\": \"^6.2.2\",\n        \"cache-point\": \"^3.0.0\",\n        \"current-module-paths\": \"^1.1.2\",\n        \"file-set\": \"^5.2.2\",\n        \"jsdoc\": \"^4.0.4\",\n        \"object-to-spawn-args\": \"^2.0.1\",\n        \"walk-back\": \"^5.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">=12.17\"\n      },\n      \"peerDependencies\": {\n        \"@75lb/nature\": \"latest\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@75lb/nature\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/jsdoc/node_modules/escape-string-regexp\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz\",\n      \"integrity\": \"sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/jsesc\": {\n      \"version\": \"3.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz\",\n      \"integrity\": \"sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"jsesc\": \"bin/jsesc\"\n      },\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/json-buffer\": {\n      \"version\": \"3.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz\",\n      \"integrity\": \"sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/json-schema-traverse\": {\n      \"version\": \"0.4.1\",\n      \"resolved\": \"https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz\",\n      \"integrity\": \"sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/json-stable-stringify-without-jsonify\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz\",\n      \"integrity\": \"sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/json-stringify-safe\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz\",\n      \"integrity\": \"sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==\",\n      \"license\": \"ISC\"\n    },\n    \"node_modules/json5\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/json5/-/json5-1.0.2.tgz\",\n      \"integrity\": \"sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"minimist\": \"^1.2.0\"\n      },\n      \"bin\": {\n        \"json5\": \"lib/cli.js\"\n      }\n    },\n    \"node_modules/jsonfile\": {\n      \"version\": \"6.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz\",\n      \"integrity\": \"sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"universalify\": \"^2.0.0\"\n      },\n      \"optionalDependencies\": {\n        \"graceful-fs\": \"^4.1.6\"\n      }\n    },\n    \"node_modules/jsx-ast-utils\": {\n      \"version\": \"3.3.5\",\n      \"resolved\": \"https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz\",\n      \"integrity\": \"sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"array-includes\": \"^3.1.6\",\n        \"array.prototype.flat\": \"^1.3.1\",\n        \"object.assign\": \"^4.1.4\",\n        \"object.values\": \"^1.1.6\"\n      },\n      \"engines\": {\n        \"node\": \">=4.0\"\n      }\n    },\n    \"node_modules/katex\": {\n      \"version\": \"0.16.22\",\n      \"resolved\": \"https://registry.npmjs.org/katex/-/katex-0.16.22.tgz\",\n      \"integrity\": \"sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==\",\n      \"funding\": [\n        \"https://opencollective.com/katex\",\n        \"https://github.com/sponsors/katex\"\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"commander\": \"^8.3.0\"\n      },\n      \"bin\": {\n        \"katex\": \"cli.js\"\n      }\n    },\n    \"node_modules/katex/node_modules/commander\": {\n      \"version\": \"8.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/commander/-/commander-8.3.0.tgz\",\n      \"integrity\": \"sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 12\"\n      }\n    },\n    \"node_modules/keyv\": {\n      \"version\": \"4.5.4\",\n      \"resolved\": \"https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz\",\n      \"integrity\": \"sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"json-buffer\": \"3.0.1\"\n      }\n    },\n    \"node_modules/klaw\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz\",\n      \"integrity\": \"sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"graceful-fs\": \"^4.1.9\"\n      }\n    },\n    \"node_modules/language-subtag-registry\": {\n      \"version\": \"0.3.23\",\n      \"resolved\": \"https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz\",\n      \"integrity\": \"sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==\",\n      \"dev\": true,\n      \"license\": \"CC0-1.0\"\n    },\n    \"node_modules/language-tags\": {\n      \"version\": \"1.0.9\",\n      \"resolved\": \"https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz\",\n      \"integrity\": \"sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"language-subtag-registry\": \"^0.3.20\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10\"\n      }\n    },\n    \"node_modules/leac\": {\n      \"version\": \"0.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/leac/-/leac-0.6.0.tgz\",\n      \"integrity\": \"sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"url\": \"https://ko-fi.com/killymxi\"\n      }\n    },\n    \"node_modules/levn\": {\n      \"version\": \"0.4.1\",\n      \"resolved\": \"https://registry.npmjs.org/levn/-/levn-0.4.1.tgz\",\n      \"integrity\": \"sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"prelude-ls\": \"^1.2.1\",\n        \"type-check\": \"~0.4.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8.0\"\n      }\n    },\n    \"node_modules/lilconfig\": {\n      \"version\": \"3.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz\",\n      \"integrity\": \"sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=14\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/antonk52\"\n      }\n    },\n    \"node_modules/lines-and-columns\": {\n      \"version\": \"1.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz\",\n      \"integrity\": \"sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/linkify-it\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz\",\n      \"integrity\": \"sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"uc.micro\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/locate-path\": {\n      \"version\": \"6.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz\",\n      \"integrity\": \"sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"p-locate\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/lodash\": {\n      \"version\": \"4.17.23\",\n      \"resolved\": \"https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz\",\n      \"integrity\": \"sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/lodash.merge\": {\n      \"version\": \"4.6.2\",\n      \"resolved\": \"https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz\",\n      \"integrity\": \"sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/log-symbols\": {\n      \"version\": \"6.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz\",\n      \"integrity\": \"sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"chalk\": \"^5.3.0\",\n        \"is-unicode-supported\": \"^1.3.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/log-symbols/node_modules/is-unicode-supported\": {\n      \"version\": \"1.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz\",\n      \"integrity\": \"sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/longest-streak\": {\n      \"version\": \"3.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz\",\n      \"integrity\": \"sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/loose-envify\": {\n      \"version\": \"1.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz\",\n      \"integrity\": \"sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"js-tokens\": \"^3.0.0 || ^4.0.0\"\n      },\n      \"bin\": {\n        \"loose-envify\": \"cli.js\"\n      }\n    },\n    \"node_modules/lowlight\": {\n      \"version\": \"1.20.0\",\n      \"resolved\": \"https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz\",\n      \"integrity\": \"sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"fault\": \"^1.0.0\",\n        \"highlight.js\": \"~10.7.0\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/lru-cache\": {\n      \"version\": \"11.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz\",\n      \"integrity\": \"sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==\",\n      \"license\": \"ISC\",\n      \"engines\": {\n        \"node\": \"20 || >=22\"\n      }\n    },\n    \"node_modules/lucide-react\": {\n      \"version\": \"0.475.0\",\n      \"resolved\": \"https://registry.npmjs.org/lucide-react/-/lucide-react-0.475.0.tgz\",\n      \"integrity\": \"sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg==\",\n      \"license\": \"ISC\",\n      \"peerDependencies\": {\n        \"react\": \"^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0\"\n      }\n    },\n    \"node_modules/lz-string\": {\n      \"version\": \"1.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz\",\n      \"integrity\": \"sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==\",\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"lz-string\": \"bin/bin.js\"\n      }\n    },\n    \"node_modules/make-error\": {\n      \"version\": \"1.3.6\",\n      \"resolved\": \"https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz\",\n      \"integrity\": \"sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==\",\n      \"devOptional\": true,\n      \"license\": \"ISC\"\n    },\n    \"node_modules/markdown-it\": {\n      \"version\": \"14.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz\",\n      \"integrity\": \"sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"argparse\": \"^2.0.1\",\n        \"entities\": \"^4.4.0\",\n        \"linkify-it\": \"^5.0.0\",\n        \"mdurl\": \"^2.0.0\",\n        \"punycode.js\": \"^2.3.1\",\n        \"uc.micro\": \"^2.1.0\"\n      },\n      \"bin\": {\n        \"markdown-it\": \"bin/markdown-it.mjs\"\n      }\n    },\n    \"node_modules/markdown-it-anchor\": {\n      \"version\": \"8.6.7\",\n      \"resolved\": \"https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz\",\n      \"integrity\": \"sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==\",\n      \"license\": \"Unlicense\",\n      \"peerDependencies\": {\n        \"@types/markdown-it\": \"*\",\n        \"markdown-it\": \"*\"\n      }\n    },\n    \"node_modules/markdown-table\": {\n      \"version\": \"3.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz\",\n      \"integrity\": \"sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/marked\": {\n      \"version\": \"4.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/marked/-/marked-4.3.0.tgz\",\n      \"integrity\": \"sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==\",\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"marked\": \"bin/marked.js\"\n      },\n      \"engines\": {\n        \"node\": \">= 12\"\n      }\n    },\n    \"node_modules/math-intrinsics\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz\",\n      \"integrity\": \"sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/md-to-react-email\": {\n      \"version\": \"5.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/md-to-react-email/-/md-to-react-email-5.0.5.tgz\",\n      \"integrity\": \"sha512-OvAXqwq57uOk+WZqFFNCMZz8yDp8BD3WazW1wAKHUrPbbdr89K9DWS6JXY09vd9xNdPNeurI8DU/X4flcfaD8A==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"marked\": \"7.0.4\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^18.0 || ^19.0\"\n      }\n    },\n    \"node_modules/md-to-react-email/node_modules/marked\": {\n      \"version\": \"7.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/marked/-/marked-7.0.4.tgz\",\n      \"integrity\": \"sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==\",\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"marked\": \"bin/marked.js\"\n      },\n      \"engines\": {\n        \"node\": \">= 16\"\n      }\n    },\n    \"node_modules/md5\": {\n      \"version\": \"2.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/md5/-/md5-2.3.0.tgz\",\n      \"integrity\": \"sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==\",\n      \"license\": \"BSD-3-Clause\",\n      \"dependencies\": {\n        \"charenc\": \"0.0.2\",\n        \"crypt\": \"0.0.2\",\n        \"is-buffer\": \"~1.1.6\"\n      }\n    },\n    \"node_modules/mdast-util-find-and-replace\": {\n      \"version\": \"3.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz\",\n      \"integrity\": \"sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/mdast\": \"^4.0.0\",\n        \"escape-string-regexp\": \"^5.0.0\",\n        \"unist-util-is\": \"^6.0.0\",\n        \"unist-util-visit-parents\": \"^6.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz\",\n      \"integrity\": \"sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/mdast-util-from-markdown\": {\n      \"version\": \"2.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz\",\n      \"integrity\": \"sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/mdast\": \"^4.0.0\",\n        \"@types/unist\": \"^3.0.0\",\n        \"decode-named-character-reference\": \"^1.0.0\",\n        \"devlop\": \"^1.0.0\",\n        \"mdast-util-to-string\": \"^4.0.0\",\n        \"micromark\": \"^4.0.0\",\n        \"micromark-util-decode-numeric-character-reference\": \"^2.0.0\",\n        \"micromark-util-decode-string\": \"^2.0.0\",\n        \"micromark-util-normalize-identifier\": \"^2.0.0\",\n        \"micromark-util-symbol\": \"^2.0.0\",\n        \"micromark-util-types\": \"^2.0.0\",\n        \"unist-util-stringify-position\": \"^4.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/mdast-util-gfm\": {\n      \"version\": \"3.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz\",\n      \"integrity\": \"sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"mdast-util-from-markdown\": \"^2.0.0\",\n        \"mdast-util-gfm-autolink-literal\": \"^2.0.0\",\n        \"mdast-util-gfm-footnote\": \"^2.0.0\",\n        \"mdast-util-gfm-strikethrough\": \"^2.0.0\",\n        \"mdast-util-gfm-table\": \"^2.0.0\",\n        \"mdast-util-gfm-task-list-item\": \"^2.0.0\",\n        \"mdast-util-to-markdown\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/mdast-util-gfm-autolink-literal\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz\",\n      \"integrity\": \"sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/mdast\": \"^4.0.0\",\n        \"ccount\": \"^2.0.0\",\n        \"devlop\": \"^1.0.0\",\n        \"mdast-util-find-and-replace\": \"^3.0.0\",\n        \"micromark-util-character\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/mdast-util-gfm-footnote\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz\",\n      \"integrity\": \"sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/mdast\": \"^4.0.0\",\n        \"devlop\": \"^1.1.0\",\n        \"mdast-util-from-markdown\": \"^2.0.0\",\n        \"mdast-util-to-markdown\": \"^2.0.0\",\n        \"micromark-util-normalize-identifier\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/mdast-util-gfm-strikethrough\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz\",\n      \"integrity\": \"sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/mdast\": \"^4.0.0\",\n        \"mdast-util-from-markdown\": \"^2.0.0\",\n        \"mdast-util-to-markdown\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/mdast-util-gfm-table\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz\",\n      \"integrity\": \"sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/mdast\": \"^4.0.0\",\n        \"devlop\": \"^1.0.0\",\n        \"markdown-table\": \"^3.0.0\",\n        \"mdast-util-from-markdown\": \"^2.0.0\",\n        \"mdast-util-to-markdown\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/mdast-util-gfm-task-list-item\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz\",\n      \"integrity\": \"sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/mdast\": \"^4.0.0\",\n        \"devlop\": \"^1.0.0\",\n        \"mdast-util-from-markdown\": \"^2.0.0\",\n        \"mdast-util-to-markdown\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/mdast-util-math\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz\",\n      \"integrity\": \"sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"@types/mdast\": \"^4.0.0\",\n        \"devlop\": \"^1.0.0\",\n        \"longest-streak\": \"^3.0.0\",\n        \"mdast-util-from-markdown\": \"^2.0.0\",\n        \"mdast-util-to-markdown\": \"^2.1.0\",\n        \"unist-util-remove-position\": \"^5.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/mdast-util-mdx-expression\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz\",\n      \"integrity\": \"sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/estree-jsx\": \"^1.0.0\",\n        \"@types/hast\": \"^3.0.0\",\n        \"@types/mdast\": \"^4.0.0\",\n        \"devlop\": \"^1.0.0\",\n        \"mdast-util-from-markdown\": \"^2.0.0\",\n        \"mdast-util-to-markdown\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/mdast-util-mdx-jsx\": {\n      \"version\": \"3.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz\",\n      \"integrity\": \"sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/estree-jsx\": \"^1.0.0\",\n        \"@types/hast\": \"^3.0.0\",\n        \"@types/mdast\": \"^4.0.0\",\n        \"@types/unist\": \"^3.0.0\",\n        \"ccount\": \"^2.0.0\",\n        \"devlop\": \"^1.1.0\",\n        \"mdast-util-from-markdown\": \"^2.0.0\",\n        \"mdast-util-to-markdown\": \"^2.0.0\",\n        \"parse-entities\": \"^4.0.0\",\n        \"stringify-entities\": \"^4.0.0\",\n        \"unist-util-stringify-position\": \"^4.0.0\",\n        \"vfile-message\": \"^4.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/mdast-util-mdxjs-esm\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz\",\n      \"integrity\": \"sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/estree-jsx\": \"^1.0.0\",\n        \"@types/hast\": \"^3.0.0\",\n        \"@types/mdast\": \"^4.0.0\",\n        \"devlop\": \"^1.0.0\",\n        \"mdast-util-from-markdown\": \"^2.0.0\",\n        \"mdast-util-to-markdown\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/mdast-util-phrasing\": {\n      \"version\": \"4.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz\",\n      \"integrity\": \"sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/mdast\": \"^4.0.0\",\n        \"unist-util-is\": \"^6.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/mdast-util-to-hast\": {\n      \"version\": \"13.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz\",\n      \"integrity\": \"sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"@types/mdast\": \"^4.0.0\",\n        \"@ungap/structured-clone\": \"^1.0.0\",\n        \"devlop\": \"^1.0.0\",\n        \"micromark-util-sanitize-uri\": \"^2.0.0\",\n        \"trim-lines\": \"^3.0.0\",\n        \"unist-util-position\": \"^5.0.0\",\n        \"unist-util-visit\": \"^5.0.0\",\n        \"vfile\": \"^6.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/mdast-util-to-markdown\": {\n      \"version\": \"2.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz\",\n      \"integrity\": \"sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/mdast\": \"^4.0.0\",\n        \"@types/unist\": \"^3.0.0\",\n        \"longest-streak\": \"^3.0.0\",\n        \"mdast-util-phrasing\": \"^4.0.0\",\n        \"mdast-util-to-string\": \"^4.0.0\",\n        \"micromark-util-classify-character\": \"^2.0.0\",\n        \"micromark-util-decode-string\": \"^2.0.0\",\n        \"unist-util-visit\": \"^5.0.0\",\n        \"zwitch\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/mdast-util-to-string\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz\",\n      \"integrity\": \"sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/mdast\": \"^4.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/mdurl\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz\",\n      \"integrity\": \"sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/merge2\": {\n      \"version\": \"1.4.1\",\n      \"resolved\": \"https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz\",\n      \"integrity\": \"sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 8\"\n      }\n    },\n    \"node_modules/micromark\": {\n      \"version\": \"4.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz\",\n      \"integrity\": \"sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/debug\": \"^4.0.0\",\n        \"debug\": \"^4.0.0\",\n        \"decode-named-character-reference\": \"^1.0.0\",\n        \"devlop\": \"^1.0.0\",\n        \"micromark-core-commonmark\": \"^2.0.0\",\n        \"micromark-factory-space\": \"^2.0.0\",\n        \"micromark-util-character\": \"^2.0.0\",\n        \"micromark-util-chunked\": \"^2.0.0\",\n        \"micromark-util-combine-extensions\": \"^2.0.0\",\n        \"micromark-util-decode-numeric-character-reference\": \"^2.0.0\",\n        \"micromark-util-encode\": \"^2.0.0\",\n        \"micromark-util-normalize-identifier\": \"^2.0.0\",\n        \"micromark-util-resolve-all\": \"^2.0.0\",\n        \"micromark-util-sanitize-uri\": \"^2.0.0\",\n        \"micromark-util-subtokenize\": \"^2.0.0\",\n        \"micromark-util-symbol\": \"^2.0.0\",\n        \"micromark-util-types\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/micromark-core-commonmark\": {\n      \"version\": \"2.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz\",\n      \"integrity\": \"sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"decode-named-character-reference\": \"^1.0.0\",\n        \"devlop\": \"^1.0.0\",\n        \"micromark-factory-destination\": \"^2.0.0\",\n        \"micromark-factory-label\": \"^2.0.0\",\n        \"micromark-factory-space\": \"^2.0.0\",\n        \"micromark-factory-title\": \"^2.0.0\",\n        \"micromark-factory-whitespace\": \"^2.0.0\",\n        \"micromark-util-character\": \"^2.0.0\",\n        \"micromark-util-chunked\": \"^2.0.0\",\n        \"micromark-util-classify-character\": \"^2.0.0\",\n        \"micromark-util-html-tag-name\": \"^2.0.0\",\n        \"micromark-util-normalize-identifier\": \"^2.0.0\",\n        \"micromark-util-resolve-all\": \"^2.0.0\",\n        \"micromark-util-subtokenize\": \"^2.0.0\",\n        \"micromark-util-symbol\": \"^2.0.0\",\n        \"micromark-util-types\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/micromark-extension-gfm\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz\",\n      \"integrity\": \"sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"micromark-extension-gfm-autolink-literal\": \"^2.0.0\",\n        \"micromark-extension-gfm-footnote\": \"^2.0.0\",\n        \"micromark-extension-gfm-strikethrough\": \"^2.0.0\",\n        \"micromark-extension-gfm-table\": \"^2.0.0\",\n        \"micromark-extension-gfm-tagfilter\": \"^2.0.0\",\n        \"micromark-extension-gfm-task-list-item\": \"^2.0.0\",\n        \"micromark-util-combine-extensions\": \"^2.0.0\",\n        \"micromark-util-types\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/micromark-extension-gfm-autolink-literal\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz\",\n      \"integrity\": \"sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"micromark-util-character\": \"^2.0.0\",\n        \"micromark-util-sanitize-uri\": \"^2.0.0\",\n        \"micromark-util-symbol\": \"^2.0.0\",\n        \"micromark-util-types\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/micromark-extension-gfm-footnote\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz\",\n      \"integrity\": \"sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"devlop\": \"^1.0.0\",\n        \"micromark-core-commonmark\": \"^2.0.0\",\n        \"micromark-factory-space\": \"^2.0.0\",\n        \"micromark-util-character\": \"^2.0.0\",\n        \"micromark-util-normalize-identifier\": \"^2.0.0\",\n        \"micromark-util-sanitize-uri\": \"^2.0.0\",\n        \"micromark-util-symbol\": \"^2.0.0\",\n        \"micromark-util-types\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/micromark-extension-gfm-strikethrough\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz\",\n      \"integrity\": \"sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"devlop\": \"^1.0.0\",\n        \"micromark-util-chunked\": \"^2.0.0\",\n        \"micromark-util-classify-character\": \"^2.0.0\",\n        \"micromark-util-resolve-all\": \"^2.0.0\",\n        \"micromark-util-symbol\": \"^2.0.0\",\n        \"micromark-util-types\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/micromark-extension-gfm-table\": {\n      \"version\": \"2.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz\",\n      \"integrity\": \"sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"devlop\": \"^1.0.0\",\n        \"micromark-factory-space\": \"^2.0.0\",\n        \"micromark-util-character\": \"^2.0.0\",\n        \"micromark-util-symbol\": \"^2.0.0\",\n        \"micromark-util-types\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/micromark-extension-gfm-tagfilter\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz\",\n      \"integrity\": \"sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"micromark-util-types\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/micromark-extension-gfm-task-list-item\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz\",\n      \"integrity\": \"sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"devlop\": \"^1.0.0\",\n        \"micromark-factory-space\": \"^2.0.0\",\n        \"micromark-util-character\": \"^2.0.0\",\n        \"micromark-util-symbol\": \"^2.0.0\",\n        \"micromark-util-types\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/micromark-extension-math\": {\n      \"version\": \"3.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz\",\n      \"integrity\": \"sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/katex\": \"^0.16.0\",\n        \"devlop\": \"^1.0.0\",\n        \"katex\": \"^0.16.0\",\n        \"micromark-factory-space\": \"^2.0.0\",\n        \"micromark-util-character\": \"^2.0.0\",\n        \"micromark-util-symbol\": \"^2.0.0\",\n        \"micromark-util-types\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/micromark-factory-destination\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz\",\n      \"integrity\": \"sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"micromark-util-character\": \"^2.0.0\",\n        \"micromark-util-symbol\": \"^2.0.0\",\n        \"micromark-util-types\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/micromark-factory-label\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz\",\n      \"integrity\": \"sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"devlop\": \"^1.0.0\",\n        \"micromark-util-character\": \"^2.0.0\",\n        \"micromark-util-symbol\": \"^2.0.0\",\n        \"micromark-util-types\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/micromark-factory-space\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz\",\n      \"integrity\": \"sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"micromark-util-character\": \"^2.0.0\",\n        \"micromark-util-types\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/micromark-factory-title\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz\",\n      \"integrity\": \"sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"micromark-factory-space\": \"^2.0.0\",\n        \"micromark-util-character\": \"^2.0.0\",\n        \"micromark-util-symbol\": \"^2.0.0\",\n        \"micromark-util-types\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/micromark-factory-whitespace\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz\",\n      \"integrity\": \"sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"micromark-factory-space\": \"^2.0.0\",\n        \"micromark-util-character\": \"^2.0.0\",\n        \"micromark-util-symbol\": \"^2.0.0\",\n        \"micromark-util-types\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/micromark-util-character\": {\n      \"version\": \"2.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz\",\n      \"integrity\": \"sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"micromark-util-symbol\": \"^2.0.0\",\n        \"micromark-util-types\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/micromark-util-chunked\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz\",\n      \"integrity\": \"sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"micromark-util-symbol\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/micromark-util-classify-character\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz\",\n      \"integrity\": \"sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"micromark-util-character\": \"^2.0.0\",\n        \"micromark-util-symbol\": \"^2.0.0\",\n        \"micromark-util-types\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/micromark-util-combine-extensions\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz\",\n      \"integrity\": \"sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"micromark-util-chunked\": \"^2.0.0\",\n        \"micromark-util-types\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/micromark-util-decode-numeric-character-reference\": {\n      \"version\": \"2.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz\",\n      \"integrity\": \"sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"micromark-util-symbol\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/micromark-util-decode-string\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz\",\n      \"integrity\": \"sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"decode-named-character-reference\": \"^1.0.0\",\n        \"micromark-util-character\": \"^2.0.0\",\n        \"micromark-util-decode-numeric-character-reference\": \"^2.0.0\",\n        \"micromark-util-symbol\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/micromark-util-encode\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz\",\n      \"integrity\": \"sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\"\n    },\n    \"node_modules/micromark-util-html-tag-name\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz\",\n      \"integrity\": \"sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\"\n    },\n    \"node_modules/micromark-util-normalize-identifier\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz\",\n      \"integrity\": \"sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"micromark-util-symbol\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/micromark-util-resolve-all\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz\",\n      \"integrity\": \"sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"micromark-util-types\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/micromark-util-sanitize-uri\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz\",\n      \"integrity\": \"sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"micromark-util-character\": \"^2.0.0\",\n        \"micromark-util-encode\": \"^2.0.0\",\n        \"micromark-util-symbol\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/micromark-util-subtokenize\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz\",\n      \"integrity\": \"sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"devlop\": \"^1.0.0\",\n        \"micromark-util-chunked\": \"^2.0.0\",\n        \"micromark-util-symbol\": \"^2.0.0\",\n        \"micromark-util-types\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/micromark-util-symbol\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz\",\n      \"integrity\": \"sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\"\n    },\n    \"node_modules/micromark-util-types\": {\n      \"version\": \"2.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz\",\n      \"integrity\": \"sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==\",\n      \"funding\": [\n        {\n          \"type\": \"GitHub Sponsors\",\n          \"url\": \"https://github.com/sponsors/unifiedjs\"\n        },\n        {\n          \"type\": \"OpenCollective\",\n          \"url\": \"https://opencollective.com/unified\"\n        }\n      ],\n      \"license\": \"MIT\"\n    },\n    \"node_modules/micromatch\": {\n      \"version\": \"4.0.8\",\n      \"resolved\": \"https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz\",\n      \"integrity\": \"sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"braces\": \"^3.0.3\",\n        \"picomatch\": \"^2.3.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8.6\"\n      }\n    },\n    \"node_modules/mimic-function\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz\",\n      \"integrity\": \"sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/mini-svg-data-uri\": {\n      \"version\": \"1.4.4\",\n      \"resolved\": \"https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz\",\n      \"integrity\": \"sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"mini-svg-data-uri\": \"cli.js\"\n      }\n    },\n    \"node_modules/minimatch\": {\n      \"version\": \"10.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz\",\n      \"integrity\": \"sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==\",\n      \"license\": \"BlueOak-1.0.0\",\n      \"dependencies\": {\n        \"brace-expansion\": \"^5.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/minimist\": {\n      \"version\": \"1.2.8\",\n      \"resolved\": \"https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz\",\n      \"integrity\": \"sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/minipass\": {\n      \"version\": \"7.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz\",\n      \"integrity\": \"sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==\",\n      \"license\": \"ISC\",\n      \"engines\": {\n        \"node\": \">=16 || 14 >=14.17\"\n      }\n    },\n    \"node_modules/mkdirp\": {\n      \"version\": \"1.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz\",\n      \"integrity\": \"sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==\",\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"mkdirp\": \"bin/cmd.js\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      }\n    },\n    \"node_modules/mobx\": {\n      \"version\": \"6.13.7\",\n      \"resolved\": \"https://registry.npmjs.org/mobx/-/mobx-6.13.7.tgz\",\n      \"integrity\": \"sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/mobx\"\n      }\n    },\n    \"node_modules/motion-dom\": {\n      \"version\": \"12.19.0\",\n      \"resolved\": \"https://registry.npmjs.org/motion-dom/-/motion-dom-12.19.0.tgz\",\n      \"integrity\": \"sha512-m96uqq8VbwxFLU0mtmlsIVe8NGGSdpBvBSHbnnOJQxniPaabvVdGgxSamhuDwBsRhwX7xPxdICgVJlOpzn/5bw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"motion-utils\": \"^12.19.0\"\n      }\n    },\n    \"node_modules/motion-utils\": {\n      \"version\": \"12.19.0\",\n      \"resolved\": \"https://registry.npmjs.org/motion-utils/-/motion-utils-12.19.0.tgz\",\n      \"integrity\": \"sha512-BuFTHINYmV07pdWs6lj6aI63vr2N4dg0vR+td0rtrdpWOhBzIkEklZyLcvKBoEtwSqx8Jg06vUB5RS0xDiUybw==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/ms\": {\n      \"version\": \"2.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/ms/-/ms-2.1.3.tgz\",\n      \"integrity\": \"sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/mz\": {\n      \"version\": \"2.7.0\",\n      \"resolved\": \"https://registry.npmjs.org/mz/-/mz-2.7.0.tgz\",\n      \"integrity\": \"sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"any-promise\": \"^1.0.0\",\n        \"object-assign\": \"^4.0.1\",\n        \"thenify-all\": \"^1.0.0\"\n      }\n    },\n    \"node_modules/nanoid\": {\n      \"version\": \"5.1.5\",\n      \"resolved\": \"https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz\",\n      \"integrity\": \"sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/ai\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"nanoid\": \"bin/nanoid.js\"\n      },\n      \"engines\": {\n        \"node\": \"^18 || >=20\"\n      }\n    },\n    \"node_modules/napi-postinstall\": {\n      \"version\": \"0.2.5\",\n      \"resolved\": \"https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.2.5.tgz\",\n      \"integrity\": \"sha512-kmsgUvCRIJohHjbZ3V8avP0I1Pekw329MVAMDzVxsrkjgdnqiwvMX5XwR+hWV66vsAtZ+iM+fVnq8RTQawUmCQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"napi-postinstall\": \"lib/cli.js\"\n      },\n      \"engines\": {\n        \"node\": \"^12.20.0 || ^14.18.0 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/napi-postinstall\"\n      }\n    },\n    \"node_modules/natural-compare\": {\n      \"version\": \"1.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz\",\n      \"integrity\": \"sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/net\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/net/-/net-1.0.2.tgz\",\n      \"integrity\": \"sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/next\": {\n      \"version\": \"16.1.6\",\n      \"resolved\": \"https://registry.npmjs.org/next/-/next-16.1.6.tgz\",\n      \"integrity\": \"sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@next/env\": \"16.1.6\",\n        \"@swc/helpers\": \"0.5.15\",\n        \"baseline-browser-mapping\": \"^2.8.3\",\n        \"caniuse-lite\": \"^1.0.30001579\",\n        \"postcss\": \"8.4.31\",\n        \"styled-jsx\": \"5.1.6\"\n      },\n      \"bin\": {\n        \"next\": \"dist/bin/next\"\n      },\n      \"engines\": {\n        \"node\": \">=20.9.0\"\n      },\n      \"optionalDependencies\": {\n        \"@next/swc-darwin-arm64\": \"16.1.6\",\n        \"@next/swc-darwin-x64\": \"16.1.6\",\n        \"@next/swc-linux-arm64-gnu\": \"16.1.6\",\n        \"@next/swc-linux-arm64-musl\": \"16.1.6\",\n        \"@next/swc-linux-x64-gnu\": \"16.1.6\",\n        \"@next/swc-linux-x64-musl\": \"16.1.6\",\n        \"@next/swc-win32-arm64-msvc\": \"16.1.6\",\n        \"@next/swc-win32-x64-msvc\": \"16.1.6\",\n        \"sharp\": \"^0.34.4\"\n      },\n      \"peerDependencies\": {\n        \"@opentelemetry/api\": \"^1.1.0\",\n        \"@playwright/test\": \"^1.51.1\",\n        \"babel-plugin-react-compiler\": \"*\",\n        \"react\": \"^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0\",\n        \"react-dom\": \"^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0\",\n        \"sass\": \"^1.3.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@opentelemetry/api\": {\n          \"optional\": true\n        },\n        \"@playwright/test\": {\n          \"optional\": true\n        },\n        \"babel-plugin-react-compiler\": {\n          \"optional\": true\n        },\n        \"sass\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/next-themes\": {\n      \"version\": \"0.4.6\",\n      \"resolved\": \"https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz\",\n      \"integrity\": \"sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==\",\n      \"license\": \"MIT\",\n      \"peerDependencies\": {\n        \"react\": \"^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc\",\n        \"react-dom\": \"^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc\"\n      }\n    },\n    \"node_modules/next/node_modules/@swc/helpers\": {\n      \"version\": \"0.5.15\",\n      \"resolved\": \"https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz\",\n      \"integrity\": \"sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==\",\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"tslib\": \"^2.8.0\"\n      }\n    },\n    \"node_modules/next/node_modules/nanoid\": {\n      \"version\": \"3.3.11\",\n      \"resolved\": \"https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz\",\n      \"integrity\": \"sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/ai\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"nanoid\": \"bin/nanoid.cjs\"\n      },\n      \"engines\": {\n        \"node\": \"^10 || ^12 || ^13.7 || ^14 || >=15.0.1\"\n      }\n    },\n    \"node_modules/next/node_modules/postcss\": {\n      \"version\": \"8.4.31\",\n      \"resolved\": \"https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz\",\n      \"integrity\": \"sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==\",\n      \"funding\": [\n        {\n          \"type\": \"opencollective\",\n          \"url\": \"https://opencollective.com/postcss/\"\n        },\n        {\n          \"type\": \"tidelift\",\n          \"url\": \"https://tidelift.com/funding/github/npm/postcss\"\n        },\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/ai\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"nanoid\": \"^3.3.6\",\n        \"picocolors\": \"^1.0.0\",\n        \"source-map-js\": \"^1.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"^10 || ^12 || >=14\"\n      }\n    },\n    \"node_modules/nock\": {\n      \"version\": \"14.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/nock/-/nock-14.0.5.tgz\",\n      \"integrity\": \"sha512-R49fALR9caB6vxuSWUIaK2eBYeTloZQUFBZ4rHO+TbhMGQHtwnhdqKLYki+o+8qMgLvoBYWrp/2KzGPhxL4S6w==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@mswjs/interceptors\": \"^0.38.7\",\n        \"json-stringify-safe\": \"^5.0.1\",\n        \"propagate\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18.20.0 <20 || >=20.12.1\"\n      }\n    },\n    \"node_modules/node-emoji\": {\n      \"version\": \"2.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz\",\n      \"integrity\": \"sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@sindresorhus/is\": \"^4.6.0\",\n        \"char-regex\": \"^1.0.2\",\n        \"emojilib\": \"^2.4.0\",\n        \"skin-tone\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/node-fetch\": {\n      \"version\": \"2.7.0\",\n      \"resolved\": \"https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz\",\n      \"integrity\": \"sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"whatwg-url\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \"4.x || >=6.0.0\"\n      },\n      \"peerDependencies\": {\n        \"encoding\": \"^0.1.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"encoding\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/node-releases\": {\n      \"version\": \"2.0.27\",\n      \"resolved\": \"https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz\",\n      \"integrity\": \"sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/nodemailer\": {\n      \"version\": \"7.0.11\",\n      \"resolved\": \"https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.11.tgz\",\n      \"integrity\": \"sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==\",\n      \"license\": \"MIT-0\",\n      \"engines\": {\n        \"node\": \">=6.0.0\"\n      }\n    },\n    \"node_modules/normalize-path\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz\",\n      \"integrity\": \"sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/object-assign\": {\n      \"version\": \"4.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz\",\n      \"integrity\": \"sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/object-hash\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz\",\n      \"integrity\": \"sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 6\"\n      }\n    },\n    \"node_modules/object-inspect\": {\n      \"version\": \"1.13.4\",\n      \"resolved\": \"https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz\",\n      \"integrity\": \"sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/object-keys\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz\",\n      \"integrity\": \"sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/object-to-spawn-args\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/object-to-spawn-args/-/object-to-spawn-args-2.0.1.tgz\",\n      \"integrity\": \"sha512-6FuKFQ39cOID+BMZ3QaphcC8Y4cw6LXBLyIgPU+OhIYwviJamPAn+4mITapnSBQrejB+NNp+FMskhD8Cq+Ys3w==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8.0.0\"\n      }\n    },\n    \"node_modules/object.assign\": {\n      \"version\": \"4.1.7\",\n      \"resolved\": \"https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz\",\n      \"integrity\": \"sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.8\",\n        \"call-bound\": \"^1.0.3\",\n        \"define-properties\": \"^1.2.1\",\n        \"es-object-atoms\": \"^1.0.0\",\n        \"has-symbols\": \"^1.1.0\",\n        \"object-keys\": \"^1.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/object.entries\": {\n      \"version\": \"1.1.9\",\n      \"resolved\": \"https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz\",\n      \"integrity\": \"sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.8\",\n        \"call-bound\": \"^1.0.4\",\n        \"define-properties\": \"^1.2.1\",\n        \"es-object-atoms\": \"^1.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/object.fromentries\": {\n      \"version\": \"2.0.8\",\n      \"resolved\": \"https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz\",\n      \"integrity\": \"sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.7\",\n        \"define-properties\": \"^1.2.1\",\n        \"es-abstract\": \"^1.23.2\",\n        \"es-object-atoms\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/object.groupby\": {\n      \"version\": \"1.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz\",\n      \"integrity\": \"sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.7\",\n        \"define-properties\": \"^1.2.1\",\n        \"es-abstract\": \"^1.23.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/object.values\": {\n      \"version\": \"1.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz\",\n      \"integrity\": \"sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.8\",\n        \"call-bound\": \"^1.0.3\",\n        \"define-properties\": \"^1.2.1\",\n        \"es-object-atoms\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/onetime\": {\n      \"version\": \"7.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz\",\n      \"integrity\": \"sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"mimic-function\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/openapi-types\": {\n      \"version\": \"12.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz\",\n      \"integrity\": \"sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/optionator\": {\n      \"version\": \"0.9.4\",\n      \"resolved\": \"https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz\",\n      \"integrity\": \"sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"deep-is\": \"^0.1.3\",\n        \"fast-levenshtein\": \"^2.0.6\",\n        \"levn\": \"^0.4.1\",\n        \"prelude-ls\": \"^1.2.1\",\n        \"type-check\": \"^0.4.0\",\n        \"word-wrap\": \"^1.2.5\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8.0\"\n      }\n    },\n    \"node_modules/ora\": {\n      \"version\": \"8.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/ora/-/ora-8.2.0.tgz\",\n      \"integrity\": \"sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"chalk\": \"^5.3.0\",\n        \"cli-cursor\": \"^5.0.0\",\n        \"cli-spinners\": \"^2.9.2\",\n        \"is-interactive\": \"^2.0.0\",\n        \"is-unicode-supported\": \"^2.0.0\",\n        \"log-symbols\": \"^6.0.0\",\n        \"stdin-discarder\": \"^0.2.2\",\n        \"string-width\": \"^7.2.0\",\n        \"strip-ansi\": \"^7.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/ora/node_modules/emoji-regex\": {\n      \"version\": \"10.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz\",\n      \"integrity\": \"sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/ora/node_modules/string-width\": {\n      \"version\": \"7.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz\",\n      \"integrity\": \"sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"emoji-regex\": \"^10.3.0\",\n        \"get-east-asian-width\": \"^1.0.0\",\n        \"strip-ansi\": \"^7.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/orderedmap\": {\n      \"version\": \"2.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz\",\n      \"integrity\": \"sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/outvariant\": {\n      \"version\": \"1.4.3\",\n      \"resolved\": \"https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz\",\n      \"integrity\": \"sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/own-keys\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz\",\n      \"integrity\": \"sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"get-intrinsic\": \"^1.2.6\",\n        \"object-keys\": \"^1.1.1\",\n        \"safe-push-apply\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/p-limit\": {\n      \"version\": \"3.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz\",\n      \"integrity\": \"sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"yocto-queue\": \"^0.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/p-locate\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz\",\n      \"integrity\": \"sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"p-limit\": \"^3.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/package-json-from-dist\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz\",\n      \"integrity\": \"sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==\",\n      \"license\": \"BlueOak-1.0.0\"\n    },\n    \"node_modules/parent-module\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz\",\n      \"integrity\": \"sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"callsites\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/parse-entities\": {\n      \"version\": \"4.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz\",\n      \"integrity\": \"sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/unist\": \"^2.0.0\",\n        \"character-entities-legacy\": \"^3.0.0\",\n        \"character-reference-invalid\": \"^2.0.0\",\n        \"decode-named-character-reference\": \"^1.0.0\",\n        \"is-alphanumerical\": \"^2.0.0\",\n        \"is-decimal\": \"^2.0.0\",\n        \"is-hexadecimal\": \"^2.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/parse-entities/node_modules/@types/unist\": {\n      \"version\": \"2.0.11\",\n      \"resolved\": \"https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz\",\n      \"integrity\": \"sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/parse5\": {\n      \"version\": \"7.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz\",\n      \"integrity\": \"sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"entities\": \"^6.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/inikulin/parse5?sponsor=1\"\n      }\n    },\n    \"node_modules/parse5/node_modules/entities\": {\n      \"version\": \"6.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/entities/-/entities-6.0.1.tgz\",\n      \"integrity\": \"sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==\",\n      \"license\": \"BSD-2-Clause\",\n      \"engines\": {\n        \"node\": \">=0.12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/fb55/entities?sponsor=1\"\n      }\n    },\n    \"node_modules/parseley\": {\n      \"version\": \"0.12.1\",\n      \"resolved\": \"https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz\",\n      \"integrity\": \"sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"leac\": \"^0.6.0\",\n        \"peberminta\": \"^0.9.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://ko-fi.com/killymxi\"\n      }\n    },\n    \"node_modules/path\": {\n      \"version\": \"0.12.7\",\n      \"resolved\": \"https://registry.npmjs.org/path/-/path-0.12.7.tgz\",\n      \"integrity\": \"sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"process\": \"^0.11.1\",\n        \"util\": \"^0.10.3\"\n      }\n    },\n    \"node_modules/path-exists\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz\",\n      \"integrity\": \"sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/path-key\": {\n      \"version\": \"3.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz\",\n      \"integrity\": \"sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/path-parse\": {\n      \"version\": \"1.0.7\",\n      \"resolved\": \"https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz\",\n      \"integrity\": \"sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/path-scurry\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz\",\n      \"integrity\": \"sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==\",\n      \"license\": \"BlueOak-1.0.0\",\n      \"dependencies\": {\n        \"lru-cache\": \"^11.0.0\",\n        \"minipass\": \"^7.1.2\"\n      },\n      \"engines\": {\n        \"node\": \"20 || >=22\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/peberminta\": {\n      \"version\": \"0.9.0\",\n      \"resolved\": \"https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz\",\n      \"integrity\": \"sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"url\": \"https://ko-fi.com/killymxi\"\n      }\n    },\n    \"node_modules/perfect-freehand\": {\n      \"version\": \"1.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.2.2.tgz\",\n      \"integrity\": \"sha512-eh31l019WICQ03pkF3FSzHxB8n07ItqIQ++G5UV8JX0zVOXzgTGCqnRR0jJ2h9U8/2uW4W4mtGJELt9kEV0CFQ==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/picocolors\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz\",\n      \"integrity\": \"sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==\",\n      \"license\": \"ISC\"\n    },\n    \"node_modules/picomatch\": {\n      \"version\": \"2.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz\",\n      \"integrity\": \"sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8.6\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/jonschlinkert\"\n      }\n    },\n    \"node_modules/pify\": {\n      \"version\": \"2.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/pify/-/pify-2.3.0.tgz\",\n      \"integrity\": \"sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/pirates\": {\n      \"version\": \"4.0.7\",\n      \"resolved\": \"https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz\",\n      \"integrity\": \"sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 6\"\n      }\n    },\n    \"node_modules/playwright\": {\n      \"version\": \"1.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz\",\n      \"integrity\": \"sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==\",\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"playwright-core\": \"1.57.0\"\n      },\n      \"bin\": {\n        \"playwright\": \"cli.js\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"optionalDependencies\": {\n        \"fsevents\": \"2.3.2\"\n      }\n    },\n    \"node_modules/playwright-core\": {\n      \"version\": \"1.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz\",\n      \"integrity\": \"sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==\",\n      \"license\": \"Apache-2.0\",\n      \"bin\": {\n        \"playwright-core\": \"cli.js\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/possible-typed-array-names\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz\",\n      \"integrity\": \"sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/postcss\": {\n      \"version\": \"8.5.6\",\n      \"resolved\": \"https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz\",\n      \"integrity\": \"sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==\",\n      \"funding\": [\n        {\n          \"type\": \"opencollective\",\n          \"url\": \"https://opencollective.com/postcss/\"\n        },\n        {\n          \"type\": \"tidelift\",\n          \"url\": \"https://tidelift.com/funding/github/npm/postcss\"\n        },\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/ai\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"nanoid\": \"^3.3.11\",\n        \"picocolors\": \"^1.1.1\",\n        \"source-map-js\": \"^1.2.1\"\n      },\n      \"engines\": {\n        \"node\": \"^10 || ^12 || >=14\"\n      }\n    },\n    \"node_modules/postcss-import\": {\n      \"version\": \"15.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz\",\n      \"integrity\": \"sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"postcss-value-parser\": \"^4.0.0\",\n        \"read-cache\": \"^1.0.0\",\n        \"resolve\": \"^1.1.7\"\n      },\n      \"engines\": {\n        \"node\": \">=14.0.0\"\n      },\n      \"peerDependencies\": {\n        \"postcss\": \"^8.0.0\"\n      }\n    },\n    \"node_modules/postcss-js\": {\n      \"version\": \"4.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz\",\n      \"integrity\": \"sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"camelcase-css\": \"^2.0.1\"\n      },\n      \"engines\": {\n        \"node\": \"^12 || ^14 || >= 16\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/postcss/\"\n      },\n      \"peerDependencies\": {\n        \"postcss\": \"^8.4.21\"\n      }\n    },\n    \"node_modules/postcss-load-config\": {\n      \"version\": \"4.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz\",\n      \"integrity\": \"sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==\",\n      \"funding\": [\n        {\n          \"type\": \"opencollective\",\n          \"url\": \"https://opencollective.com/postcss/\"\n        },\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/ai\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"lilconfig\": \"^3.0.0\",\n        \"yaml\": \"^2.3.4\"\n      },\n      \"engines\": {\n        \"node\": \">= 14\"\n      },\n      \"peerDependencies\": {\n        \"postcss\": \">=8.0.9\",\n        \"ts-node\": \">=9.0.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"postcss\": {\n          \"optional\": true\n        },\n        \"ts-node\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/postcss-nested\": {\n      \"version\": \"6.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz\",\n      \"integrity\": \"sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==\",\n      \"funding\": [\n        {\n          \"type\": \"opencollective\",\n          \"url\": \"https://opencollective.com/postcss/\"\n        },\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/ai\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"postcss-selector-parser\": \"^6.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">=12.0\"\n      },\n      \"peerDependencies\": {\n        \"postcss\": \"^8.2.14\"\n      }\n    },\n    \"node_modules/postcss-selector-parser\": {\n      \"version\": \"6.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz\",\n      \"integrity\": \"sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"cssesc\": \"^3.0.0\",\n        \"util-deprecate\": \"^1.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/postcss-value-parser\": {\n      \"version\": \"4.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz\",\n      \"integrity\": \"sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/postcss/node_modules/nanoid\": {\n      \"version\": \"3.3.11\",\n      \"resolved\": \"https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz\",\n      \"integrity\": \"sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/ai\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"nanoid\": \"bin/nanoid.cjs\"\n      },\n      \"engines\": {\n        \"node\": \"^10 || ^12 || ^13.7 || ^14 || >=15.0.1\"\n      }\n    },\n    \"node_modules/prelude-ls\": {\n      \"version\": \"1.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz\",\n      \"integrity\": \"sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.8.0\"\n      }\n    },\n    \"node_modules/prettier\": {\n      \"version\": \"3.6.2\",\n      \"resolved\": \"https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz\",\n      \"integrity\": \"sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==\",\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"prettier\": \"bin/prettier.cjs\"\n      },\n      \"engines\": {\n        \"node\": \">=14\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/prettier/prettier?sponsor=1\"\n      }\n    },\n    \"node_modules/pretty-format\": {\n      \"version\": \"27.5.1\",\n      \"resolved\": \"https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz\",\n      \"integrity\": \"sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ansi-regex\": \"^5.0.1\",\n        \"ansi-styles\": \"^5.0.0\",\n        \"react-is\": \"^17.0.1\"\n      },\n      \"engines\": {\n        \"node\": \"^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0\"\n      }\n    },\n    \"node_modules/pretty-format/node_modules/ansi-regex\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz\",\n      \"integrity\": \"sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/pretty-format/node_modules/ansi-styles\": {\n      \"version\": \"5.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz\",\n      \"integrity\": \"sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/ansi-styles?sponsor=1\"\n      }\n    },\n    \"node_modules/pretty-format/node_modules/react-is\": {\n      \"version\": \"17.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz\",\n      \"integrity\": \"sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/prisma\": {\n      \"version\": \"6.10.1\",\n      \"resolved\": \"https://registry.npmjs.org/prisma/-/prisma-6.10.1.tgz\",\n      \"integrity\": \"sha512-khhlC/G49E4+uyA3T3H5PRBut486HD2bDqE2+rvkU0pwk9IAqGFacLFUyIx9Uw+W2eCtf6XGwsp+/strUwMNPw==\",\n      \"devOptional\": true,\n      \"hasInstallScript\": true,\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@prisma/config\": \"6.10.1\",\n        \"@prisma/engines\": \"6.10.1\"\n      },\n      \"bin\": {\n        \"prisma\": \"build/index.js\"\n      },\n      \"engines\": {\n        \"node\": \">=18.18\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=5.1.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"typescript\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/prismjs\": {\n      \"version\": \"1.30.0\",\n      \"resolved\": \"https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz\",\n      \"integrity\": \"sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/process\": {\n      \"version\": \"0.11.10\",\n      \"resolved\": \"https://registry.npmjs.org/process/-/process-0.11.10.tgz\",\n      \"integrity\": \"sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.6.0\"\n      }\n    },\n    \"node_modules/prop-types\": {\n      \"version\": \"15.8.1\",\n      \"resolved\": \"https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz\",\n      \"integrity\": \"sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"loose-envify\": \"^1.4.0\",\n        \"object-assign\": \"^4.1.1\",\n        \"react-is\": \"^16.13.1\"\n      }\n    },\n    \"node_modules/propagate\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz\",\n      \"integrity\": \"sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 8\"\n      }\n    },\n    \"node_modules/property-information\": {\n      \"version\": \"7.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz\",\n      \"integrity\": \"sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/prosemirror-changeset\": {\n      \"version\": \"2.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz\",\n      \"integrity\": \"sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"prosemirror-transform\": \"^1.0.0\"\n      }\n    },\n    \"node_modules/prosemirror-collab\": {\n      \"version\": \"1.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz\",\n      \"integrity\": \"sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"prosemirror-state\": \"^1.0.0\"\n      }\n    },\n    \"node_modules/prosemirror-commands\": {\n      \"version\": \"1.7.1\",\n      \"resolved\": \"https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz\",\n      \"integrity\": \"sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"prosemirror-model\": \"^1.0.0\",\n        \"prosemirror-state\": \"^1.0.0\",\n        \"prosemirror-transform\": \"^1.10.2\"\n      }\n    },\n    \"node_modules/prosemirror-dropcursor\": {\n      \"version\": \"1.8.2\",\n      \"resolved\": \"https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz\",\n      \"integrity\": \"sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"prosemirror-state\": \"^1.0.0\",\n        \"prosemirror-transform\": \"^1.1.0\",\n        \"prosemirror-view\": \"^1.1.0\"\n      }\n    },\n    \"node_modules/prosemirror-gapcursor\": {\n      \"version\": \"1.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz\",\n      \"integrity\": \"sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"prosemirror-keymap\": \"^1.0.0\",\n        \"prosemirror-model\": \"^1.0.0\",\n        \"prosemirror-state\": \"^1.0.0\",\n        \"prosemirror-view\": \"^1.0.0\"\n      }\n    },\n    \"node_modules/prosemirror-history\": {\n      \"version\": \"1.4.1\",\n      \"resolved\": \"https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.4.1.tgz\",\n      \"integrity\": \"sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"prosemirror-state\": \"^1.2.2\",\n        \"prosemirror-transform\": \"^1.0.0\",\n        \"prosemirror-view\": \"^1.31.0\",\n        \"rope-sequence\": \"^1.3.0\"\n      }\n    },\n    \"node_modules/prosemirror-inputrules\": {\n      \"version\": \"1.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.0.tgz\",\n      \"integrity\": \"sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"prosemirror-state\": \"^1.0.0\",\n        \"prosemirror-transform\": \"^1.0.0\"\n      }\n    },\n    \"node_modules/prosemirror-keymap\": {\n      \"version\": \"1.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz\",\n      \"integrity\": \"sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"prosemirror-state\": \"^1.0.0\",\n        \"w3c-keyname\": \"^2.2.0\"\n      }\n    },\n    \"node_modules/prosemirror-markdown\": {\n      \"version\": \"1.13.2\",\n      \"resolved\": \"https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz\",\n      \"integrity\": \"sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/markdown-it\": \"^14.0.0\",\n        \"markdown-it\": \"^14.0.0\",\n        \"prosemirror-model\": \"^1.25.0\"\n      }\n    },\n    \"node_modules/prosemirror-menu\": {\n      \"version\": \"1.2.5\",\n      \"resolved\": \"https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz\",\n      \"integrity\": \"sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"crelt\": \"^1.0.0\",\n        \"prosemirror-commands\": \"^1.0.0\",\n        \"prosemirror-history\": \"^1.0.0\",\n        \"prosemirror-state\": \"^1.0.0\"\n      }\n    },\n    \"node_modules/prosemirror-model\": {\n      \"version\": \"1.25.1\",\n      \"resolved\": \"https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.1.tgz\",\n      \"integrity\": \"sha512-AUvbm7qqmpZa5d9fPKMvH1Q5bqYQvAZWOGRvxsB6iFLyycvC9MwNemNVjHVrWgjaoxAfY8XVg7DbvQ/qxvI9Eg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"orderedmap\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/prosemirror-schema-basic\": {\n      \"version\": \"1.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz\",\n      \"integrity\": \"sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"prosemirror-model\": \"^1.25.0\"\n      }\n    },\n    \"node_modules/prosemirror-schema-list\": {\n      \"version\": \"1.5.1\",\n      \"resolved\": \"https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz\",\n      \"integrity\": \"sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"prosemirror-model\": \"^1.0.0\",\n        \"prosemirror-state\": \"^1.0.0\",\n        \"prosemirror-transform\": \"^1.7.3\"\n      }\n    },\n    \"node_modules/prosemirror-state\": {\n      \"version\": \"1.4.3\",\n      \"resolved\": \"https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz\",\n      \"integrity\": \"sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"prosemirror-model\": \"^1.0.0\",\n        \"prosemirror-transform\": \"^1.0.0\",\n        \"prosemirror-view\": \"^1.27.0\"\n      }\n    },\n    \"node_modules/prosemirror-tables\": {\n      \"version\": \"1.7.1\",\n      \"resolved\": \"https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.7.1.tgz\",\n      \"integrity\": \"sha512-eRQ97Bf+i9Eby99QbyAiyov43iOKgWa7QCGly+lrDt7efZ1v8NWolhXiB43hSDGIXT1UXgbs4KJN3a06FGpr1Q==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"prosemirror-keymap\": \"^1.2.2\",\n        \"prosemirror-model\": \"^1.25.0\",\n        \"prosemirror-state\": \"^1.4.3\",\n        \"prosemirror-transform\": \"^1.10.3\",\n        \"prosemirror-view\": \"^1.39.1\"\n      }\n    },\n    \"node_modules/prosemirror-trailing-node\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz\",\n      \"integrity\": \"sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@remirror/core-constants\": \"3.0.0\",\n        \"escape-string-regexp\": \"^4.0.0\"\n      },\n      \"peerDependencies\": {\n        \"prosemirror-model\": \"^1.22.1\",\n        \"prosemirror-state\": \"^1.4.2\",\n        \"prosemirror-view\": \"^1.33.8\"\n      }\n    },\n    \"node_modules/prosemirror-transform\": {\n      \"version\": \"1.10.4\",\n      \"resolved\": \"https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.4.tgz\",\n      \"integrity\": \"sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"prosemirror-model\": \"^1.21.0\"\n      }\n    },\n    \"node_modules/prosemirror-view\": {\n      \"version\": \"1.40.0\",\n      \"resolved\": \"https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.40.0.tgz\",\n      \"integrity\": \"sha512-2G3svX0Cr1sJjkD/DYWSe3cfV5VPVTBOxI9XQEGWJDFEpsZb/gh4MV29ctv+OJx2RFX4BLt09i+6zaGM/ldkCw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"prosemirror-model\": \"^1.20.0\",\n        \"prosemirror-state\": \"^1.0.0\",\n        \"prosemirror-transform\": \"^1.1.0\"\n      }\n    },\n    \"node_modules/punycode\": {\n      \"version\": \"2.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz\",\n      \"integrity\": \"sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/punycode.js\": {\n      \"version\": \"2.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz\",\n      \"integrity\": \"sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/pvtsutils\": {\n      \"version\": \"1.3.6\",\n      \"resolved\": \"https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz\",\n      \"integrity\": \"sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"tslib\": \"^2.8.1\"\n      }\n    },\n    \"node_modules/pvutils\": {\n      \"version\": \"1.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz\",\n      \"integrity\": \"sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6.0.0\"\n      }\n    },\n    \"node_modules/queue-microtask\": {\n      \"version\": \"1.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz\",\n      \"integrity\": \"sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/feross\"\n        },\n        {\n          \"type\": \"patreon\",\n          \"url\": \"https://www.patreon.com/feross\"\n        },\n        {\n          \"type\": \"consulting\",\n          \"url\": \"https://feross.org/support\"\n        }\n      ],\n      \"license\": \"MIT\"\n    },\n    \"node_modules/react\": {\n      \"version\": \"19.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/react/-/react-19.2.4.tgz\",\n      \"integrity\": \"sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/react-csv\": {\n      \"version\": \"2.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/react-csv/-/react-csv-2.2.2.tgz\",\n      \"integrity\": \"sha512-RG5hOcZKZFigIGE8LxIEV/OgS1vigFQT4EkaHeKgyuCbUAu9Nbd/1RYq++bJcJJ9VOqO/n9TZRADsXNDR4VEpw==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/react-day-picker\": {\n      \"version\": \"9.7.0\",\n      \"resolved\": \"https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.7.0.tgz\",\n      \"integrity\": \"sha512-urlK4C9XJZVpQ81tmVgd2O7lZ0VQldZeHzNejbwLWZSkzHH498KnArT0EHNfKBOWwKc935iMLGZdxXPRISzUxQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@date-fns/tz\": \"1.2.0\",\n        \"date-fns\": \"4.1.0\",\n        \"date-fns-jalali\": \"4.1.0-0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"type\": \"individual\",\n        \"url\": \"https://github.com/sponsors/gpbl\"\n      },\n      \"peerDependencies\": {\n        \"react\": \">=16.8.0\"\n      }\n    },\n    \"node_modules/react-dnd\": {\n      \"version\": \"16.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz\",\n      \"integrity\": \"sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@react-dnd/invariant\": \"^4.0.1\",\n        \"@react-dnd/shallowequal\": \"^4.0.1\",\n        \"dnd-core\": \"^16.0.1\",\n        \"fast-deep-equal\": \"^3.1.3\",\n        \"hoist-non-react-statics\": \"^3.3.2\"\n      },\n      \"peerDependencies\": {\n        \"@types/hoist-non-react-statics\": \">= 3.3.1\",\n        \"@types/node\": \">= 12\",\n        \"@types/react\": \">= 16\",\n        \"react\": \">= 16.14\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/hoist-non-react-statics\": {\n          \"optional\": true\n        },\n        \"@types/node\": {\n          \"optional\": true\n        },\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/react-dnd-html5-backend\": {\n      \"version\": \"16.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz\",\n      \"integrity\": \"sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"dnd-core\": \"^16.0.1\"\n      }\n    },\n    \"node_modules/react-dom\": {\n      \"version\": \"19.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz\",\n      \"integrity\": \"sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"scheduler\": \"^0.27.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^19.2.4\"\n      }\n    },\n    \"node_modules/react-hook-form\": {\n      \"version\": \"7.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.59.0.tgz\",\n      \"integrity\": \"sha512-kmkek2/8grqarTJExFNjy+RXDIP8yM+QTl3QL6m6Q8b2bih4ltmiXxH7T9n+yXNK477xPh5yZT/6vD8sYGzJTA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/react-hook-form\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^16.8.0 || ^17 || ^18 || ^19\"\n      }\n    },\n    \"node_modules/react-is\": {\n      \"version\": \"16.13.1\",\n      \"resolved\": \"https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz\",\n      \"integrity\": \"sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/react-markdown\": {\n      \"version\": \"9.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz\",\n      \"integrity\": \"sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"@types/mdast\": \"^4.0.0\",\n        \"devlop\": \"^1.0.0\",\n        \"hast-util-to-jsx-runtime\": \"^2.0.0\",\n        \"html-url-attributes\": \"^3.0.0\",\n        \"mdast-util-to-hast\": \"^13.0.0\",\n        \"remark-parse\": \"^11.0.0\",\n        \"remark-rehype\": \"^11.0.0\",\n        \"unified\": \"^11.0.0\",\n        \"unist-util-visit\": \"^5.0.0\",\n        \"vfile\": \"^6.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \">=18\",\n        \"react\": \">=18\"\n      }\n    },\n    \"node_modules/react-promise-suspense\": {\n      \"version\": \"0.3.4\",\n      \"resolved\": \"https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz\",\n      \"integrity\": \"sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"fast-deep-equal\": \"^2.0.1\"\n      }\n    },\n    \"node_modules/react-promise-suspense/node_modules/fast-deep-equal\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz\",\n      \"integrity\": \"sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/react-remove-scroll\": {\n      \"version\": \"2.7.1\",\n      \"resolved\": \"https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz\",\n      \"integrity\": \"sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"react-remove-scroll-bar\": \"^2.3.7\",\n        \"react-style-singleton\": \"^2.2.3\",\n        \"tslib\": \"^2.1.0\",\n        \"use-callback-ref\": \"^1.3.3\",\n        \"use-sidecar\": \"^1.1.3\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"react\": \"^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/react-remove-scroll-bar\": {\n      \"version\": \"2.3.8\",\n      \"resolved\": \"https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz\",\n      \"integrity\": \"sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"react-style-singleton\": \"^2.2.2\",\n        \"tslib\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"react\": \"^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/react-smooth\": {\n      \"version\": \"4.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz\",\n      \"integrity\": \"sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"fast-equals\": \"^5.0.1\",\n        \"prop-types\": \"^15.8.1\",\n        \"react-transition-group\": \"^4.4.5\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0\",\n        \"react-dom\": \"^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0\"\n      }\n    },\n    \"node_modules/react-style-singleton\": {\n      \"version\": \"2.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz\",\n      \"integrity\": \"sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"get-nonce\": \"^1.0.0\",\n        \"tslib\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"react\": \"^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/react-syntax-highlighter\": {\n      \"version\": \"16.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.0.tgz\",\n      \"integrity\": \"sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/runtime\": \"^7.28.4\",\n        \"highlight.js\": \"^10.4.1\",\n        \"highlightjs-vue\": \"^1.0.0\",\n        \"lowlight\": \"^1.17.0\",\n        \"prismjs\": \"^1.30.0\",\n        \"refractor\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 16.20.2\"\n      },\n      \"peerDependencies\": {\n        \"react\": \">= 0.14.0\"\n      }\n    },\n    \"node_modules/react-transition-group\": {\n      \"version\": \"4.4.5\",\n      \"resolved\": \"https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz\",\n      \"integrity\": \"sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==\",\n      \"license\": \"BSD-3-Clause\",\n      \"dependencies\": {\n        \"@babel/runtime\": \"^7.5.5\",\n        \"dom-helpers\": \"^5.0.1\",\n        \"loose-envify\": \"^1.4.0\",\n        \"prop-types\": \"^15.6.2\"\n      },\n      \"peerDependencies\": {\n        \"react\": \">=16.6.0\",\n        \"react-dom\": \">=16.6.0\"\n      }\n    },\n    \"node_modules/read-cache\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz\",\n      \"integrity\": \"sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"pify\": \"^2.3.0\"\n      }\n    },\n    \"node_modules/readdirp\": {\n      \"version\": \"3.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz\",\n      \"integrity\": \"sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"picomatch\": \"^2.2.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8.10.0\"\n      }\n    },\n    \"node_modules/recharts\": {\n      \"version\": \"2.15.4\",\n      \"resolved\": \"https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz\",\n      \"integrity\": \"sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"clsx\": \"^2.0.0\",\n        \"eventemitter3\": \"^4.0.1\",\n        \"lodash\": \"^4.17.21\",\n        \"react-is\": \"^18.3.1\",\n        \"react-smooth\": \"^4.0.4\",\n        \"recharts-scale\": \"^0.4.4\",\n        \"tiny-invariant\": \"^1.3.1\",\n        \"victory-vendor\": \"^36.6.8\"\n      },\n      \"engines\": {\n        \"node\": \">=14\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0\",\n        \"react-dom\": \"^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0\"\n      }\n    },\n    \"node_modules/recharts-scale\": {\n      \"version\": \"0.4.5\",\n      \"resolved\": \"https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz\",\n      \"integrity\": \"sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"decimal.js-light\": \"^2.4.1\"\n      }\n    },\n    \"node_modules/recharts/node_modules/react-is\": {\n      \"version\": \"18.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz\",\n      \"integrity\": \"sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/redux\": {\n      \"version\": \"4.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/redux/-/redux-4.2.1.tgz\",\n      \"integrity\": \"sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/runtime\": \"^7.9.2\"\n      }\n    },\n    \"node_modules/reflect.getprototypeof\": {\n      \"version\": \"1.0.10\",\n      \"resolved\": \"https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz\",\n      \"integrity\": \"sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.8\",\n        \"define-properties\": \"^1.2.1\",\n        \"es-abstract\": \"^1.23.9\",\n        \"es-errors\": \"^1.3.0\",\n        \"es-object-atoms\": \"^1.0.0\",\n        \"get-intrinsic\": \"^1.2.7\",\n        \"get-proto\": \"^1.0.1\",\n        \"which-builtin-type\": \"^1.2.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/refractor\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz\",\n      \"integrity\": \"sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"@types/prismjs\": \"^1.0.0\",\n        \"hastscript\": \"^9.0.0\",\n        \"parse-entities\": \"^4.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/regexp.prototype.flags\": {\n      \"version\": \"1.5.4\",\n      \"resolved\": \"https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz\",\n      \"integrity\": \"sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.8\",\n        \"define-properties\": \"^1.2.1\",\n        \"es-errors\": \"^1.3.0\",\n        \"get-proto\": \"^1.0.1\",\n        \"gopd\": \"^1.2.0\",\n        \"set-function-name\": \"^2.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/rehype-highlight\": {\n      \"version\": \"7.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz\",\n      \"integrity\": \"sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"hast-util-to-text\": \"^4.0.0\",\n        \"lowlight\": \"^3.0.0\",\n        \"unist-util-visit\": \"^5.0.0\",\n        \"vfile\": \"^6.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/rehype-highlight/node_modules/highlight.js\": {\n      \"version\": \"11.11.1\",\n      \"resolved\": \"https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz\",\n      \"integrity\": \"sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==\",\n      \"license\": \"BSD-3-Clause\",\n      \"engines\": {\n        \"node\": \">=12.0.0\"\n      }\n    },\n    \"node_modules/rehype-highlight/node_modules/lowlight\": {\n      \"version\": \"3.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz\",\n      \"integrity\": \"sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"devlop\": \"^1.0.0\",\n        \"highlight.js\": \"~11.11.0\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/rehype-katex\": {\n      \"version\": \"7.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz\",\n      \"integrity\": \"sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"@types/katex\": \"^0.16.0\",\n        \"hast-util-from-html-isomorphic\": \"^2.0.0\",\n        \"hast-util-to-text\": \"^4.0.0\",\n        \"katex\": \"^0.16.0\",\n        \"unist-util-visit-parents\": \"^6.0.0\",\n        \"vfile\": \"^6.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/rehype-raw\": {\n      \"version\": \"7.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz\",\n      \"integrity\": \"sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"hast-util-raw\": \"^9.0.0\",\n        \"vfile\": \"^6.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/rehype-sanitize\": {\n      \"version\": \"6.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz\",\n      \"integrity\": \"sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"hast-util-sanitize\": \"^5.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/rehype-stringify\": {\n      \"version\": \"10.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz\",\n      \"integrity\": \"sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"hast-util-to-html\": \"^9.0.0\",\n        \"unified\": \"^11.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/remark-emoji\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/remark-emoji/-/remark-emoji-5.0.1.tgz\",\n      \"integrity\": \"sha512-QCqTSvcZ65Ym+P+VyBKd4JfJfh7icMl7cIOGVmPMzWkDtdD8pQ0nQG7yxGolVIiMzSx90EZ7SwNiVpYpfTxn7w==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/mdast\": \"^4.0.4\",\n        \"emoticon\": \"^4.0.1\",\n        \"mdast-util-find-and-replace\": \"^3.0.1\",\n        \"node-emoji\": \"^2.1.3\",\n        \"unified\": \"^11.0.4\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/remark-gfm\": {\n      \"version\": \"4.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz\",\n      \"integrity\": \"sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/mdast\": \"^4.0.0\",\n        \"mdast-util-gfm\": \"^3.0.0\",\n        \"micromark-extension-gfm\": \"^3.0.0\",\n        \"remark-parse\": \"^11.0.0\",\n        \"remark-stringify\": \"^11.0.0\",\n        \"unified\": \"^11.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/remark-math\": {\n      \"version\": \"6.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz\",\n      \"integrity\": \"sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/mdast\": \"^4.0.0\",\n        \"mdast-util-math\": \"^3.0.0\",\n        \"micromark-extension-math\": \"^3.0.0\",\n        \"unified\": \"^11.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/remark-parse\": {\n      \"version\": \"11.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz\",\n      \"integrity\": \"sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/mdast\": \"^4.0.0\",\n        \"mdast-util-from-markdown\": \"^2.0.0\",\n        \"micromark-util-types\": \"^2.0.0\",\n        \"unified\": \"^11.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/remark-rehype\": {\n      \"version\": \"11.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz\",\n      \"integrity\": \"sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/hast\": \"^3.0.0\",\n        \"@types/mdast\": \"^4.0.0\",\n        \"mdast-util-to-hast\": \"^13.0.0\",\n        \"unified\": \"^11.0.0\",\n        \"vfile\": \"^6.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/remark-stringify\": {\n      \"version\": \"11.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz\",\n      \"integrity\": \"sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/mdast\": \"^4.0.0\",\n        \"mdast-util-to-markdown\": \"^2.0.0\",\n        \"unified\": \"^11.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/requizzle\": {\n      \"version\": \"0.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz\",\n      \"integrity\": \"sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"lodash\": \"^4.17.21\"\n      }\n    },\n    \"node_modules/resolve\": {\n      \"version\": \"1.22.10\",\n      \"resolved\": \"https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz\",\n      \"integrity\": \"sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"is-core-module\": \"^2.16.0\",\n        \"path-parse\": \"^1.0.7\",\n        \"supports-preserve-symlinks-flag\": \"^1.0.0\"\n      },\n      \"bin\": {\n        \"resolve\": \"bin/resolve\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/resolve-from\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz\",\n      \"integrity\": \"sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/resolve-pkg-maps\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz\",\n      \"integrity\": \"sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"url\": \"https://github.com/privatenumber/resolve-pkg-maps?sponsor=1\"\n      }\n    },\n    \"node_modules/restore-cursor\": {\n      \"version\": \"5.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz\",\n      \"integrity\": \"sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"onetime\": \"^7.0.0\",\n        \"signal-exit\": \"^4.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/reusify\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz\",\n      \"integrity\": \"sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"iojs\": \">=1.0.0\",\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/rope-sequence\": {\n      \"version\": \"1.3.4\",\n      \"resolved\": \"https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz\",\n      \"integrity\": \"sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/run-parallel\": {\n      \"version\": \"1.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz\",\n      \"integrity\": \"sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/feross\"\n        },\n        {\n          \"type\": \"patreon\",\n          \"url\": \"https://www.patreon.com/feross\"\n        },\n        {\n          \"type\": \"consulting\",\n          \"url\": \"https://feross.org/support\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"queue-microtask\": \"^1.2.2\"\n      }\n    },\n    \"node_modules/safe-array-concat\": {\n      \"version\": \"1.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz\",\n      \"integrity\": \"sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.8\",\n        \"call-bound\": \"^1.0.2\",\n        \"get-intrinsic\": \"^1.2.6\",\n        \"has-symbols\": \"^1.1.0\",\n        \"isarray\": \"^2.0.5\"\n      },\n      \"engines\": {\n        \"node\": \">=0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/safe-push-apply\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz\",\n      \"integrity\": \"sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"es-errors\": \"^1.3.0\",\n        \"isarray\": \"^2.0.5\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/safe-regex-test\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz\",\n      \"integrity\": \"sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.2\",\n        \"es-errors\": \"^1.3.0\",\n        \"is-regex\": \"^1.2.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/scheduler\": {\n      \"version\": \"0.27.0\",\n      \"resolved\": \"https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz\",\n      \"integrity\": \"sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/selderee\": {\n      \"version\": \"0.11.0\",\n      \"resolved\": \"https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz\",\n      \"integrity\": \"sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"parseley\": \"^0.12.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://ko-fi.com/killymxi\"\n      }\n    },\n    \"node_modules/semver\": {\n      \"version\": \"7.7.4\",\n      \"resolved\": \"https://registry.npmjs.org/semver/-/semver-7.7.4.tgz\",\n      \"integrity\": \"sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==\",\n      \"devOptional\": true,\n      \"license\": \"ISC\",\n      \"bin\": {\n        \"semver\": \"bin/semver.js\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      }\n    },\n    \"node_modules/set-function-length\": {\n      \"version\": \"1.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz\",\n      \"integrity\": \"sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"define-data-property\": \"^1.1.4\",\n        \"es-errors\": \"^1.3.0\",\n        \"function-bind\": \"^1.1.2\",\n        \"get-intrinsic\": \"^1.2.4\",\n        \"gopd\": \"^1.0.1\",\n        \"has-property-descriptors\": \"^1.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/set-function-name\": {\n      \"version\": \"2.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz\",\n      \"integrity\": \"sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"define-data-property\": \"^1.1.4\",\n        \"es-errors\": \"^1.3.0\",\n        \"functions-have-names\": \"^1.2.3\",\n        \"has-property-descriptors\": \"^1.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/set-proto\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz\",\n      \"integrity\": \"sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"dunder-proto\": \"^1.0.1\",\n        \"es-errors\": \"^1.3.0\",\n        \"es-object-atoms\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/shallowequal\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz\",\n      \"integrity\": \"sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/sharp\": {\n      \"version\": \"0.34.4\",\n      \"resolved\": \"https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz\",\n      \"integrity\": \"sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==\",\n      \"hasInstallScript\": true,\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"@img/colour\": \"^1.0.0\",\n        \"detect-libc\": \"^2.1.0\",\n        \"semver\": \"^7.7.2\"\n      },\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-darwin-arm64\": \"0.34.4\",\n        \"@img/sharp-darwin-x64\": \"0.34.4\",\n        \"@img/sharp-libvips-darwin-arm64\": \"1.2.3\",\n        \"@img/sharp-libvips-darwin-x64\": \"1.2.3\",\n        \"@img/sharp-libvips-linux-arm\": \"1.2.3\",\n        \"@img/sharp-libvips-linux-arm64\": \"1.2.3\",\n        \"@img/sharp-libvips-linux-ppc64\": \"1.2.3\",\n        \"@img/sharp-libvips-linux-s390x\": \"1.2.3\",\n        \"@img/sharp-libvips-linux-x64\": \"1.2.3\",\n        \"@img/sharp-libvips-linuxmusl-arm64\": \"1.2.3\",\n        \"@img/sharp-libvips-linuxmusl-x64\": \"1.2.3\",\n        \"@img/sharp-linux-arm\": \"0.34.4\",\n        \"@img/sharp-linux-arm64\": \"0.34.4\",\n        \"@img/sharp-linux-ppc64\": \"0.34.4\",\n        \"@img/sharp-linux-s390x\": \"0.34.4\",\n        \"@img/sharp-linux-x64\": \"0.34.4\",\n        \"@img/sharp-linuxmusl-arm64\": \"0.34.4\",\n        \"@img/sharp-linuxmusl-x64\": \"0.34.4\",\n        \"@img/sharp-wasm32\": \"0.34.4\",\n        \"@img/sharp-win32-arm64\": \"0.34.4\",\n        \"@img/sharp-win32-ia32\": \"0.34.4\",\n        \"@img/sharp-win32-x64\": \"0.34.4\"\n      }\n    },\n    \"node_modules/shebang-command\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz\",\n      \"integrity\": \"sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"shebang-regex\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/shebang-regex\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz\",\n      \"integrity\": \"sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/side-channel\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz\",\n      \"integrity\": \"sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"es-errors\": \"^1.3.0\",\n        \"object-inspect\": \"^1.13.3\",\n        \"side-channel-list\": \"^1.0.0\",\n        \"side-channel-map\": \"^1.0.1\",\n        \"side-channel-weakmap\": \"^1.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/side-channel-list\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz\",\n      \"integrity\": \"sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"es-errors\": \"^1.3.0\",\n        \"object-inspect\": \"^1.13.3\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/side-channel-map\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz\",\n      \"integrity\": \"sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.2\",\n        \"es-errors\": \"^1.3.0\",\n        \"get-intrinsic\": \"^1.2.5\",\n        \"object-inspect\": \"^1.13.3\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/side-channel-weakmap\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz\",\n      \"integrity\": \"sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.2\",\n        \"es-errors\": \"^1.3.0\",\n        \"get-intrinsic\": \"^1.2.5\",\n        \"object-inspect\": \"^1.13.3\",\n        \"side-channel-map\": \"^1.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/signal-exit\": {\n      \"version\": \"4.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz\",\n      \"integrity\": \"sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==\",\n      \"license\": \"ISC\",\n      \"engines\": {\n        \"node\": \">=14\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/skin-tone\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz\",\n      \"integrity\": \"sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"unicode-emoji-modifier-base\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/slackify-markdown\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/slackify-markdown/-/slackify-markdown-5.0.0.tgz\",\n      \"integrity\": \"sha512-jTWvwjOYGAqS0NNyhTm+9H8S0/tux2anPEwyeIBCayH14jGHIrN70yj4afUYP2zq54aUwEFkASdXytjO/5GA+A==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"mdast-util-to-markdown\": \"^2.1.2\",\n        \"remark-gfm\": \"^4.0.0\",\n        \"remark-parse\": \"^11.0.0\",\n        \"remark-stringify\": \"^11.0.0\",\n        \"unified\": \"^11.0.5\",\n        \"unist-util-remove\": \"^4.0.0\",\n        \"unist-util-visit\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=22\"\n      }\n    },\n    \"node_modules/source-map-js\": {\n      \"version\": \"1.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz\",\n      \"integrity\": \"sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==\",\n      \"license\": \"BSD-3-Clause\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/space-separated-tokens\": {\n      \"version\": \"2.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz\",\n      \"integrity\": \"sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/stable-hash\": {\n      \"version\": \"0.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz\",\n      \"integrity\": \"sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/stdin-discarder\": {\n      \"version\": \"0.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz\",\n      \"integrity\": \"sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/stop-iteration-iterator\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz\",\n      \"integrity\": \"sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"es-errors\": \"^1.3.0\",\n        \"internal-slot\": \"^1.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/strict-event-emitter\": {\n      \"version\": \"0.5.1\",\n      \"resolved\": \"https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz\",\n      \"integrity\": \"sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/string-width\": {\n      \"version\": \"5.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz\",\n      \"integrity\": \"sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"eastasianwidth\": \"^0.2.0\",\n        \"emoji-regex\": \"^9.2.2\",\n        \"strip-ansi\": \"^7.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/string-width-cjs\": {\n      \"name\": \"string-width\",\n      \"version\": \"4.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz\",\n      \"integrity\": \"sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"emoji-regex\": \"^8.0.0\",\n        \"is-fullwidth-code-point\": \"^3.0.0\",\n        \"strip-ansi\": \"^6.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/string-width-cjs/node_modules/ansi-regex\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz\",\n      \"integrity\": \"sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/string-width-cjs/node_modules/emoji-regex\": {\n      \"version\": \"8.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz\",\n      \"integrity\": \"sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/string-width-cjs/node_modules/strip-ansi\": {\n      \"version\": \"6.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz\",\n      \"integrity\": \"sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ansi-regex\": \"^5.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/string.prototype.includes\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz\",\n      \"integrity\": \"sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.7\",\n        \"define-properties\": \"^1.2.1\",\n        \"es-abstract\": \"^1.23.3\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/string.prototype.matchall\": {\n      \"version\": \"4.0.12\",\n      \"resolved\": \"https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz\",\n      \"integrity\": \"sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.8\",\n        \"call-bound\": \"^1.0.3\",\n        \"define-properties\": \"^1.2.1\",\n        \"es-abstract\": \"^1.23.6\",\n        \"es-errors\": \"^1.3.0\",\n        \"es-object-atoms\": \"^1.0.0\",\n        \"get-intrinsic\": \"^1.2.6\",\n        \"gopd\": \"^1.2.0\",\n        \"has-symbols\": \"^1.1.0\",\n        \"internal-slot\": \"^1.1.0\",\n        \"regexp.prototype.flags\": \"^1.5.3\",\n        \"set-function-name\": \"^2.0.2\",\n        \"side-channel\": \"^1.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/string.prototype.repeat\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz\",\n      \"integrity\": \"sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"define-properties\": \"^1.1.3\",\n        \"es-abstract\": \"^1.17.5\"\n      }\n    },\n    \"node_modules/string.prototype.trim\": {\n      \"version\": \"1.2.10\",\n      \"resolved\": \"https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz\",\n      \"integrity\": \"sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.8\",\n        \"call-bound\": \"^1.0.2\",\n        \"define-data-property\": \"^1.1.4\",\n        \"define-properties\": \"^1.2.1\",\n        \"es-abstract\": \"^1.23.5\",\n        \"es-object-atoms\": \"^1.0.0\",\n        \"has-property-descriptors\": \"^1.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/string.prototype.trimend\": {\n      \"version\": \"1.0.9\",\n      \"resolved\": \"https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz\",\n      \"integrity\": \"sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.8\",\n        \"call-bound\": \"^1.0.2\",\n        \"define-properties\": \"^1.2.1\",\n        \"es-object-atoms\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/string.prototype.trimstart\": {\n      \"version\": \"1.0.8\",\n      \"resolved\": \"https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz\",\n      \"integrity\": \"sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.7\",\n        \"define-properties\": \"^1.2.1\",\n        \"es-object-atoms\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/stringify-entities\": {\n      \"version\": \"4.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz\",\n      \"integrity\": \"sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"character-entities-html4\": \"^2.0.0\",\n        \"character-entities-legacy\": \"^3.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/strip-ansi\": {\n      \"version\": \"7.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz\",\n      \"integrity\": \"sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ansi-regex\": \"^6.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/strip-ansi?sponsor=1\"\n      }\n    },\n    \"node_modules/strip-ansi-cjs\": {\n      \"name\": \"strip-ansi\",\n      \"version\": \"6.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz\",\n      \"integrity\": \"sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ansi-regex\": \"^5.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/strip-ansi-cjs/node_modules/ansi-regex\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz\",\n      \"integrity\": \"sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/strip-bom\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz\",\n      \"integrity\": \"sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/strip-json-comments\": {\n      \"version\": \"3.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz\",\n      \"integrity\": \"sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/style-to-js\": {\n      \"version\": \"1.1.17\",\n      \"resolved\": \"https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz\",\n      \"integrity\": \"sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"style-to-object\": \"1.0.9\"\n      }\n    },\n    \"node_modules/style-to-object\": {\n      \"version\": \"1.0.9\",\n      \"resolved\": \"https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz\",\n      \"integrity\": \"sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"inline-style-parser\": \"0.2.4\"\n      }\n    },\n    \"node_modules/styled-components\": {\n      \"version\": \"6.1.19\",\n      \"resolved\": \"https://registry.npmjs.org/styled-components/-/styled-components-6.1.19.tgz\",\n      \"integrity\": \"sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@emotion/is-prop-valid\": \"1.2.2\",\n        \"@emotion/unitless\": \"0.8.1\",\n        \"@types/stylis\": \"4.2.5\",\n        \"css-to-react-native\": \"3.2.0\",\n        \"csstype\": \"3.1.3\",\n        \"postcss\": \"8.4.49\",\n        \"shallowequal\": \"1.1.0\",\n        \"stylis\": \"4.3.2\",\n        \"tslib\": \"2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 16\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/styled-components\"\n      },\n      \"peerDependencies\": {\n        \"react\": \">= 16.8.0\",\n        \"react-dom\": \">= 16.8.0\"\n      }\n    },\n    \"node_modules/styled-components/node_modules/nanoid\": {\n      \"version\": \"3.3.11\",\n      \"resolved\": \"https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz\",\n      \"integrity\": \"sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/ai\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"nanoid\": \"bin/nanoid.cjs\"\n      },\n      \"engines\": {\n        \"node\": \"^10 || ^12 || ^13.7 || ^14 || >=15.0.1\"\n      }\n    },\n    \"node_modules/styled-components/node_modules/postcss\": {\n      \"version\": \"8.4.49\",\n      \"resolved\": \"https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz\",\n      \"integrity\": \"sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==\",\n      \"funding\": [\n        {\n          \"type\": \"opencollective\",\n          \"url\": \"https://opencollective.com/postcss/\"\n        },\n        {\n          \"type\": \"tidelift\",\n          \"url\": \"https://tidelift.com/funding/github/npm/postcss\"\n        },\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/ai\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"nanoid\": \"^3.3.7\",\n        \"picocolors\": \"^1.1.1\",\n        \"source-map-js\": \"^1.2.1\"\n      },\n      \"engines\": {\n        \"node\": \"^10 || ^12 || >=14\"\n      }\n    },\n    \"node_modules/styled-components/node_modules/tslib\": {\n      \"version\": \"2.6.2\",\n      \"resolved\": \"https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz\",\n      \"integrity\": \"sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==\",\n      \"license\": \"0BSD\"\n    },\n    \"node_modules/styled-jsx\": {\n      \"version\": \"5.1.6\",\n      \"resolved\": \"https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz\",\n      \"integrity\": \"sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"client-only\": \"0.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 12.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@babel/core\": {\n          \"optional\": true\n        },\n        \"babel-plugin-macros\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/stylis\": {\n      \"version\": \"4.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz\",\n      \"integrity\": \"sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/sucrase\": {\n      \"version\": \"3.35.1\",\n      \"resolved\": \"https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz\",\n      \"integrity\": \"sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@jridgewell/gen-mapping\": \"^0.3.2\",\n        \"commander\": \"^4.0.0\",\n        \"lines-and-columns\": \"^1.1.6\",\n        \"mz\": \"^2.7.0\",\n        \"pirates\": \"^4.0.1\",\n        \"tinyglobby\": \"^0.2.11\",\n        \"ts-interface-checker\": \"^0.1.9\"\n      },\n      \"bin\": {\n        \"sucrase\": \"bin/sucrase\",\n        \"sucrase-node\": \"bin/sucrase-node\"\n      },\n      \"engines\": {\n        \"node\": \">=16 || 14 >=14.17\"\n      }\n    },\n    \"node_modules/sucrase/node_modules/commander\": {\n      \"version\": \"4.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/commander/-/commander-4.1.1.tgz\",\n      \"integrity\": \"sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 6\"\n      }\n    },\n    \"node_modules/supports-color\": {\n      \"version\": \"7.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz\",\n      \"integrity\": \"sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"has-flag\": \"^4.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/supports-preserve-symlinks-flag\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz\",\n      \"integrity\": \"sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/tabbable\": {\n      \"version\": \"6.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz\",\n      \"integrity\": \"sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/tagged-tag\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz\",\n      \"integrity\": \"sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=20\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/tailwind-merge\": {\n      \"version\": \"3.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz\",\n      \"integrity\": \"sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/dcastil\"\n      }\n    },\n    \"node_modules/tailwindcss\": {\n      \"version\": \"3.4.17\",\n      \"resolved\": \"https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz\",\n      \"integrity\": \"sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@alloc/quick-lru\": \"^5.2.0\",\n        \"arg\": \"^5.0.2\",\n        \"chokidar\": \"^3.6.0\",\n        \"didyoumean\": \"^1.2.2\",\n        \"dlv\": \"^1.1.3\",\n        \"fast-glob\": \"^3.3.2\",\n        \"glob-parent\": \"^6.0.2\",\n        \"is-glob\": \"^4.0.3\",\n        \"jiti\": \"^1.21.6\",\n        \"lilconfig\": \"^3.1.3\",\n        \"micromatch\": \"^4.0.8\",\n        \"normalize-path\": \"^3.0.0\",\n        \"object-hash\": \"^3.0.0\",\n        \"picocolors\": \"^1.1.1\",\n        \"postcss\": \"^8.4.47\",\n        \"postcss-import\": \"^15.1.0\",\n        \"postcss-js\": \"^4.0.1\",\n        \"postcss-load-config\": \"^4.0.2\",\n        \"postcss-nested\": \"^6.2.0\",\n        \"postcss-selector-parser\": \"^6.1.2\",\n        \"resolve\": \"^1.22.8\",\n        \"sucrase\": \"^3.35.0\"\n      },\n      \"bin\": {\n        \"tailwind\": \"lib/cli.js\",\n        \"tailwindcss\": \"lib/cli.js\"\n      },\n      \"engines\": {\n        \"node\": \">=14.0.0\"\n      }\n    },\n    \"node_modules/tailwindcss-animate\": {\n      \"version\": \"1.0.7\",\n      \"resolved\": \"https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz\",\n      \"integrity\": \"sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==\",\n      \"license\": \"MIT\",\n      \"peerDependencies\": {\n        \"tailwindcss\": \">=3.0.0 || insiders\"\n      }\n    },\n    \"node_modules/tailwindcss/node_modules/fast-glob\": {\n      \"version\": \"3.3.3\",\n      \"resolved\": \"https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz\",\n      \"integrity\": \"sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@nodelib/fs.stat\": \"^2.0.2\",\n        \"@nodelib/fs.walk\": \"^1.2.3\",\n        \"glob-parent\": \"^5.1.2\",\n        \"merge2\": \"^1.3.0\",\n        \"micromatch\": \"^4.0.8\"\n      },\n      \"engines\": {\n        \"node\": \">=8.6.0\"\n      }\n    },\n    \"node_modules/tailwindcss/node_modules/fast-glob/node_modules/glob-parent\": {\n      \"version\": \"5.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz\",\n      \"integrity\": \"sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"is-glob\": \"^4.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 6\"\n      }\n    },\n    \"node_modules/tailwindcss/node_modules/jiti\": {\n      \"version\": \"1.21.7\",\n      \"resolved\": \"https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz\",\n      \"integrity\": \"sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==\",\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"jiti\": \"bin/jiti.js\"\n      }\n    },\n    \"node_modules/thenify\": {\n      \"version\": \"3.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz\",\n      \"integrity\": \"sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"any-promise\": \"^1.0.0\"\n      }\n    },\n    \"node_modules/thenify-all\": {\n      \"version\": \"1.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz\",\n      \"integrity\": \"sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"thenify\": \">= 3.1.0 < 4\"\n      },\n      \"engines\": {\n        \"node\": \">=0.8\"\n      }\n    },\n    \"node_modules/tiny-invariant\": {\n      \"version\": \"1.3.3\",\n      \"resolved\": \"https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz\",\n      \"integrity\": \"sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/tinyglobby\": {\n      \"version\": \"0.2.15\",\n      \"resolved\": \"https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz\",\n      \"integrity\": \"sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"fdir\": \"^6.5.0\",\n        \"picomatch\": \"^4.0.3\"\n      },\n      \"engines\": {\n        \"node\": \">=12.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/SuperchupuDev\"\n      }\n    },\n    \"node_modules/tinyglobby/node_modules/fdir\": {\n      \"version\": \"6.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz\",\n      \"integrity\": \"sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12.0.0\"\n      },\n      \"peerDependencies\": {\n        \"picomatch\": \"^3 || ^4\"\n      },\n      \"peerDependenciesMeta\": {\n        \"picomatch\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/tinyglobby/node_modules/picomatch\": {\n      \"version\": \"4.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz\",\n      \"integrity\": \"sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/jonschlinkert\"\n      }\n    },\n    \"node_modules/tippy.js\": {\n      \"version\": \"6.3.7\",\n      \"resolved\": \"https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz\",\n      \"integrity\": \"sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@popperjs/core\": \"^2.9.0\"\n      }\n    },\n    \"node_modules/tls\": {\n      \"version\": \"0.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/tls/-/tls-0.0.1.tgz\",\n      \"integrity\": \"sha512-GzHpG+hwupY8VMR6rYsnAhTHqT/97zT45PG8WD5eTT1lq+dFE0nN+1PYpsoBcHJgSmTz5ceK2Cv88IkPmIPOtQ==\"\n    },\n    \"node_modules/to-regex-range\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz\",\n      \"integrity\": \"sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"is-number\": \"^7.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=8.0\"\n      }\n    },\n    \"node_modules/tr46\": {\n      \"version\": \"0.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz\",\n      \"integrity\": \"sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/trim-lines\": {\n      \"version\": \"3.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz\",\n      \"integrity\": \"sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/trough\": {\n      \"version\": \"2.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/trough/-/trough-2.2.0.tgz\",\n      \"integrity\": \"sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/ts-api-utils\": {\n      \"version\": \"2.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz\",\n      \"integrity\": \"sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18.12\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4\"\n      }\n    },\n    \"node_modules/ts-interface-checker\": {\n      \"version\": \"0.1.13\",\n      \"resolved\": \"https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz\",\n      \"integrity\": \"sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==\",\n      \"license\": \"Apache-2.0\"\n    },\n    \"node_modules/ts-node\": {\n      \"version\": \"10.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz\",\n      \"integrity\": \"sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==\",\n      \"devOptional\": true,\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@cspotcode/source-map-support\": \"^0.8.0\",\n        \"@tsconfig/node10\": \"^1.0.7\",\n        \"@tsconfig/node12\": \"^1.0.7\",\n        \"@tsconfig/node14\": \"^1.0.0\",\n        \"@tsconfig/node16\": \"^1.0.2\",\n        \"acorn\": \"^8.4.1\",\n        \"acorn-walk\": \"^8.1.1\",\n        \"arg\": \"^4.1.0\",\n        \"create-require\": \"^1.1.0\",\n        \"diff\": \"^4.0.1\",\n        \"make-error\": \"^1.1.1\",\n        \"v8-compile-cache-lib\": \"^3.0.1\",\n        \"yn\": \"3.1.1\"\n      },\n      \"bin\": {\n        \"ts-node\": \"dist/bin.js\",\n        \"ts-node-cwd\": \"dist/bin-cwd.js\",\n        \"ts-node-esm\": \"dist/bin-esm.js\",\n        \"ts-node-script\": \"dist/bin-script.js\",\n        \"ts-node-transpile-only\": \"dist/bin-transpile.js\",\n        \"ts-script\": \"dist/bin-script-deprecated.js\"\n      },\n      \"peerDependencies\": {\n        \"@swc/core\": \">=1.2.50\",\n        \"@swc/wasm\": \">=1.2.50\",\n        \"@types/node\": \"*\",\n        \"typescript\": \">=2.7\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@swc/core\": {\n          \"optional\": true\n        },\n        \"@swc/wasm\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/ts-node/node_modules/arg\": {\n      \"version\": \"4.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/arg/-/arg-4.1.3.tgz\",\n      \"integrity\": \"sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==\",\n      \"devOptional\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/tsconfig-paths\": {\n      \"version\": \"3.15.0\",\n      \"resolved\": \"https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz\",\n      \"integrity\": \"sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/json5\": \"^0.0.29\",\n        \"json5\": \"^1.0.2\",\n        \"minimist\": \"^1.2.6\",\n        \"strip-bom\": \"^3.0.0\"\n      }\n    },\n    \"node_modules/tslib\": {\n      \"version\": \"2.8.1\",\n      \"resolved\": \"https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz\",\n      \"integrity\": \"sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==\",\n      \"license\": \"0BSD\"\n    },\n    \"node_modules/tsx\": {\n      \"version\": \"4.20.3\",\n      \"resolved\": \"https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz\",\n      \"integrity\": \"sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"esbuild\": \"~0.25.0\",\n        \"get-tsconfig\": \"^4.7.5\"\n      },\n      \"bin\": {\n        \"tsx\": \"dist/cli.mjs\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"optionalDependencies\": {\n        \"fsevents\": \"~2.3.3\"\n      }\n    },\n    \"node_modules/tsx/node_modules/fsevents\": {\n      \"version\": \"2.3.3\",\n      \"resolved\": \"https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz\",\n      \"integrity\": \"sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==\",\n      \"dev\": true,\n      \"hasInstallScript\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"engines\": {\n        \"node\": \"^8.16.0 || ^10.6.0 || >=11.0.0\"\n      }\n    },\n    \"node_modules/type-check\": {\n      \"version\": \"0.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz\",\n      \"integrity\": \"sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"prelude-ls\": \"^1.2.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8.0\"\n      }\n    },\n    \"node_modules/type-fest\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/type-fest/-/type-fest-5.0.0.tgz\",\n      \"integrity\": \"sha512-GeJop7+u7BYlQ6yQCAY1nBQiRSHR+6OdCEtd8Bwp9a3NK3+fWAVjOaPKJDteB9f6cIJ0wt4IfnScjLG450EpXA==\",\n      \"license\": \"(MIT OR CC0-1.0)\",\n      \"dependencies\": {\n        \"tagged-tag\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=20\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/typed-array-buffer\": {\n      \"version\": \"1.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz\",\n      \"integrity\": \"sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.3\",\n        \"es-errors\": \"^1.3.0\",\n        \"is-typed-array\": \"^1.1.14\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/typed-array-byte-length\": {\n      \"version\": \"1.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz\",\n      \"integrity\": \"sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.8\",\n        \"for-each\": \"^0.3.3\",\n        \"gopd\": \"^1.2.0\",\n        \"has-proto\": \"^1.2.0\",\n        \"is-typed-array\": \"^1.1.14\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/typed-array-byte-offset\": {\n      \"version\": \"1.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz\",\n      \"integrity\": \"sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"available-typed-arrays\": \"^1.0.7\",\n        \"call-bind\": \"^1.0.8\",\n        \"for-each\": \"^0.3.3\",\n        \"gopd\": \"^1.2.0\",\n        \"has-proto\": \"^1.2.0\",\n        \"is-typed-array\": \"^1.1.15\",\n        \"reflect.getprototypeof\": \"^1.0.9\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/typed-array-length\": {\n      \"version\": \"1.0.7\",\n      \"resolved\": \"https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz\",\n      \"integrity\": \"sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bind\": \"^1.0.7\",\n        \"for-each\": \"^0.3.3\",\n        \"gopd\": \"^1.0.1\",\n        \"is-typed-array\": \"^1.1.13\",\n        \"possible-typed-array-names\": \"^1.0.0\",\n        \"reflect.getprototypeof\": \"^1.0.6\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/typescript\": {\n      \"version\": \"5.8.3\",\n      \"resolved\": \"https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz\",\n      \"integrity\": \"sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==\",\n      \"devOptional\": true,\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"bin\": {\n        \"tsc\": \"bin/tsc\",\n        \"tsserver\": \"bin/tsserver\"\n      },\n      \"engines\": {\n        \"node\": \">=14.17\"\n      }\n    },\n    \"node_modules/typescript-eslint\": {\n      \"version\": \"8.56.0\",\n      \"resolved\": \"https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz\",\n      \"integrity\": \"sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/eslint-plugin\": \"8.56.0\",\n        \"@typescript-eslint/parser\": \"8.56.0\",\n        \"@typescript-eslint/typescript-estree\": \"8.56.0\",\n        \"@typescript-eslint/utils\": \"8.56.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/uc.micro\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz\",\n      \"integrity\": \"sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/unbox-primitive\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz\",\n      \"integrity\": \"sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.3\",\n        \"has-bigints\": \"^1.0.2\",\n        \"has-symbols\": \"^1.1.0\",\n        \"which-boxed-primitive\": \"^1.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/underscore\": {\n      \"version\": \"1.13.7\",\n      \"resolved\": \"https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz\",\n      \"integrity\": \"sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/undici-types\": {\n      \"version\": \"6.21.0\",\n      \"resolved\": \"https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz\",\n      \"integrity\": \"sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==\",\n      \"devOptional\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/unicode-emoji-modifier-base\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz\",\n      \"integrity\": \"sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/unified\": {\n      \"version\": \"11.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/unified/-/unified-11.0.5.tgz\",\n      \"integrity\": \"sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/unist\": \"^3.0.0\",\n        \"bail\": \"^2.0.0\",\n        \"devlop\": \"^1.0.0\",\n        \"extend\": \"^3.0.0\",\n        \"is-plain-obj\": \"^4.0.0\",\n        \"trough\": \"^2.0.0\",\n        \"vfile\": \"^6.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/unist-util-find-after\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz\",\n      \"integrity\": \"sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/unist\": \"^3.0.0\",\n        \"unist-util-is\": \"^6.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/unist-util-is\": {\n      \"version\": \"6.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz\",\n      \"integrity\": \"sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/unist\": \"^3.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/unist-util-position\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz\",\n      \"integrity\": \"sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/unist\": \"^3.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/unist-util-remove\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-4.0.0.tgz\",\n      \"integrity\": \"sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/unist\": \"^3.0.0\",\n        \"unist-util-is\": \"^6.0.0\",\n        \"unist-util-visit-parents\": \"^6.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/unist-util-remove-position\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz\",\n      \"integrity\": \"sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/unist\": \"^3.0.0\",\n        \"unist-util-visit\": \"^5.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/unist-util-stringify-position\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz\",\n      \"integrity\": \"sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/unist\": \"^3.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/unist-util-visit\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz\",\n      \"integrity\": \"sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/unist\": \"^3.0.0\",\n        \"unist-util-is\": \"^6.0.0\",\n        \"unist-util-visit-parents\": \"^6.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/unist-util-visit-parents\": {\n      \"version\": \"6.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz\",\n      \"integrity\": \"sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/unist\": \"^3.0.0\",\n        \"unist-util-is\": \"^6.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/universalify\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz\",\n      \"integrity\": \"sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 10.0.0\"\n      }\n    },\n    \"node_modules/unrs-resolver\": {\n      \"version\": \"1.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.9.2.tgz\",\n      \"integrity\": \"sha512-VUyWiTNQD7itdiMuJy+EuLEErLj3uwX/EpHQF8EOf33Dq3Ju6VW1GXm+swk6+1h7a49uv9fKZ+dft9jU7esdLA==\",\n      \"dev\": true,\n      \"hasInstallScript\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"napi-postinstall\": \"^0.2.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/unrs-resolver\"\n      },\n      \"optionalDependencies\": {\n        \"@unrs/resolver-binding-android-arm-eabi\": \"1.9.2\",\n        \"@unrs/resolver-binding-android-arm64\": \"1.9.2\",\n        \"@unrs/resolver-binding-darwin-arm64\": \"1.9.2\",\n        \"@unrs/resolver-binding-darwin-x64\": \"1.9.2\",\n        \"@unrs/resolver-binding-freebsd-x64\": \"1.9.2\",\n        \"@unrs/resolver-binding-linux-arm-gnueabihf\": \"1.9.2\",\n        \"@unrs/resolver-binding-linux-arm-musleabihf\": \"1.9.2\",\n        \"@unrs/resolver-binding-linux-arm64-gnu\": \"1.9.2\",\n        \"@unrs/resolver-binding-linux-arm64-musl\": \"1.9.2\",\n        \"@unrs/resolver-binding-linux-ppc64-gnu\": \"1.9.2\",\n        \"@unrs/resolver-binding-linux-riscv64-gnu\": \"1.9.2\",\n        \"@unrs/resolver-binding-linux-riscv64-musl\": \"1.9.2\",\n        \"@unrs/resolver-binding-linux-s390x-gnu\": \"1.9.2\",\n        \"@unrs/resolver-binding-linux-x64-gnu\": \"1.9.2\",\n        \"@unrs/resolver-binding-linux-x64-musl\": \"1.9.2\",\n        \"@unrs/resolver-binding-wasm32-wasi\": \"1.9.2\",\n        \"@unrs/resolver-binding-win32-arm64-msvc\": \"1.9.2\",\n        \"@unrs/resolver-binding-win32-ia32-msvc\": \"1.9.2\",\n        \"@unrs/resolver-binding-win32-x64-msvc\": \"1.9.2\"\n      }\n    },\n    \"node_modules/update-browserslist-db\": {\n      \"version\": \"1.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz\",\n      \"integrity\": \"sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==\",\n      \"dev\": true,\n      \"funding\": [\n        {\n          \"type\": \"opencollective\",\n          \"url\": \"https://opencollective.com/browserslist\"\n        },\n        {\n          \"type\": \"tidelift\",\n          \"url\": \"https://tidelift.com/funding/github/npm/browserslist\"\n        },\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/ai\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"escalade\": \"^3.2.0\",\n        \"picocolors\": \"^1.1.1\"\n      },\n      \"bin\": {\n        \"update-browserslist-db\": \"cli.js\"\n      },\n      \"peerDependencies\": {\n        \"browserslist\": \">= 4.21.0\"\n      }\n    },\n    \"node_modules/uri-js\": {\n      \"version\": \"4.4.1\",\n      \"resolved\": \"https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz\",\n      \"integrity\": \"sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==\",\n      \"dev\": true,\n      \"license\": \"BSD-2-Clause\",\n      \"dependencies\": {\n        \"punycode\": \"^2.1.0\"\n      }\n    },\n    \"node_modules/use-callback-ref\": {\n      \"version\": \"1.3.3\",\n      \"resolved\": \"https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz\",\n      \"integrity\": \"sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"tslib\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"react\": \"^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/use-debounce\": {\n      \"version\": \"10.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.5.tgz\",\n      \"integrity\": \"sha512-Q76E3lnIV+4YT9AHcrHEHYmAd9LKwUAbPXDm7FlqVGDHiSOhX3RDjT8dm0AxbJup6WgOb1YEcKyCr11kBJR5KQ==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 16.0.0\"\n      },\n      \"peerDependencies\": {\n        \"react\": \"*\"\n      }\n    },\n    \"node_modules/use-sidecar\": {\n      \"version\": \"1.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz\",\n      \"integrity\": \"sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"detect-node-es\": \"^1.1.0\",\n        \"tslib\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"peerDependencies\": {\n        \"@types/react\": \"*\",\n        \"react\": \"^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/react\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/use-sync-external-store\": {\n      \"version\": \"1.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz\",\n      \"integrity\": \"sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==\",\n      \"license\": \"MIT\",\n      \"peerDependencies\": {\n        \"react\": \"^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0\"\n      }\n    },\n    \"node_modules/util\": {\n      \"version\": \"0.10.4\",\n      \"resolved\": \"https://registry.npmjs.org/util/-/util-0.10.4.tgz\",\n      \"integrity\": \"sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"inherits\": \"2.0.3\"\n      }\n    },\n    \"node_modules/util-deprecate\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz\",\n      \"integrity\": \"sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/v8-compile-cache-lib\": {\n      \"version\": \"3.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz\",\n      \"integrity\": \"sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==\",\n      \"devOptional\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/vfile\": {\n      \"version\": \"6.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz\",\n      \"integrity\": \"sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/unist\": \"^3.0.0\",\n        \"vfile-message\": \"^4.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/vfile-location\": {\n      \"version\": \"5.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz\",\n      \"integrity\": \"sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/unist\": \"^3.0.0\",\n        \"vfile\": \"^6.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/vfile-message\": {\n      \"version\": \"4.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz\",\n      \"integrity\": \"sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/unist\": \"^3.0.0\",\n        \"unist-util-stringify-position\": \"^4.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/unified\"\n      }\n    },\n    \"node_modules/victory-vendor\": {\n      \"version\": \"36.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz\",\n      \"integrity\": \"sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==\",\n      \"license\": \"MIT AND ISC\",\n      \"dependencies\": {\n        \"@types/d3-array\": \"^3.0.3\",\n        \"@types/d3-ease\": \"^3.0.0\",\n        \"@types/d3-interpolate\": \"^3.0.1\",\n        \"@types/d3-scale\": \"^4.0.2\",\n        \"@types/d3-shape\": \"^3.1.0\",\n        \"@types/d3-time\": \"^3.0.0\",\n        \"@types/d3-timer\": \"^3.0.0\",\n        \"d3-array\": \"^3.1.6\",\n        \"d3-ease\": \"^3.0.1\",\n        \"d3-interpolate\": \"^3.0.1\",\n        \"d3-scale\": \"^4.0.2\",\n        \"d3-shape\": \"^3.1.0\",\n        \"d3-time\": \"^3.0.0\",\n        \"d3-timer\": \"^3.0.1\"\n      }\n    },\n    \"node_modules/w3c-keyname\": {\n      \"version\": \"2.2.8\",\n      \"resolved\": \"https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz\",\n      \"integrity\": \"sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/walk-back\": {\n      \"version\": \"5.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/walk-back/-/walk-back-5.1.1.tgz\",\n      \"integrity\": \"sha512-e/FRLDVdZQWFrAzU6Hdvpm7D7m2ina833gIKLptQykRK49mmCYHLHq7UqjPDbxbKLZkTkW1rFqbengdE3sLfdw==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12.17\"\n      }\n    },\n    \"node_modules/web-namespaces\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz\",\n      \"integrity\": \"sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    },\n    \"node_modules/webidl-conversions\": {\n      \"version\": \"3.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz\",\n      \"integrity\": \"sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==\",\n      \"license\": \"BSD-2-Clause\"\n    },\n    \"node_modules/whatwg-url\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz\",\n      \"integrity\": \"sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"tr46\": \"~0.0.3\",\n        \"webidl-conversions\": \"^3.0.0\"\n      }\n    },\n    \"node_modules/which\": {\n      \"version\": \"2.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/which/-/which-2.0.2.tgz\",\n      \"integrity\": \"sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"isexe\": \"^2.0.0\"\n      },\n      \"bin\": {\n        \"node-which\": \"bin/node-which\"\n      },\n      \"engines\": {\n        \"node\": \">= 8\"\n      }\n    },\n    \"node_modules/which-boxed-primitive\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz\",\n      \"integrity\": \"sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"is-bigint\": \"^1.1.0\",\n        \"is-boolean-object\": \"^1.2.1\",\n        \"is-number-object\": \"^1.1.1\",\n        \"is-string\": \"^1.1.1\",\n        \"is-symbol\": \"^1.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/which-builtin-type\": {\n      \"version\": \"1.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz\",\n      \"integrity\": \"sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.2\",\n        \"function.prototype.name\": \"^1.1.6\",\n        \"has-tostringtag\": \"^1.0.2\",\n        \"is-async-function\": \"^2.0.0\",\n        \"is-date-object\": \"^1.1.0\",\n        \"is-finalizationregistry\": \"^1.1.0\",\n        \"is-generator-function\": \"^1.0.10\",\n        \"is-regex\": \"^1.2.1\",\n        \"is-weakref\": \"^1.0.2\",\n        \"isarray\": \"^2.0.5\",\n        \"which-boxed-primitive\": \"^1.1.0\",\n        \"which-collection\": \"^1.0.2\",\n        \"which-typed-array\": \"^1.1.16\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/which-collection\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz\",\n      \"integrity\": \"sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"is-map\": \"^2.0.3\",\n        \"is-set\": \"^2.0.3\",\n        \"is-weakmap\": \"^2.0.2\",\n        \"is-weakset\": \"^2.0.3\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/which-typed-array\": {\n      \"version\": \"1.1.19\",\n      \"resolved\": \"https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz\",\n      \"integrity\": \"sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"available-typed-arrays\": \"^1.0.7\",\n        \"call-bind\": \"^1.0.8\",\n        \"call-bound\": \"^1.0.4\",\n        \"for-each\": \"^0.3.5\",\n        \"get-proto\": \"^1.0.1\",\n        \"gopd\": \"^1.2.0\",\n        \"has-tostringtag\": \"^1.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/word-wrap\": {\n      \"version\": \"1.2.5\",\n      \"resolved\": \"https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz\",\n      \"integrity\": \"sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/wrap-ansi\": {\n      \"version\": \"8.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz\",\n      \"integrity\": \"sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ansi-styles\": \"^6.1.0\",\n        \"string-width\": \"^5.0.1\",\n        \"strip-ansi\": \"^7.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/wrap-ansi?sponsor=1\"\n      }\n    },\n    \"node_modules/wrap-ansi-cjs\": {\n      \"name\": \"wrap-ansi\",\n      \"version\": \"7.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz\",\n      \"integrity\": \"sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ansi-styles\": \"^4.0.0\",\n        \"string-width\": \"^4.1.0\",\n        \"strip-ansi\": \"^6.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/wrap-ansi?sponsor=1\"\n      }\n    },\n    \"node_modules/wrap-ansi-cjs/node_modules/ansi-regex\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz\",\n      \"integrity\": \"sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/wrap-ansi-cjs/node_modules/ansi-styles\": {\n      \"version\": \"4.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz\",\n      \"integrity\": \"sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"color-convert\": \"^2.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/ansi-styles?sponsor=1\"\n      }\n    },\n    \"node_modules/wrap-ansi-cjs/node_modules/emoji-regex\": {\n      \"version\": \"8.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz\",\n      \"integrity\": \"sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/wrap-ansi-cjs/node_modules/string-width\": {\n      \"version\": \"4.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz\",\n      \"integrity\": \"sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"emoji-regex\": \"^8.0.0\",\n        \"is-fullwidth-code-point\": \"^3.0.0\",\n        \"strip-ansi\": \"^6.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/wrap-ansi-cjs/node_modules/strip-ansi\": {\n      \"version\": \"6.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz\",\n      \"integrity\": \"sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ansi-regex\": \"^5.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/xmlcreate\": {\n      \"version\": \"2.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz\",\n      \"integrity\": \"sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==\",\n      \"license\": \"Apache-2.0\"\n    },\n    \"node_modules/yallist\": {\n      \"version\": \"3.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz\",\n      \"integrity\": \"sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==\",\n      \"dev\": true,\n      \"license\": \"ISC\"\n    },\n    \"node_modules/yaml\": {\n      \"version\": \"2.8.0\",\n      \"resolved\": \"https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz\",\n      \"integrity\": \"sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==\",\n      \"license\": \"ISC\",\n      \"bin\": {\n        \"yaml\": \"bin.mjs\"\n      },\n      \"engines\": {\n        \"node\": \">= 14.6\"\n      }\n    },\n    \"node_modules/yn\": {\n      \"version\": \"3.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/yn/-/yn-3.1.1.tgz\",\n      \"integrity\": \"sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==\",\n      \"devOptional\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/yocto-queue\": {\n      \"version\": \"0.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz\",\n      \"integrity\": \"sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/zod\": {\n      \"version\": \"3.25.67\",\n      \"resolved\": \"https://registry.npmjs.org/zod/-/zod-3.25.67.tgz\",\n      \"integrity\": \"sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/colinhacks\"\n      }\n    },\n    \"node_modules/zod-to-json-schema\": {\n      \"version\": \"3.24.6\",\n      \"resolved\": \"https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz\",\n      \"integrity\": \"sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==\",\n      \"license\": \"ISC\",\n      \"peerDependencies\": {\n        \"zod\": \"^3.24.1\"\n      }\n    },\n    \"node_modules/zod-validation-error\": {\n      \"version\": \"4.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz\",\n      \"integrity\": \"sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"zod\": \"^3.25.0 || ^4.0.0\"\n      }\n    },\n    \"node_modules/zwitch\": {\n      \"version\": \"2.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz\",\n      \"integrity\": \"sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/wooorm\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"changerawr\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev -p 3001\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\",\n    \"build:widget\": \"tsx scripts/widget/build.ts\",\n    \"generate-swagger\": \"tsx scripts/api/generateSwagger.ts\",\n    \"start:prod\": \"NODE_ENV=production next start\",\n    \"start:prod:win\": \"set NODE_ENV=production&& next start\",\n    \"maintenance\": \"node scripts/maintenance/server.js\",\n    \"start:with-maintenance\": \"bash docker-entrypoint.sh npm start\",\n    \"prisma:studio\": \"npx prisma studio --schema=./prisma/schema --browser none\",\n    \"prisma:generate\": \"npx prisma generate --schema=./prisma/schema\",\n    \"prisma:migrate\": \"npx prisma migrate dev --schema=./prisma/schema\",\n    \"prisma:migrate:deploy\": \"npx prisma migrate deploy --schema=./prisma/schema\",\n    \"prisma:format\": \"npx prisma format --schema=./prisma/schema\"\n  },\n  \"prisma\": {\n    \"seed\": \"ts-node --compiler-options {\\\"module\\\":\\\"CommonJS\\\"} prisma/seed.ts\",\n    \"schema\": \"./prisma/schema\"\n  },\n  \"dependencies\": {\n    \"@changerawr/markdown\": \"^1.2.0\",\n    \"@faker-js/faker\": \"^9.5.0\",\n    \"@headlessui/react\": \"^2.2.0\",\n    \"@heroicons/react\": \"^2.2.0\",\n    \"@hookform/resolvers\": \"^4.0.0\",\n    \"@icons-pack/react-simple-icons\": \"^13.6.0\",\n    \"@mistralai/mistralai\": \"^1.5.0\",\n    \"@node-saml/node-saml\": \"^5.1.0\",\n    \"@prisma/client\": \"^6.7.0\",\n    \"@radix-ui/react-accordion\": \"^1.2.3\",\n    \"@radix-ui/react-alert-dialog\": \"^1.1.6\",\n    \"@radix-ui/react-avatar\": \"^1.1.3\",\n    \"@radix-ui/react-checkbox\": \"^1.2.2\",\n    \"@radix-ui/react-collapsible\": \"^1.1.7\",\n    \"@radix-ui/react-dialog\": \"^1.1.6\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.6\",\n    \"@radix-ui/react-hover-card\": \"^1.1.7\",\n    \"@radix-ui/react-icons\": \"^1.3.2\",\n    \"@radix-ui/react-label\": \"^2.1.2\",\n    \"@radix-ui/react-popover\": \"^1.1.6\",\n    \"@radix-ui/react-progress\": \"^1.1.2\",\n    \"@radix-ui/react-radio-group\": \"^1.2.3\",\n    \"@radix-ui/react-scroll-area\": \"^1.2.3\",\n    \"@radix-ui/react-select\": \"^2.1.6\",\n    \"@radix-ui/react-separator\": \"^1.1.2\",\n    \"@radix-ui/react-slider\": \"^1.3.4\",\n    \"@radix-ui/react-slot\": \"^1.1.2\",\n    \"@radix-ui/react-switch\": \"^1.1.3\",\n    \"@radix-ui/react-tabs\": \"^1.1.3\",\n    \"@radix-ui/react-toast\": \"^1.2.6\",\n    \"@radix-ui/react-tooltip\": \"^1.1.8\",\n    \"@react-email/components\": \"^0.0.36\",\n    \"@react-email/render\": \"^1.0.6\",\n    \"@scalar/nextjs-api-reference\": \"^0.9.0\",\n    \"@simplewebauthn/browser\": \"^9.0.1\",\n    \"@simplewebauthn/server\": \"^9.0.3\",\n    \"@tanstack/react-query\": \"^5.66.0\",\n    \"@tiptap/react\": \"^2.11.5\",\n    \"@tiptap/starter-kit\": \"^2.11.5\",\n    \"@types/bcryptjs\": \"^2.4.6\",\n    \"@types/md5\": \"^2.3.5\",\n    \"@types/react-syntax-highlighter\": \"^15.5.13\",\n    \"acme-client\": \"^5.4.0\",\n    \"babel-plugin-react-compiler\": \"^1.0.0\",\n    \"bcryptjs\": \"^3.0.0\",\n    \"canvas-confetti\": \"^1.9.3\",\n    \"chalk\": \"^5.4.1\",\n    \"child_process\": \"^1.0.2\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"cmdk\": \"^1.1.1\",\n    \"comment-parser\": \"^1.4.1\",\n    \"compare-versions\": \"^6.1.1\",\n    \"cookies-next\": \"^5.1.0\",\n    \"date-fns\": \"^4.1.0\",\n    \"dompurify\": \"^3.2.4\",\n    \"dotenv\": \"^16.4.7\",\n    \"framer-motion\": \"^12.5.0\",\n    \"fs\": \"^0.0.1-security\",\n    \"fs-extra\": \"^11.3.0\",\n    \"glob\": \"^11.0.1\",\n    \"jose\": \"^5.9.6\",\n    \"jsdoc\": \"^4.0.4\",\n    \"jsdoc-api\": \"^9.3.4\",\n    \"lucide-react\": \"^0.475.0\",\n    \"md5\": \"^2.3.0\",\n    \"mobx\": \"^6.13.6\",\n    \"net\": \"^1.0.2\",\n    \"next\": \"^16.1.6\",\n    \"next-themes\": \"^0.4.4\",\n    \"nock\": \"^14.0.1\",\n    \"nodemailer\": \"^8.0.5\",\n    \"openapi-types\": \"^12.1.3\",\n    \"ora\": \"^8.2.0\",\n    \"path\": \"^0.12.7\",\n    \"perfect-freehand\": \"^1.2.2\",\n    \"playwright\": \"^1.53.1\",\n    \"prismjs\": \"^1.30.0\",\n    \"react\": \"^19.2.4\",\n    \"react-csv\": \"^2.2.2\",\n    \"react-day-picker\": \"^9.4.3\",\n    \"react-dnd\": \"^16.0.1\",\n    \"react-dnd-html5-backend\": \"^16.0.1\",\n    \"react-dom\": \"^19.2.4\",\n    \"react-hook-form\": \"^7.54.2\",\n    \"react-markdown\": \"^9.0.3\",\n    \"react-syntax-highlighter\": \"^16.1.0\",\n    \"recharts\": \"^2.15.4\",\n    \"rehype-highlight\": \"^7.0.2\",\n    \"rehype-katex\": \"^7.0.1\",\n    \"rehype-raw\": \"^7.0.0\",\n    \"rehype-sanitize\": \"^6.0.0\",\n    \"rehype-stringify\": \"^10.0.1\",\n    \"remark-emoji\": \"^5.0.1\",\n    \"remark-gfm\": \"^4.0.1\",\n    \"remark-math\": \"^6.0.0\",\n    \"remark-parse\": \"^11.0.0\",\n    \"remark-rehype\": \"^11.1.1\",\n    \"slackify-markdown\": \"^5.0.0\",\n    \"styled-components\": \"^6.1.15\",\n    \"tailwind-merge\": \"^3.0.1\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"tls\": \"^0.0.1\",\n    \"unified\": \"^11.0.5\",\n    \"use-debounce\": \"^10.0.4\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@eslint/eslintrc\": \"^3\",\n    \"@simplewebauthn/types\": \"^12.0.0\",\n    \"@tailwindcss/forms\": \"^0.5.10\",\n    \"@types/canvas-confetti\": \"^1.9.0\",\n    \"@types/fs-extra\": \"^11.0.4\",\n    \"@types/node\": \"^20\",\n    \"@types/nodemailer\": \"^6.4.17\",\n    \"@types/react\": \"19.2.2\",\n    \"@types/react-csv\": \"^1.1.10\",\n    \"@types/react-dom\": \"19.2.2\",\n    \"@types/swagger-ui-react\": \"^5.18.0\",\n    \"baseline-browser-mapping\": \"^2.9.19\",\n    \"eslint\": \"^9\",\n    \"eslint-config-next\": \"^16.1.6\",\n    \"postcss\": \"^8\",\n    \"prisma\": \"^6.7.0\",\n    \"tailwindcss\": \"^3.4.1\",\n    \"ts-node\": \"^10.9.2\",\n    \"tsx\": \"^4.19.3\",\n    \"typescript\": \"^5\"\n  },\n  \"overrides\": {\n    \"@types/react\": \"19.2.2\",\n    \"@types/react-dom\": \"19.2.2\",\n    \"minimatch\": \">=10.2.1\"\n  }\n}\n"
  },
  {
    "path": "package.json.backup",
    "content": "{\n  \"name\": \"changerawr\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\",\n    \"build:widget\": \"tsx scripts/widget/build.ts\",\n    \"generate-swagger\": \"tsx scripts/api/generateSwagger.ts\",\n    \"start:prod\": \"NODE_ENV=production next start\",\n    \"start:prod:win\": \"set NODE_ENV=production&& next start\",\n    \"maintenance\": \"node scripts/maintenance/server.js\",\n    \"start:with-maintenance\": \"bash docker-entrypoint.sh npm start\",\n    \"prisma:studio\": \"npx prisma studio --browser none\"\n  },\n  \"prisma\": {\n    \"seed\": \"ts-node --compiler-options {\\\"module\\\":\\\"CommonJS\\\"} prisma/seed.ts\"\n  },\n  \"dependencies\": {\n    \"@changerawr/markdown\": \"^1.1.5\",\n    \"@faker-js/faker\": \"^9.5.0\",\n    \"@headlessui/react\": \"^2.2.0\",\n    \"@heroicons/react\": \"^2.2.0\",\n    \"@hookform/resolvers\": \"^4.0.0\",\n    \"@icons-pack/react-simple-icons\": \"^13.6.0\",\n    \"@mistralai/mistralai\": \"^1.5.0\",\n    \"@prisma/client\": \"^6.6.0\",\n    \"@radix-ui/react-accordion\": \"^1.2.3\",\n    \"@radix-ui/react-alert-dialog\": \"^1.1.6\",\n    \"@radix-ui/react-avatar\": \"^1.1.3\",\n    \"@radix-ui/react-checkbox\": \"^1.2.2\",\n    \"@radix-ui/react-collapsible\": \"^1.1.7\",\n    \"@radix-ui/react-dialog\": \"^1.1.6\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.6\",\n    \"@radix-ui/react-hover-card\": \"^1.1.7\",\n    \"@radix-ui/react-icons\": \"^1.3.2\",\n    \"@radix-ui/react-label\": \"^2.1.2\",\n    \"@radix-ui/react-popover\": \"^1.1.6\",\n    \"@radix-ui/react-progress\": \"^1.1.2\",\n    \"@radix-ui/react-radio-group\": \"^1.2.3\",\n    \"@radix-ui/react-scroll-area\": \"^1.2.3\",\n    \"@radix-ui/react-select\": \"^2.1.6\",\n    \"@radix-ui/react-separator\": \"^1.1.2\",\n    \"@radix-ui/react-slider\": \"^1.3.4\",\n    \"@radix-ui/react-slot\": \"^1.1.2\",\n    \"@radix-ui/react-switch\": \"^1.1.3\",\n    \"@radix-ui/react-tabs\": \"^1.1.3\",\n    \"@radix-ui/react-toast\": \"^1.2.6\",\n    \"@radix-ui/react-tooltip\": \"^1.1.8\",\n    \"@react-email/components\": \"^0.0.36\",\n    \"@react-email/render\": \"^1.0.6\",\n    \"@scalar/nextjs-api-reference\": \"^0.9.0\",\n    \"@simplewebauthn/browser\": \"^9.0.1\",\n    \"@simplewebauthn/server\": \"^9.0.3\",\n    \"@tanstack/react-query\": \"^5.66.0\",\n    \"@tiptap/react\": \"^2.11.5\",\n    \"@tiptap/starter-kit\": \"^2.11.5\",\n    \"@types/bcryptjs\": \"^2.4.6\",\n    \"@types/md5\": \"^2.3.5\",\n    \"@types/react-syntax-highlighter\": \"^15.5.13\",\n    \"babel-plugin-react-compiler\": \"^1.0.0\",\n    \"bcryptjs\": \"^3.0.0\",\n    \"canvas-confetti\": \"^1.9.3\",\n    \"chalk\": \"^5.4.1\",\n    \"child_process\": \"^1.0.2\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"cmdk\": \"^1.1.1\",\n    \"comment-parser\": \"^1.4.1\",\n    \"compare-versions\": \"^6.1.1\",\n    \"cookies-next\": \"^5.1.0\",\n    \"date-fns\": \"^4.1.0\",\n    \"dompurify\": \"^3.2.4\",\n    \"dotenv\": \"^16.4.7\",\n    \"framer-motion\": \"^12.5.0\",\n    \"fs\": \"^0.0.1-security\",\n    \"fs-extra\": \"^11.3.0\",\n    \"glob\": \"^11.0.1\",\n    \"jose\": \"^5.9.6\",\n    \"jsdoc\": \"^4.0.4\",\n    \"jsdoc-api\": \"^9.3.4\",\n    \"lucide-react\": \"^0.475.0\",\n    \"md5\": \"^2.3.0\",\n    \"mobx\": \"^6.13.6\",\n    \"net\": \"^1.0.2\",\n    \"next\": \"^16.1.6\",\n    \"next-themes\": \"^0.4.4\",\n    \"nock\": \"^14.0.1\",\n    \"nodemailer\": \"^7.0.11\",\n    \"openapi-types\": \"^12.1.3\",\n    \"ora\": \"^8.2.0\",\n    \"path\": \"^0.12.7\",\n    \"perfect-freehand\": \"^1.2.2\",\n    \"playwright\": \"^1.53.1\",\n    \"prismjs\": \"^1.30.0\",\n    \"react\": \"^19.2.4\",\n    \"react-csv\": \"^2.2.2\",\n    \"react-day-picker\": \"^9.4.3\",\n    \"react-dnd\": \"^16.0.1\",\n    \"react-dnd-html5-backend\": \"^16.0.1\",\n    \"react-dom\": \"^19.2.4\",\n    \"react-hook-form\": \"^7.54.2\",\n    \"react-markdown\": \"^9.0.3\",\n    \"react-syntax-highlighter\": \"^16.1.0\",\n    \"recharts\": \"^2.15.4\",\n    \"rehype-highlight\": \"^7.0.2\",\n    \"rehype-katex\": \"^7.0.1\",\n    \"rehype-raw\": \"^7.0.0\",\n    \"rehype-sanitize\": \"^6.0.0\",\n    \"rehype-stringify\": \"^10.0.1\",\n    \"remark-emoji\": \"^5.0.1\",\n    \"remark-gfm\": \"^4.0.1\",\n    \"remark-math\": \"^6.0.0\",\n    \"remark-parse\": \"^11.0.0\",\n    \"remark-rehype\": \"^11.1.1\",\n    \"slackify-markdown\": \"^5.0.0\",\n    \"styled-components\": \"^6.1.15\",\n    \"tailwind-merge\": \"^3.0.1\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"tls\": \"^0.0.1\",\n    \"unified\": \"^11.0.5\",\n    \"use-debounce\": \"^10.0.4\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@eslint/eslintrc\": \"^3\",\n    \"@simplewebauthn/types\": \"^12.0.0\",\n    \"@tailwindcss/forms\": \"^0.5.10\",\n    \"@types/canvas-confetti\": \"^1.9.0\",\n    \"@types/fs-extra\": \"^11.0.4\",\n    \"@types/node\": \"^20\",\n    \"@types/nodemailer\": \"^6.4.17\",\n    \"@types/react\": \"19.2.2\",\n    \"@types/react-csv\": \"^1.1.10\",\n    \"@types/react-dom\": \"19.2.2\",\n    \"@types/swagger-ui-react\": \"^5.18.0\",\n    \"baseline-browser-mapping\": \"^2.9.19\",\n    \"eslint\": \"^9\",\n    \"eslint-config-next\": \"^16.1.6\",\n    \"postcss\": \"^8\",\n    \"prisma\": \"^6.6.0\",\n    \"tailwindcss\": \"^3.4.1\",\n    \"ts-node\": \"^10.9.2\",\n    \"tsx\": \"^4.19.3\",\n    \"typescript\": \"^5\"\n  },\n  \"overrides\": {\n    \"@types/react\": \"19.2.2\",\n    \"@types/react-dom\": \"19.2.2\",\n    \"minimatch\": \">=10.2.1\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.mjs",
    "content": "/** @type {import('postcss-load-config').Config} */\nconst config = {\n  plugins: {\n    tailwindcss: {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "prisma/migrations/20250215042251_init_authentication/migration.sql",
    "content": "-- CreateEnum\nCREATE TYPE \"Role\" AS ENUM ('ADMIN', 'STAFF');\n\n-- CreateTable\nCREATE TABLE \"User\" (\n    \"id\" TEXT NOT NULL,\n    \"email\" TEXT NOT NULL,\n    \"password\" TEXT NOT NULL,\n    \"name\" TEXT,\n    \"role\" \"Role\" NOT NULL DEFAULT 'STAFF',\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"User_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"RefreshToken\" (\n    \"id\" TEXT NOT NULL,\n    \"token\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"expiresAt\" TIMESTAMP(3) NOT NULL,\n    \"invalidated\" BOOLEAN NOT NULL DEFAULT false,\n\n    CONSTRAINT \"RefreshToken_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"Settings\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"theme\" TEXT NOT NULL DEFAULT 'light',\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"Settings_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"User_email_key\" ON \"User\"(\"email\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"RefreshToken_token_key\" ON \"RefreshToken\"(\"token\");\n\n-- CreateIndex\nCREATE INDEX \"RefreshToken_userId_idx\" ON \"RefreshToken\"(\"userId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"Settings_userId_key\" ON \"Settings\"(\"userId\");\n\n-- CreateIndex\nCREATE INDEX \"Settings_userId_idx\" ON \"Settings\"(\"userId\");\n\n-- AddForeignKey\nALTER TABLE \"RefreshToken\" ADD CONSTRAINT \"RefreshToken_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"Settings\" ADD CONSTRAINT \"Settings_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations/20250215051621_add_invitation_links/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"InvitationLink\" (\n    \"id\" TEXT NOT NULL,\n    \"token\" TEXT NOT NULL,\n    \"role\" \"Role\" NOT NULL,\n    \"email\" TEXT NOT NULL,\n    \"createdBy\" TEXT NOT NULL,\n    \"expiresAt\" TIMESTAMP(3) NOT NULL,\n    \"usedAt\" TIMESTAMP(3),\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n    CONSTRAINT \"InvitationLink_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"InvitationLink_token_key\" ON \"InvitationLink\"(\"token\");\n\n-- CreateIndex\nCREATE INDEX \"InvitationLink_token_idx\" ON \"InvitationLink\"(\"token\");\n\n-- CreateIndex\nCREATE INDEX \"InvitationLink_email_idx\" ON \"InvitationLink\"(\"email\");\n"
  },
  {
    "path": "prisma/migrations/20250215063030_add_last_logged_in_at_field/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"User\" ADD COLUMN     \"lastLoginAt\" TIMESTAMP(3);\n"
  },
  {
    "path": "prisma/migrations/20250215075317_add_changelog_models/migration.sql",
    "content": "-- CreateEnum\nCREATE TYPE \"RequestType\" AS ENUM ('DELETE_ENTRY', 'DELETE_TAG');\n\n-- CreateEnum\nCREATE TYPE \"RequestStatus\" AS ENUM ('PENDING', 'APPROVED', 'REJECTED');\n\n-- CreateTable\nCREATE TABLE \"Project\" (\n    \"id\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"Project_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"Changelog\" (\n    \"id\" TEXT NOT NULL,\n    \"projectId\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"Changelog_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"ChangelogEntry\" (\n    \"id\" TEXT NOT NULL,\n    \"title\" TEXT NOT NULL,\n    \"content\" TEXT NOT NULL,\n    \"version\" TEXT,\n    \"publishedAt\" TIMESTAMP(3),\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n    \"changelogId\" TEXT NOT NULL,\n\n    CONSTRAINT \"ChangelogEntry_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"ChangelogTag\" (\n    \"id\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"ChangelogTag_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"ChangelogRequest\" (\n    \"id\" TEXT NOT NULL,\n    \"type\" \"RequestType\" NOT NULL,\n    \"status\" \"RequestStatus\" NOT NULL DEFAULT 'PENDING',\n    \"staffId\" TEXT NOT NULL,\n    \"adminId\" TEXT,\n    \"changelogEntryId\" TEXT,\n    \"changelogTagId\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"reviewedAt\" TIMESTAMP(3),\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"ChangelogRequest_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"_ChangelogEntryToChangelogTag\" (\n    \"A\" TEXT NOT NULL,\n    \"B\" TEXT NOT NULL,\n\n    CONSTRAINT \"_ChangelogEntryToChangelogTag_AB_pkey\" PRIMARY KEY (\"A\",\"B\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"Changelog_projectId_key\" ON \"Changelog\"(\"projectId\");\n\n-- CreateIndex\nCREATE INDEX \"ChangelogEntry_changelogId_idx\" ON \"ChangelogEntry\"(\"changelogId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"ChangelogTag_name_key\" ON \"ChangelogTag\"(\"name\");\n\n-- CreateIndex\nCREATE INDEX \"ChangelogRequest_staffId_idx\" ON \"ChangelogRequest\"(\"staffId\");\n\n-- CreateIndex\nCREATE INDEX \"ChangelogRequest_adminId_idx\" ON \"ChangelogRequest\"(\"adminId\");\n\n-- CreateIndex\nCREATE INDEX \"ChangelogRequest_changelogEntryId_idx\" ON \"ChangelogRequest\"(\"changelogEntryId\");\n\n-- CreateIndex\nCREATE INDEX \"ChangelogRequest_changelogTagId_idx\" ON \"ChangelogRequest\"(\"changelogTagId\");\n\n-- CreateIndex\nCREATE INDEX \"_ChangelogEntryToChangelogTag_B_index\" ON \"_ChangelogEntryToChangelogTag\"(\"B\");\n\n-- AddForeignKey\nALTER TABLE \"Changelog\" ADD CONSTRAINT \"Changelog_projectId_fkey\" FOREIGN KEY (\"projectId\") REFERENCES \"Project\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"ChangelogEntry\" ADD CONSTRAINT \"ChangelogEntry_changelogId_fkey\" FOREIGN KEY (\"changelogId\") REFERENCES \"Changelog\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"ChangelogRequest\" ADD CONSTRAINT \"ChangelogRequest_staffId_fkey\" FOREIGN KEY (\"staffId\") REFERENCES \"User\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"ChangelogRequest\" ADD CONSTRAINT \"ChangelogRequest_adminId_fkey\" FOREIGN KEY (\"adminId\") REFERENCES \"User\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"ChangelogRequest\" ADD CONSTRAINT \"ChangelogRequest_changelogEntryId_fkey\" FOREIGN KEY (\"changelogEntryId\") REFERENCES \"ChangelogEntry\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"ChangelogRequest\" ADD CONSTRAINT \"ChangelogRequest_changelogTagId_fkey\" FOREIGN KEY (\"changelogTagId\") REFERENCES \"ChangelogTag\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"_ChangelogEntryToChangelogTag\" ADD CONSTRAINT \"_ChangelogEntryToChangelogTag_A_fkey\" FOREIGN KEY (\"A\") REFERENCES \"ChangelogEntry\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"_ChangelogEntryToChangelogTag\" ADD CONSTRAINT \"_ChangelogEntryToChangelogTag_B_fkey\" FOREIGN KEY (\"B\") REFERENCES \"ChangelogTag\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations/20250215080528_add_viewer_role/migration.sql",
    "content": "-- AlterEnum\nALTER TYPE \"Role\" ADD VALUE 'VIEWER';\n"
  },
  {
    "path": "prisma/migrations/20250215180005_add_project_settings/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Project\" ADD COLUMN     \"allowAutoPublish\" BOOLEAN NOT NULL DEFAULT false,\nADD COLUMN     \"defaultTags\" TEXT[] DEFAULT ARRAY[]::TEXT[],\nADD COLUMN     \"isPublic\" BOOLEAN NOT NULL DEFAULT false,\nADD COLUMN     \"requireApproval\" BOOLEAN NOT NULL DEFAULT true;\n"
  },
  {
    "path": "prisma/migrations/20250215232640_add_api_keys/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"ApiKey\" (\n    \"id\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"key\" TEXT NOT NULL,\n    \"lastUsed\" TIMESTAMP(3),\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"expiresAt\" TIMESTAMP(3),\n    \"userId\" TEXT NOT NULL,\n    \"permissions\" TEXT[],\n    \"isRevoked\" BOOLEAN NOT NULL DEFAULT false,\n\n    CONSTRAINT \"ApiKey_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"ApiKey_key_key\" ON \"ApiKey\"(\"key\");\n\n-- CreateIndex\nCREATE INDEX \"ApiKey_userId_idx\" ON \"ApiKey\"(\"userId\");\n\n-- CreateIndex\nCREATE INDEX \"ApiKey_key_idx\" ON \"ApiKey\"(\"key\");\n\n-- AddForeignKey\nALTER TABLE \"ApiKey\" ADD CONSTRAINT \"ApiKey_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations/20250216005356_add_audit_logs/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"AuditLog\" (\n    \"id\" TEXT NOT NULL,\n    \"action\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"targetUserId\" TEXT,\n    \"details\" JSONB,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n    CONSTRAINT \"AuditLog_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE INDEX \"AuditLog_userId_idx\" ON \"AuditLog\"(\"userId\");\n\n-- CreateIndex\nCREATE INDEX \"AuditLog_targetUserId_idx\" ON \"AuditLog\"(\"targetUserId\");\n\n-- CreateIndex\nCREATE INDEX \"AuditLog_action_idx\" ON \"AuditLog\"(\"action\");\n\n-- CreateIndex\nCREATE INDEX \"AuditLog_createdAt_idx\" ON \"AuditLog\"(\"createdAt\");\n\n-- AddForeignKey\nALTER TABLE \"AuditLog\" ADD CONSTRAINT \"AuditLog_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"AuditLog\" ADD CONSTRAINT \"AuditLog_targetUserId_fkey\" FOREIGN KEY (\"targetUserId\") REFERENCES \"User\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations/20250216050812_add_system_configuration/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"SystemConfig\" (\n    \"id\" INTEGER NOT NULL DEFAULT 1,\n    \"defaultInvitationExpiry\" INTEGER NOT NULL DEFAULT 7,\n    \"requireApprovalForChangelogs\" BOOLEAN NOT NULL DEFAULT true,\n    \"maxChangelogEntriesPerProject\" INTEGER NOT NULL DEFAULT 100,\n    \"enableAnalytics\" BOOLEAN NOT NULL DEFAULT true,\n    \"enableNotifications\" BOOLEAN NOT NULL DEFAULT true,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"SystemConfig_pkey\" PRIMARY KEY (\"id\")\n);\n"
  },
  {
    "path": "prisma/migrations/20250216053424_update_changelog_requests_schema/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the column `updatedAt` on the `ChangelogRequest` table. All the data in the column will be lost.\n  - The `status` column on the `ChangelogRequest` table would be dropped and recreated. This will lead to data loss if there is data in the column.\n  - Added the required column `projectId` to the `ChangelogRequest` table without a default value. This is not possible if the table is not empty.\n  - Changed the type of `type` on the `ChangelogRequest` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.\n\n*/\n-- DropIndex\nDROP INDEX \"ChangelogRequest_changelogEntryId_idx\";\n\n-- DropIndex\nDROP INDEX \"ChangelogRequest_changelogTagId_idx\";\n\n-- AlterTable\nALTER TABLE \"ChangelogRequest\" DROP COLUMN \"updatedAt\",\nADD COLUMN     \"projectId\" TEXT NOT NULL,\nADD COLUMN     \"targetId\" TEXT,\nDROP COLUMN \"type\",\nADD COLUMN     \"type\" TEXT NOT NULL,\nDROP COLUMN \"status\",\nADD COLUMN     \"status\" TEXT NOT NULL DEFAULT 'PENDING';\n\n-- CreateIndex\nCREATE INDEX \"ChangelogRequest_projectId_idx\" ON \"ChangelogRequest\"(\"projectId\");\n\n-- CreateIndex\nCREATE INDEX \"ChangelogRequest_status_idx\" ON \"ChangelogRequest\"(\"status\");\n\n-- AddForeignKey\nALTER TABLE \"ChangelogRequest\" ADD CONSTRAINT \"ChangelogRequest_projectId_fkey\" FOREIGN KEY (\"projectId\") REFERENCES \"Project\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations/20250221000121_add_new_request_types/migration.sql",
    "content": "-- AlterEnum\n-- This migration adds more than one value to an enum.\n-- With PostgreSQL versions 11 and earlier, this is not possible\n-- in a single migration. This can be worked around by creating\n-- multiple migrations, each migration adding only one value to\n-- the enum.\n\n\nALTER TYPE \"RequestType\" ADD VALUE 'DELETE_PROJECT';\nALTER TYPE \"RequestType\" ADD VALUE 'ALLOW_PUBLISH';\n"
  },
  {
    "path": "prisma/migrations/20250305225249_add_oauth/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"OAuthProvider\" (\n    \"id\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"clientId\" TEXT NOT NULL,\n    \"clientSecret\" TEXT NOT NULL,\n    \"authorizationUrl\" TEXT NOT NULL,\n    \"tokenUrl\" TEXT NOT NULL,\n    \"userInfoUrl\" TEXT NOT NULL,\n    \"callbackUrl\" TEXT NOT NULL,\n    \"scopes\" TEXT[],\n    \"enabled\" BOOLEAN NOT NULL DEFAULT true,\n    \"isDefault\" BOOLEAN NOT NULL DEFAULT false,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"OAuthProvider_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"OAuthConnection\" (\n    \"id\" TEXT NOT NULL,\n    \"providerId\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"providerUserId\" TEXT NOT NULL,\n    \"accessToken\" TEXT,\n    \"refreshToken\" TEXT,\n    \"expiresAt\" TIMESTAMP(3),\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"OAuthConnection_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE INDEX \"OAuthConnection_userId_idx\" ON \"OAuthConnection\"(\"userId\");\n\n-- CreateIndex\nCREATE INDEX \"OAuthConnection_providerId_idx\" ON \"OAuthConnection\"(\"providerId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"OAuthConnection_providerId_providerUserId_key\" ON \"OAuthConnection\"(\"providerId\", \"providerUserId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"OAuthConnection_providerId_userId_key\" ON \"OAuthConnection\"(\"providerId\", \"userId\");\n\n-- AddForeignKey\nALTER TABLE \"OAuthConnection\" ADD CONSTRAINT \"OAuthConnection_providerId_fkey\" FOREIGN KEY (\"providerId\") REFERENCES \"OAuthProvider\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"OAuthConnection\" ADD CONSTRAINT \"OAuthConnection_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations/20250418044737_add_email_integration/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"EmailConfig\" (\n    \"id\" TEXT NOT NULL,\n    \"projectId\" TEXT NOT NULL,\n    \"enabled\" BOOLEAN NOT NULL DEFAULT false,\n    \"smtpHost\" TEXT NOT NULL,\n    \"smtpPort\" INTEGER NOT NULL,\n    \"smtpUser\" TEXT,\n    \"smtpPassword\" TEXT,\n    \"smtpSecure\" BOOLEAN NOT NULL DEFAULT true,\n    \"fromEmail\" TEXT NOT NULL,\n    \"fromName\" TEXT,\n    \"replyToEmail\" TEXT,\n    \"defaultSubject\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n    \"lastTestedAt\" TIMESTAMP(3),\n    \"testStatus\" TEXT,\n\n    CONSTRAINT \"EmailConfig_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"EmailConfig_projectId_key\" ON \"EmailConfig\"(\"projectId\");\n\n-- AddForeignKey\nALTER TABLE \"EmailConfig\" ADD CONSTRAINT \"EmailConfig_projectId_fkey\" FOREIGN KEY (\"projectId\") REFERENCES \"Project\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations/20250418045834_add_email_logs/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"EmailLog\" (\n    \"id\" TEXT NOT NULL,\n    \"projectId\" TEXT NOT NULL,\n    \"recipients\" TEXT[],\n    \"subject\" TEXT NOT NULL,\n    \"messageId\" TEXT,\n    \"type\" TEXT NOT NULL,\n    \"entryIds\" TEXT[],\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n    CONSTRAINT \"EmailLog_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- AddForeignKey\nALTER TABLE \"EmailLog\" ADD CONSTRAINT \"EmailLog_projectId_fkey\" FOREIGN KEY (\"projectId\") REFERENCES \"Project\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations/20250418161805_add_email_log_enums/migration.sql",
    "content": "-- CreateEnum\nCREATE TYPE \"emailType\" AS ENUM ('SINGLE_UPDATE', 'DIGEST');\n"
  },
  {
    "path": "prisma/migrations/20250418163601_add_email_subscription/migration.sql",
    "content": "-- CreateEnum\nCREATE TYPE \"SubscriptionType\" AS ENUM ('ALL_UPDATES', 'MAJOR_ONLY', 'DIGEST_ONLY');\n\n-- CreateTable\nCREATE TABLE \"EmailSubscriber\" (\n    \"id\" TEXT NOT NULL,\n    \"email\" TEXT NOT NULL,\n    \"name\" TEXT,\n    \"isActive\" BOOLEAN NOT NULL DEFAULT true,\n    \"unsubscribeToken\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n    \"lastEmailSentAt\" TIMESTAMP(3),\n\n    CONSTRAINT \"EmailSubscriber_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"ProjectSubscription\" (\n    \"id\" TEXT NOT NULL,\n    \"projectId\" TEXT NOT NULL,\n    \"subscriberId\" TEXT NOT NULL,\n    \"subscriptionType\" \"SubscriptionType\" NOT NULL DEFAULT 'ALL_UPDATES',\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"ProjectSubscription_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"EmailSubscriber_unsubscribeToken_key\" ON \"EmailSubscriber\"(\"unsubscribeToken\");\n\n-- CreateIndex\nCREATE INDEX \"EmailSubscriber_email_idx\" ON \"EmailSubscriber\"(\"email\");\n\n-- CreateIndex\nCREATE INDEX \"EmailSubscriber_unsubscribeToken_idx\" ON \"EmailSubscriber\"(\"unsubscribeToken\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"EmailSubscriber_email_key\" ON \"EmailSubscriber\"(\"email\");\n\n-- CreateIndex\nCREATE INDEX \"ProjectSubscription_projectId_idx\" ON \"ProjectSubscription\"(\"projectId\");\n\n-- CreateIndex\nCREATE INDEX \"ProjectSubscription_subscriberId_idx\" ON \"ProjectSubscription\"(\"subscriberId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"ProjectSubscription_projectId_subscriberId_key\" ON \"ProjectSubscription\"(\"projectId\", \"subscriberId\");\n\n-- AddForeignKey\nALTER TABLE \"ProjectSubscription\" ADD CONSTRAINT \"ProjectSubscription_projectId_fkey\" FOREIGN KEY (\"projectId\") REFERENCES \"Project\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"ProjectSubscription\" ADD CONSTRAINT \"ProjectSubscription_subscriberId_fkey\" FOREIGN KEY (\"subscriberId\") REFERENCES \"EmailSubscriber\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations/20250419000001_add_password_reset/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"PasswordReset\" (\n                                 \"id\" TEXT NOT NULL,\n                                 \"token\" TEXT NOT NULL,\n                                 \"userId\" TEXT NOT NULL,\n                                 \"email\" TEXT NOT NULL,\n                                 \"expiresAt\" TIMESTAMP(3) NOT NULL,\n                                 \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n                                 \"usedAt\" TIMESTAMP(3),\n\n                                 CONSTRAINT \"PasswordReset_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"PasswordReset_token_key\" ON \"PasswordReset\"(\"token\");\n\n-- CreateIndex\nCREATE INDEX \"PasswordReset_token_idx\" ON \"PasswordReset\"(\"token\");\n\n-- CreateIndex\nCREATE INDEX \"PasswordReset_userId_idx\" ON \"PasswordReset\"(\"userId\");\n\n-- CreateIndex\nCREATE INDEX \"PasswordReset_email_idx\" ON \"PasswordReset\"(\"email\");\n\n-- AddForeignKey\nALTER TABLE \"PasswordReset\" ADD CONSTRAINT \"PasswordReset_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AlterTable\nALTER TABLE \"SystemConfig\" ADD COLUMN \"enablePasswordReset\" BOOLEAN NOT NULL DEFAULT false;\nALTER TABLE \"SystemConfig\" ADD COLUMN \"smtpHost\" TEXT;\nALTER TABLE \"SystemConfig\" ADD COLUMN \"smtpPort\" INTEGER;\nALTER TABLE \"SystemConfig\" ADD COLUMN \"smtpUser\" TEXT;\nALTER TABLE \"SystemConfig\" ADD COLUMN \"smtpPassword\" TEXT;\nALTER TABLE \"SystemConfig\" ADD COLUMN \"smtpSecure\" BOOLEAN;\nALTER TABLE \"SystemConfig\" ADD COLUMN \"systemEmail\" TEXT;"
  },
  {
    "path": "prisma/migrations/20250421000001_add_notification_preferences/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Settings\" ADD COLUMN \"enableNotifications\" BOOLEAN NOT NULL DEFAULT true;"
  },
  {
    "path": "prisma/migrations/20250504203021_passkey_support/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"Passkey\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"credentialId\" TEXT NOT NULL,\n    \"publicKey\" TEXT NOT NULL,\n    \"counter\" INTEGER NOT NULL DEFAULT 0,\n    \"transports\" TEXT[],\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"lastUsedAt\" TIMESTAMP(3),\n\n    CONSTRAINT \"Passkey_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"Passkey_credentialId_key\" ON \"Passkey\"(\"credentialId\");\n\n-- CreateIndex\nCREATE INDEX \"Passkey_userId_idx\" ON \"Passkey\"(\"userId\");\n\n-- CreateIndex\nCREATE INDEX \"Passkey_credentialId_idx\" ON \"Passkey\"(\"credentialId\");\n\n-- AddForeignKey\nALTER TABLE \"Passkey\" ADD CONSTRAINT \"Passkey_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations/20250504203707_add_last_challenge/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"User\" ADD COLUMN     \"lastChallenge\" TEXT;\n"
  },
  {
    "path": "prisma/migrations/20250504211358_add_two_factor_mode/migration.sql",
    "content": "-- CreateEnum\nCREATE TYPE \"TwoFactorMode\" AS ENUM ('NONE', 'PASSKEY_PLUS_PASSWORD', 'PASSWORD_PLUS_PASSKEY');\n\n-- AlterTable\nALTER TABLE \"User\" ADD COLUMN     \"twoFactorMode\" \"TwoFactorMode\" DEFAULT 'NONE';\n"
  },
  {
    "path": "prisma/migrations/20250504212400_add_two_factor_session/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"TwoFactorSession\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"type\" \"TwoFactorMode\" NOT NULL,\n    \"expiresAt\" TIMESTAMP(3) NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n    CONSTRAINT \"TwoFactorSession_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE INDEX \"TwoFactorSession_userId_idx\" ON \"TwoFactorSession\"(\"userId\");\n\n-- CreateIndex\nCREATE INDEX \"TwoFactorSession_expiresAt_idx\" ON \"TwoFactorSession\"(\"expiresAt\");\n\n-- AddForeignKey\nALTER TABLE \"TwoFactorSession\" ADD CONSTRAINT \"TwoFactorSession_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations/20250511204818_add_ai_assistant_config/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"SystemConfig\" ADD COLUMN     \"aiApiKey\" TEXT,\nADD COLUMN     \"aiApiProvider\" TEXT DEFAULT 'secton',\nADD COLUMN     \"aiDefaultModel\" TEXT DEFAULT 'copilot-zero',\nADD COLUMN     \"enableAIAssistant\" BOOLEAN NOT NULL DEFAULT false;\n"
  },
  {
    "path": "prisma/migrations/20250512000000_add_github_integration/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"GitHubIntegration\" (\n                                     \"id\" TEXT NOT NULL,\n                                     \"projectId\" TEXT NOT NULL,\n                                     \"repositoryUrl\" TEXT NOT NULL,\n                                     \"accessToken\" TEXT NOT NULL,\n                                     \"defaultBranch\" TEXT NOT NULL DEFAULT 'main',\n                                     \"lastSyncAt\" TIMESTAMP(3),\n                                     \"lastCommitSha\" TEXT,\n                                     \"includeBreakingChanges\" BOOLEAN NOT NULL DEFAULT true,\n                                     \"includeFixes\" BOOLEAN NOT NULL DEFAULT true,\n                                     \"includeFeatures\" BOOLEAN NOT NULL DEFAULT true,\n                                     \"includeChores\" BOOLEAN NOT NULL DEFAULT false,\n                                     \"customCommitTypes\" TEXT[] DEFAULT ARRAY[]::TEXT[],\n                                     \"enabled\" BOOLEAN NOT NULL DEFAULT true,\n                                     \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n                                     \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n                                     CONSTRAINT \"GitHubIntegration_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"GitHubIntegration_projectId_key\" ON \"GitHubIntegration\"(\"projectId\");\n\n-- CreateIndex\nCREATE INDEX \"GitHubIntegration_projectId_idx\" ON \"GitHubIntegration\"(\"projectId\");\n\n-- CreateIndex\nCREATE INDEX \"GitHubIntegration_enabled_idx\" ON \"GitHubIntegration\"(\"enabled\");\n\n-- AddForeignKey\nALTER TABLE \"GitHubIntegration\" ADD CONSTRAINT \"GitHubIntegration_projectId_fkey\" FOREIGN KEY (\"projectId\") REFERENCES \"Project\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;"
  },
  {
    "path": "prisma/migrations/20250610120000_fix_user_deletion_constraints/migration.sql",
    "content": "-- Make staffId nullable in ChangelogRequest (if not already)\nALTER TABLE \"ChangelogRequest\" ALTER COLUMN \"staffId\" DROP NOT NULL;\n\n-- Update AuditLog foreign key constraints to SET NULL on delete\nALTER TABLE \"AuditLog\" DROP CONSTRAINT IF EXISTS \"AuditLog_userId_fkey\";\nALTER TABLE \"AuditLog\" ADD CONSTRAINT \"AuditLog_userId_fkey\"\n    FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n\n-- Update targetUserId constraint to SET NULL on delete\nALTER TABLE \"AuditLog\" DROP CONSTRAINT IF EXISTS \"AuditLog_targetUserId_fkey\";\nALTER TABLE \"AuditLog\" ADD CONSTRAINT \"AuditLog_targetUserId_fkey\"\n    FOREIGN KEY (\"targetUserId\") REFERENCES \"User\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n\n-- Update ChangelogRequest staffId foreign key constraint to SET NULL on delete\nALTER TABLE \"ChangelogRequest\" DROP CONSTRAINT IF EXISTS \"ChangelogRequest_staffId_fkey\";\nALTER TABLE \"ChangelogRequest\" ADD CONSTRAINT \"ChangelogRequest_staffId_fkey\"\n    FOREIGN KEY (\"staffId\") REFERENCES \"User\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n\n-- Update ChangelogRequest adminId constraint to SET NULL on delete (should already be correct)\nALTER TABLE \"ChangelogRequest\" DROP CONSTRAINT IF EXISTS \"ChangelogRequest_adminId_fkey\";\nALTER TABLE \"ChangelogRequest\" ADD CONSTRAINT \"ChangelogRequest_adminId_fkey\"\n    FOREIGN KEY (\"adminId\") REFERENCES \"User\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;"
  },
  {
    "path": "prisma/migrations/20250613040144_changelog_analytics/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"PublicChangelogAnalytics\"\n(\n    \"id\"               TEXT         NOT NULL,\n    \"projectId\"        TEXT         NOT NULL,\n    \"changelogEntryId\" TEXT,\n    \"ipHash\"           TEXT         NOT NULL,\n    \"country\"          TEXT,\n    \"userAgent\"        TEXT,\n    \"referrer\"         TEXT,\n    \"viewedAt\"         TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"sessionHash\"      TEXT         NOT NULL,\n\n    CONSTRAINT \"PublicChangelogAnalytics_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE INDEX \"PublicChangelogAnalytics_projectId_idx\" ON \"PublicChangelogAnalytics\" (\"projectId\");\n\n-- CreateIndex\nCREATE INDEX \"PublicChangelogAnalytics_changelogEntryId_idx\" ON \"PublicChangelogAnalytics\" (\"changelogEntryId\");\n\n-- CreateIndex\nCREATE INDEX \"PublicChangelogAnalytics_viewedAt_idx\" ON \"PublicChangelogAnalytics\" (\"viewedAt\");\n\n-- CreateIndex\nCREATE INDEX \"PublicChangelogAnalytics_country_idx\" ON \"PublicChangelogAnalytics\" (\"country\");\n\n-- CreateIndex\nCREATE INDEX \"PublicChangelogAnalytics_sessionHash_idx\" ON \"PublicChangelogAnalytics\" (\"sessionHash\");\n\n-- AddForeignKey\nALTER TABLE \"PublicChangelogAnalytics\"\n    ADD CONSTRAINT \"PublicChangelogAnalytics_projectId_fkey\" FOREIGN KEY (\"projectId\") REFERENCES \"Project\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"PublicChangelogAnalytics\"\n    ADD CONSTRAINT \"PublicChangelogAnalytics_changelogEntryId_fkey\" FOREIGN KEY (\"changelogEntryId\") REFERENCES \"ChangelogEntry\" (\"id\") ON DELETE SET NULL ON UPDATE CASCADE;"
  },
  {
    "path": "prisma/migrations/20250624024525_add_custom_domains/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"AuditLog\" ALTER COLUMN \"userId\" DROP NOT NULL;\n\n-- CreateTable\nCREATE TABLE \"custom_domains\" (\n    \"id\" TEXT NOT NULL,\n    \"domain\" TEXT NOT NULL,\n    \"projectId\" TEXT NOT NULL,\n    \"verificationToken\" TEXT NOT NULL,\n    \"verified\" BOOLEAN NOT NULL DEFAULT false,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"verifiedAt\" TIMESTAMP(3),\n    \"userId\" TEXT,\n\n    CONSTRAINT \"custom_domains_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"custom_domains_domain_key\" ON \"custom_domains\"(\"domain\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"custom_domains_verificationToken_key\" ON \"custom_domains\"(\"verificationToken\");\n\n-- CreateIndex\nCREATE INDEX \"custom_domains_domain_idx\" ON \"custom_domains\"(\"domain\");\n\n-- CreateIndex\nCREATE INDEX \"custom_domains_projectId_idx\" ON \"custom_domains\"(\"projectId\");\n\n-- CreateIndex\nCREATE INDEX \"custom_domains_userId_idx\" ON \"custom_domains\"(\"userId\");\n\n-- CreateIndex\nCREATE INDEX \"custom_domains_verified_idx\" ON \"custom_domains\"(\"verified\");\n\n-- AddForeignKey\nALTER TABLE \"custom_domains\" ADD CONSTRAINT \"custom_domains_projectId_fkey\" FOREIGN KEY (\"projectId\") REFERENCES \"Project\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations/20250625030954_add_custom_domains_to_email_notifications/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"ProjectSubscription\" ADD COLUMN customDomain VARCHAR(255);\n\n-- CreateIndex\nCREATE INDEX idx_project_subscriptions_custom_domain ON \"ProjectSubscription\"(customDomain);\n"
  },
  {
    "path": "prisma/migrations/20250625120000_add_changelog_scheduling/migration.sql",
    "content": "-- Add scheduledAt field to existing ChangelogEntry table\nALTER TABLE \"ChangelogEntry\" ADD COLUMN \"scheduledAt\" TIMESTAMP(3);\n\n-- Add indexes for efficient querying of scheduled entries\nCREATE INDEX \"ChangelogEntry_scheduledAt_idx\" ON \"ChangelogEntry\"(\"scheduledAt\");\nCREATE INDEX \"ChangelogEntry_scheduled_published_idx\" ON \"ChangelogEntry\"(\"scheduledAt\", \"publishedAt\");\n\n-- Create ScheduledJobType enum\nCREATE TYPE \"ScheduledJobType\" AS ENUM ('PUBLISH_CHANGELOG_ENTRY', 'UNPUBLISH_CHANGELOG_ENTRY', 'DELETE_CHANGELOG_ENTRY', 'SEND_EMAIL_NOTIFICATION');\n\n-- Create JobStatus enum\nCREATE TYPE \"JobStatus\" AS ENUM ('PENDING', 'RUNNING', 'COMPLETED', 'FAILED', 'CANCELLED');\n\n-- Create ScheduledJob table for background job management\nCREATE TABLE \"ScheduledJob\" (\n                                \"id\" TEXT NOT NULL,\n                                \"type\" \"ScheduledJobType\" NOT NULL,\n                                \"entityId\" TEXT NOT NULL,\n                                \"scheduledAt\" TIMESTAMP(3) NOT NULL,\n                                \"executedAt\" TIMESTAMP(3),\n                                \"status\" \"JobStatus\" NOT NULL DEFAULT 'PENDING',\n                                \"errorMessage\" TEXT,\n                                \"retryCount\" INTEGER NOT NULL DEFAULT 0,\n                                \"maxRetries\" INTEGER NOT NULL DEFAULT 3,\n                                \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n                                \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n                                CONSTRAINT \"ScheduledJob_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- Create indexes for ScheduledJob table\nCREATE INDEX \"ScheduledJob_type_idx\" ON \"ScheduledJob\"(\"type\");\nCREATE INDEX \"ScheduledJob_entityId_idx\" ON \"ScheduledJob\"(\"entityId\");\nCREATE INDEX \"ScheduledJob_scheduledAt_idx\" ON \"ScheduledJob\"(\"scheduledAt\");\nCREATE INDEX \"ScheduledJob_status_idx\" ON \"ScheduledJob\"(\"status\");\nCREATE INDEX \"ScheduledJob_scheduled_pending_idx\" ON \"ScheduledJob\"(\"scheduledAt\", \"status\");\n\n-- Add foreign key constraint from ScheduledJob to ChangelogEntry\n-- Note: This is optional since we use entityId as a generic reference\n-- ALTER TABLE \"ScheduledJob\" ADD CONSTRAINT \"ScheduledJob_entityId_fkey\" FOREIGN KEY (\"entityId\") REFERENCES \"ChangelogEntry\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;"
  },
  {
    "path": "prisma/migrations/20250627020135_add_color_field_to_changelog_tag/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the column `customdomain` on the `ProjectSubscription` table. All the data in the column will be lost.\n\n*/\n-- AlterEnum\nALTER TYPE \"RequestType\" ADD VALUE 'ALLOW_SCHEDULE';\n\n-- DropIndex\nDROP INDEX \"idx_project_subscriptions_custom_domain\";\n\n-- AlterTable\nALTER TABLE \"ChangelogTag\" ADD COLUMN     \"color\" TEXT;\n\n-- AlterTable\nALTER TABLE \"ProjectSubscription\" DROP COLUMN \"customdomain\",\nADD COLUMN     \"customDomain\" TEXT;\n\n-- CreateIndex\nCREATE INDEX \"ProjectSubscription_customDomain_idx\" ON \"ProjectSubscription\"(\"customDomain\");\n\n-- AddForeignKey\nALTER TABLE \"ScheduledJob\" ADD CONSTRAINT \"ScheduledJob_entityId_fkey\" FOREIGN KEY (\"entityId\") REFERENCES \"ChangelogEntry\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- RenameIndex\nALTER INDEX \"ChangelogEntry_scheduled_published_idx\" RENAME TO \"ChangelogEntry_scheduledAt_publishedAt_idx\";\n\n-- RenameIndex\nALTER INDEX \"ScheduledJob_scheduled_pending_idx\" RENAME TO \"ScheduledJob_scheduledAt_status_idx\";\n"
  },
  {
    "path": "prisma/migrations/20250628171029_add_telemetry/migration.sql",
    "content": "/*\n  Warnings:\n\n  - A unique constraint covering the columns `[telemetryInstanceId]` on the table `SystemConfig` will be added. If there are existing duplicate values, this will fail.\n\n*/\n-- CreateEnum\nCREATE TYPE \"TelemetryState\" AS ENUM ('PROMPT', 'ENABLED', 'DISABLED');\n\n-- AlterEnum\nALTER TYPE \"ScheduledJobType\" ADD VALUE 'TELEMETRY_SEND';\n\n-- AlterTable\nALTER TABLE \"SystemConfig\" ADD COLUMN     \"allowTelemetry\" \"TelemetryState\" NOT NULL DEFAULT 'PROMPT',\nADD COLUMN     \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\nADD COLUMN     \"telemetryInstanceId\" TEXT;\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"SystemConfig_telemetryInstanceId_key\" ON \"SystemConfig\"(\"telemetryInstanceId\");\n"
  },
  {
    "path": "prisma/migrations/20250628175000_preemptive_telemetry_setup/migration.sql",
    "content": "-- This migration runs BEFORE 20250628180800_add_telemetry_support to prevent failures\n-- It makes all the changes that the problematic migration attempts, but with proper conditional logic\n\nDO $$\n    BEGIN\n        -- 1. Add TELEMETRY_SEND to ScheduledJobType enum if it doesn't exist\n        -- This prevents the ALTER TYPE ADD VALUE from failing if it already exists\n        IF NOT EXISTS (\n            SELECT 1 FROM pg_enum e\n                              JOIN pg_type t ON e.enumtypid = t.oid\n            WHERE t.typname = 'ScheduledJobType' AND e.enumlabel = 'TELEMETRY_SEND'\n        ) THEN\n            ALTER TYPE \"ScheduledJobType\" ADD VALUE 'TELEMETRY_SEND';\n        END IF;\n\n        -- 2. Create TelemetryState enum if it doesn't exist\n        -- This prevents the CREATE TYPE from failing if it already exists\n        IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'TelemetryState') THEN\n            CREATE TYPE \"TelemetryState\" AS ENUM ('PROMPT', 'ENABLED', 'DISABLED');\n        END IF;\n\n        -- 3. Add telemetry columns to SystemConfig if they don't exist\n        -- This prevents the ALTER TABLE ADD COLUMN from failing if columns exist\n        IF NOT EXISTS (\n            SELECT 1 FROM information_schema.columns\n            WHERE table_name = 'SystemConfig' AND column_name = 'allowTelemetry'\n        ) THEN\n            ALTER TABLE \"SystemConfig\" ADD COLUMN \"allowTelemetry\" \"TelemetryState\" NOT NULL DEFAULT 'PROMPT';\n        END IF;\n\n        IF NOT EXISTS (\n            SELECT 1 FROM information_schema.columns\n            WHERE table_name = 'SystemConfig' AND column_name = 'telemetryInstanceId'\n        ) THEN\n            ALTER TABLE \"SystemConfig\" ADD COLUMN \"telemetryInstanceId\" TEXT;\n        END IF;\n\n        -- 4. Create unique index if it doesn't exist\n        -- This prevents the CREATE UNIQUE INDEX from failing if it already exists\n        IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'SystemConfig_telemetryInstanceId_key') THEN\n            CREATE UNIQUE INDEX \"SystemConfig_telemetryInstanceId_key\" ON \"SystemConfig\"(\"telemetryInstanceId\");\n        END IF;\n\n        -- 5. Remove foreign key constraint if it exists\n        -- This prevents issues with the DROP CONSTRAINT IF EXISTS\n        IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'ScheduledJob_entityId_fkey') THEN\n            ALTER TABLE \"ScheduledJob\" DROP CONSTRAINT \"ScheduledJob_entityId_fkey\";\n        END IF;\n\n        -- 6. Clean up any problematic columns that might exist\n        IF EXISTS (\n            SELECT 1 FROM information_schema.columns\n            WHERE table_name = 'ScheduledJob' AND column_name = 'changelogEntryId'\n        ) THEN\n            ALTER TABLE \"ScheduledJob\" DROP COLUMN \"changelogEntryId\";\n        END IF;\n    END $$;"
  },
  {
    "path": "prisma/migrations/20250628180800_add_telemetry_support/migration.sql",
    "content": "DO $$\n    BEGIN\n        -- Add TELEMETRY_SEND to ScheduledJobType enum (with conditional check)\n        IF NOT EXISTS (\n            SELECT 1 FROM pg_enum e\n                              JOIN pg_type t ON e.enumtypid = t.oid\n            WHERE t.typname = 'ScheduledJobType' AND e.enumlabel = 'TELEMETRY_SEND'\n        ) THEN\n            ALTER TYPE \"ScheduledJobType\" ADD VALUE 'TELEMETRY_SEND';\n        END IF;\n    END $$;\n\n-- Add TelemetryState enum (with conditional check)\nDO $$\n    BEGIN\n        IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'TelemetryState') THEN\n            CREATE TYPE \"TelemetryState\" AS ENUM ('PROMPT', 'ENABLED', 'DISABLED');\n        END IF;\n    END $$;\n\n-- Add telemetry fields to SystemConfig table (with conditional checks)\nDO $$\n    BEGIN\n        IF NOT EXISTS (\n            SELECT 1 FROM information_schema.columns\n            WHERE table_name = 'SystemConfig' AND column_name = 'allowTelemetry'\n        ) THEN\n            ALTER TABLE \"SystemConfig\" ADD COLUMN \"allowTelemetry\" \"TelemetryState\" NOT NULL DEFAULT 'PROMPT';\n        END IF;\n\n        IF NOT EXISTS (\n            SELECT 1 FROM information_schema.columns\n            WHERE table_name = 'SystemConfig' AND column_name = 'telemetryInstanceId'\n        ) THEN\n            ALTER TABLE \"SystemConfig\" ADD COLUMN \"telemetryInstanceId\" TEXT;\n        END IF;\n    END $$;\n\n-- Create unique constraint on telemetryInstanceId (with conditional check)\nDO $$\n    BEGIN\n        IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'SystemConfig_telemetryInstanceId_key') THEN\n            CREATE UNIQUE INDEX \"SystemConfig_telemetryInstanceId_key\" ON \"SystemConfig\"(\"telemetryInstanceId\");\n        END IF;\n    END $$;\n\n-- Remove foreign key constraint from ScheduledJob to allow generic entityIds (already conditional)\nALTER TABLE \"ScheduledJob\" DROP CONSTRAINT IF EXISTS \"ScheduledJob_entityId_fkey\";"
  },
  {
    "path": "prisma/migrations/20250628215000_complete_telemetry_fix/migration.sql",
    "content": "-- Migration: Complete telemetry fix - removes old relation and adds telemetry support\n-- 1. Add TELEMETRY_SEND to ScheduledJobType enum if it doesn't exist\nDO $$\nBEGIN\n    IF NOT EXISTS (\n        SELECT 1 FROM pg_enum e\n        JOIN pg_type t ON e.enumtypid = t.oid\n        WHERE t.typname = 'ScheduledJobType' AND e.enumlabel = 'TELEMETRY_SEND'\n    ) THEN\nALTER TYPE \"ScheduledJobType\" ADD VALUE 'TELEMETRY_SEND';\nEND IF;\nEND $$;\n\n-- 2. Create TelemetryState enum if it doesn't exist\nDO $$\nBEGIN\n    IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'TelemetryState') THEN\nCREATE TYPE \"TelemetryState\" AS ENUM ('PROMPT', 'ENABLED', 'DISABLED');\nEND IF;\nEND $$;\n\n-- 3. Add telemetry columns to SystemConfig if they don't exist\nDO $$\nBEGIN\n    IF NOT EXISTS (\n        SELECT 1 FROM information_schema.columns\n        WHERE table_name = 'SystemConfig' AND column_name = 'allowTelemetry'\n    ) THEN\nALTER TABLE \"SystemConfig\" ADD COLUMN \"allowTelemetry\" \"TelemetryState\" NOT NULL DEFAULT 'PROMPT';\nEND IF;\n\nIF NOT EXISTS (\n        SELECT 1 FROM information_schema.columns\n        WHERE table_name = 'SystemConfig' AND column_name = 'telemetryInstanceId'\n    ) THEN\nALTER TABLE \"SystemConfig\" ADD COLUMN \"telemetryInstanceId\" TEXT;\nEND IF;\nEND $$;\n\n-- 4. Create unique index on telemetryInstanceId if it doesn't exist\nDO $$\nBEGIN\n    IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'SystemConfig_telemetryInstanceId_key') THEN\nCREATE UNIQUE INDEX \"SystemConfig_telemetryInstanceId_key\" ON \"SystemConfig\"(\"telemetryInstanceId\");\nEND IF;\nEND $$;\n\n-- 5. Remove foreign key constraint from ScheduledJob if it exists\nDO $$\nBEGIN\n    IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'ScheduledJob_entityId_fkey') THEN\nALTER TABLE \"ScheduledJob\" DROP CONSTRAINT \"ScheduledJob_entityId_fkey\";\nEND IF;\nEND $$;\n\n-- 6. Remove any remaining changelogEntryId column from ScheduledJob if it exists\nDO $$\nBEGIN\n    IF EXISTS (\n        SELECT 1 FROM information_schema.columns\n        WHERE table_name = 'ScheduledJob' AND column_name = 'changelogEntryId'\n    ) THEN\nALTER TABLE \"ScheduledJob\" DROP COLUMN \"changelogEntryId\";\nEND IF;\nEND $$;"
  },
  {
    "path": "prisma/migrations/20250628215100_fix_telemetry_casting/migration.sql",
    "content": "-- Migration: Fix telemetry enum casting issues\n-- 1. Add TELEMETRY_SEND to ScheduledJobType enum if it doesn't exist\nDO $$\nBEGIN\n    IF NOT EXISTS (\n        SELECT 1 FROM pg_enum e\n        JOIN pg_type t ON e.enumtypid = t.oid\n        WHERE t.typname = 'ScheduledJobType' AND e.enumlabel = 'TELEMETRY_SEND'\n    ) THEN\nALTER TYPE \"ScheduledJobType\" ADD VALUE 'TELEMETRY_SEND';\nEND IF;\nEND $$;\n\n-- 2. Create TelemetryState enum if it doesn't exist\nDO $$\nBEGIN\n    IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'TelemetryState') THEN\nCREATE TYPE \"TelemetryState\" AS ENUM ('PROMPT', 'ENABLED', 'DISABLED');\nEND IF;\nEND $$;\n\n-- 3. Remove foreign key constraint from ScheduledJob if it exists\nDO $$\nBEGIN\n    IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'ScheduledJob_entityId_fkey') THEN\nALTER TABLE \"ScheduledJob\" DROP CONSTRAINT \"ScheduledJob_entityId_fkey\";\nEND IF;\nEND $$;\n\n-- 4. Remove any remaining changelogEntryId column from ScheduledJob if it exists\nDO $$\nBEGIN\n    IF EXISTS (\n        SELECT 1 FROM information_schema.columns\n        WHERE table_name = 'ScheduledJob' AND column_name = 'changelogEntryId'\n    ) THEN\nALTER TABLE \"ScheduledJob\" DROP COLUMN \"changelogEntryId\";\nEND IF;\nEND $$;\n\n-- 5. Add telemetry columns to SystemConfig with proper enum casting\nDO $$\nBEGIN\n    -- Add allowTelemetry column if it doesn't exist\n    IF NOT EXISTS (\n        SELECT 1 FROM information_schema.columns\n        WHERE table_name = 'SystemConfig' AND column_name = 'allowTelemetry'\n    ) THEN\nALTER TABLE \"SystemConfig\" ADD COLUMN \"allowTelemetry\" \"TelemetryState\" NOT NULL DEFAULT 'PROMPT'::\"TelemetryState\";\nEND IF;\n\n    -- Add telemetryInstanceId column if it doesn't exist\nIF NOT EXISTS (\n        SELECT 1 FROM information_schema.columns\n        WHERE table_name = 'SystemConfig' AND column_name = 'telemetryInstanceId'\n    ) THEN\nALTER TABLE \"SystemConfig\" ADD COLUMN \"telemetryInstanceId\" TEXT;\nEND IF;\nEND $$;\n\n-- 6. Create unique index on telemetryInstanceId if it doesn't exist\nDO $$\nBEGIN\n    IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'SystemConfig_telemetryInstanceId_key') THEN\nCREATE UNIQUE INDEX \"SystemConfig_telemetryInstanceId_key\" ON \"SystemConfig\"(\"telemetryInstanceId\");\nEND IF;\nEND $$;\n\n-- 7. Ensure existing SystemConfig records have proper telemetry values\nDO $$\nBEGIN\n    -- Update any existing records that might have null or invalid telemetry states\n    IF EXISTS (SELECT 1 FROM \"SystemConfig\" WHERE \"allowTelemetry\" IS NULL) THEN\nUPDATE \"SystemConfig\" SET \"allowTelemetry\" = 'PROMPT'::\"TelemetryState\" WHERE \"allowTelemetry\" IS NULL;\nEND IF;\nEND $$;"
  },
  {
    "path": "prisma/migrations/20250628230000_add_changelogentryid_back/migration.sql",
    "content": "-- Migration: Add changelogEntryId column back to ScheduledJob without forced relations\n\n-- Add changelogEntryId column back to ScheduledJob table if it doesn't exist\nDO $$\nBEGIN\n    IF NOT EXISTS (\n        SELECT 1 FROM information_schema.columns\n        WHERE table_name = 'ScheduledJob' AND column_name = 'changelogEntryId'\n    ) THEN\nALTER TABLE \"ScheduledJob\" ADD COLUMN \"changelogEntryId\" TEXT;\nEND IF;\nEND $$;\n\n-- Create index on changelogEntryId for better query performance\nDO $$\nBEGIN\n    IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'ScheduledJob_changelogEntryId_idx') THEN\nCREATE INDEX \"ScheduledJob_changelogEntryId_idx\" ON \"ScheduledJob\"(\"changelogEntryId\");\nEND IF;\nEND $$;\n\n-- Note: We intentionally DO NOT add a foreign key constraint to keep this as a flexible reference"
  },
  {
    "path": "prisma/migrations/20250702092443_add_cli_auth_codes/migration.sql",
    "content": "-- Create CliAuthCode table\nCREATE TABLE \"CliAuthCode\" (\n                               \"id\" TEXT NOT NULL,\n                               \"code\" TEXT NOT NULL,\n                               \"userId\" TEXT NOT NULL,\n                               \"callbackUrl\" TEXT NOT NULL,\n                               \"expiresAt\" TIMESTAMP(3) NOT NULL,\n                               \"usedAt\" TIMESTAMP(3),\n                               \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n                               CONSTRAINT \"CliAuthCode_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- Create indexes for CliAuthCode\nCREATE UNIQUE INDEX \"CliAuthCode_code_key\" ON \"CliAuthCode\"(\"code\");\nCREATE INDEX \"CliAuthCode_code_idx\" ON \"CliAuthCode\"(\"code\");\nCREATE INDEX \"CliAuthCode_userId_idx\" ON \"CliAuthCode\"(\"userId\");\nCREATE INDEX \"CliAuthCode_expiresAt_idx\" ON \"CliAuthCode\"(\"expiresAt\");\n\n-- Add foreign key constraint for CliAuthCode\nALTER TABLE \"CliAuthCode\" ADD CONSTRAINT \"CliAuthCode_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- The changelogEntryId column should exist from migration 20250628230000\n-- But let's add the foreign key constraint that was removed in the telemetry fixes\n-- Only add it if both the column exists AND the constraint doesn't exist\nDO $$\n    BEGIN\n        IF EXISTS (\n            SELECT 1 FROM information_schema.columns\n            WHERE table_name = 'ScheduledJob' AND column_name = 'changelogEntryId'\n        ) AND NOT EXISTS (\n            SELECT 1 FROM pg_constraint\n            WHERE conname = 'ScheduledJob_changelogEntryId_fkey'\n        ) THEN\n            ALTER TABLE \"ScheduledJob\"\n                ADD CONSTRAINT \"ScheduledJob_changelogEntryId_fkey\"\n                    FOREIGN KEY (\"changelogEntryId\") REFERENCES \"ChangelogEntry\"(\"id\")\n                        ON DELETE SET NULL ON UPDATE CASCADE;\n        END IF;\n    END $$;"
  },
  {
    "path": "prisma/migrations/20250702121802_add_synced_cli_migrations_and_metadata/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"ProjectSyncMetadata\" (\n    \"id\" TEXT NOT NULL,\n    \"projectId\" TEXT NOT NULL,\n    \"lastSyncHash\" TEXT,\n    \"lastSyncedAt\" TIMESTAMP(3),\n    \"totalCommitsSynced\" INTEGER NOT NULL DEFAULT 0,\n    \"repositoryUrl\" TEXT,\n    \"branch\" TEXT NOT NULL DEFAULT 'main',\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"ProjectSyncMetadata_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"SyncedCommit\" (\n    \"id\" TEXT NOT NULL,\n    \"projectId\" TEXT NOT NULL,\n    \"commitHash\" TEXT NOT NULL,\n    \"commitMessage\" TEXT NOT NULL,\n    \"commitAuthor\" TEXT NOT NULL,\n    \"commitEmail\" TEXT NOT NULL,\n    \"commitDate\" TIMESTAMP(3) NOT NULL,\n    \"commitFiles\" TEXT[],\n    \"conventionalType\" TEXT,\n    \"conventionalScope\" TEXT,\n    \"isBreaking\" BOOLEAN NOT NULL DEFAULT false,\n    \"commitBody\" TEXT,\n    \"commitFooter\" TEXT,\n    \"syncedAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"branch\" TEXT NOT NULL DEFAULT 'main',\n\n    CONSTRAINT \"SyncedCommit_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"ProjectSyncMetadata_projectId_key\" ON \"ProjectSyncMetadata\"(\"projectId\");\n\n-- CreateIndex\nCREATE INDEX \"ProjectSyncMetadata_projectId_idx\" ON \"ProjectSyncMetadata\"(\"projectId\");\n\n-- CreateIndex\nCREATE INDEX \"ProjectSyncMetadata_lastSyncedAt_idx\" ON \"ProjectSyncMetadata\"(\"lastSyncedAt\");\n\n-- CreateIndex\nCREATE INDEX \"SyncedCommit_projectId_idx\" ON \"SyncedCommit\"(\"projectId\");\n\n-- CreateIndex\nCREATE INDEX \"SyncedCommit_commitHash_idx\" ON \"SyncedCommit\"(\"commitHash\");\n\n-- CreateIndex\nCREATE INDEX \"SyncedCommit_syncedAt_idx\" ON \"SyncedCommit\"(\"syncedAt\");\n\n-- CreateIndex\nCREATE INDEX \"SyncedCommit_conventionalType_idx\" ON \"SyncedCommit\"(\"conventionalType\");\n\n-- CreateIndex\nCREATE INDEX \"SyncedCommit_branch_idx\" ON \"SyncedCommit\"(\"branch\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"SyncedCommit_projectId_commitHash_key\" ON \"SyncedCommit\"(\"projectId\", \"commitHash\");\n\n-- AddForeignKey\nALTER TABLE \"ProjectSyncMetadata\" ADD CONSTRAINT \"ProjectSyncMetadata_projectId_fkey\" FOREIGN KEY (\"projectId\") REFERENCES \"Project\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"SyncedCommit\" ADD CONSTRAINT \"SyncedCommit_projectId_fkey\" FOREIGN KEY (\"projectId\") REFERENCES \"Project\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations/20250703092000_fix_missing_changelogentryid_column/migration.sql",
    "content": "-- Migration: 20250703092000_fix_missing_changelogentryid_column\n-- Adds the missing changelogEntryId column that should have been created by 20250628230000_add_changelogentryid_back\n\n-- Add the missing changelogEntryId column to ScheduledJob table if it doesn't exist\nALTER TABLE \"ScheduledJob\" ADD COLUMN IF NOT EXISTS \"changelogEntryId\" TEXT;\n\n-- Create index on changelogEntryId if it doesn't exist\nCREATE INDEX IF NOT EXISTS \"ScheduledJob_changelogEntryId_idx\" ON \"ScheduledJob\"(\"changelogEntryId\");\n\n-- Add foreign key constraint (ignore error if it already exists)\nDO $$\n    BEGIN\n        BEGIN\n            ALTER TABLE \"ScheduledJob\" ADD CONSTRAINT \"ScheduledJob_changelogEntryId_fkey\" FOREIGN KEY (\"changelogEntryId\") REFERENCES \"ChangelogEntry\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n        EXCEPTION\n            WHEN duplicate_object THEN NULL;\n        END;\n    END $$;"
  },
  {
    "path": "prisma/migrations/20251031_add_metadata_to_changelog_request/migration.sql",
    "content": "-- AddColumn metadata to ChangelogRequest\nALTER TABLE \"ChangelogRequest\" ADD COLUMN \"metadata\" JSONB;\n"
  },
  {
    "path": "prisma/migrations/20251111210147_add_api_key_and_widgets/migration.sql",
    "content": "-- DropIndex\nDROP INDEX IF EXISTS \"ScheduledJob_changelogEntryId_idx\";\n\n-- AlterTable\nALTER TABLE \"ApiKey\" ADD COLUMN     \"isGlobal\" BOOLEAN NOT NULL DEFAULT false,\nADD COLUMN     \"projectId\" TEXT;\n\n-- AlterTable\nALTER TABLE \"ChangelogEntry\" ADD COLUMN     \"excerpt\" TEXT;\n\n-- AlterTable\nALTER TABLE \"SystemConfig\" ADD COLUMN     \"adminOnlyApiKeyCreation\" BOOLEAN NOT NULL DEFAULT false;\n\n-- CreateTable\nCREATE TABLE \"Widget\" (\n    \"id\" TEXT NOT NULL,\n    \"projectId\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"variant\" TEXT NOT NULL,\n    \"settings\" JSONB NOT NULL DEFAULT '{}',\n    \"customCSS\" TEXT,\n    \"isActive\" BOOLEAN NOT NULL DEFAULT true,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"Widget_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE INDEX \"Widget_projectId_idx\" ON \"Widget\"(\"projectId\");\n\n-- CreateIndex\nCREATE INDEX \"Widget_projectId_isActive_idx\" ON \"Widget\"(\"projectId\", \"isActive\");\n\n-- CreateIndex\nCREATE INDEX \"ApiKey_projectId_idx\" ON \"ApiKey\"(\"projectId\");\n\n-- AddForeignKey\nALTER TABLE \"Widget\" ADD CONSTRAINT \"Widget_projectId_fkey\" FOREIGN KEY (\"projectId\") REFERENCES \"Project\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"ApiKey\" ADD CONSTRAINT \"ApiKey_projectId_fkey\" FOREIGN KEY (\"projectId\") REFERENCES \"Project\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations/20251127011835_add_slack_integration/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"SlackIntegration\" (\n    \"id\" TEXT NOT NULL,\n    \"projectId\" TEXT NOT NULL,\n    \"accessToken\" TEXT NOT NULL,\n    \"refreshToken\" TEXT,\n    \"tokenExpiresAt\" TIMESTAMP(3),\n    \"teamId\" TEXT NOT NULL,\n    \"teamName\" TEXT,\n    \"botUserId\" TEXT NOT NULL,\n    \"botUsername\" TEXT,\n    \"channelId\" TEXT NOT NULL,\n    \"channelName\" TEXT,\n    \"autoSend\" BOOLEAN NOT NULL DEFAULT true,\n    \"enabled\" BOOLEAN NOT NULL DEFAULT true,\n    \"lastSyncAt\" TIMESTAMP(3),\n    \"lastErrorMessage\" TEXT,\n    \"postCount\" INTEGER NOT NULL DEFAULT 0,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"SlackIntegration_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"SlackIntegration_projectId_key\" ON \"SlackIntegration\"(\"projectId\");\n\n-- CreateIndex\nCREATE INDEX \"SlackIntegration_projectId_idx\" ON \"SlackIntegration\"(\"projectId\");\n\n-- CreateIndex\nCREATE INDEX \"SlackIntegration_teamId_idx\" ON \"SlackIntegration\"(\"teamId\");\n\n-- AddForeignKey\nALTER TABLE \"SlackIntegration\" ADD CONSTRAINT \"SlackIntegration_projectId_fkey\" FOREIGN KEY (\"projectId\") REFERENCES \"Project\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations/20251127015242_add_slack_oauth_to_system_config/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"SystemConfig\" ADD COLUMN     \"slackOAuthClientId\" TEXT,\nADD COLUMN     \"slackOAuthClientSecret\" TEXT,\nADD COLUMN     \"slackOAuthEnabled\" BOOLEAN NOT NULL DEFAULT false;\n"
  },
  {
    "path": "prisma/migrations/20251127_add_slack_signing_secret/migration.sql",
    "content": "-- Add Slack signing secret to SystemConfig\n-- Used for verifying Slack API requests\n\nALTER TABLE \"SystemConfig\" ADD COLUMN \"slackSigningSecret\" TEXT;\n\n-- Add comment to document the field\nCOMMENT ON COLUMN \"SystemConfig\".\"slackSigningSecret\" IS 'Slack app signing secret (encrypted) - used to verify webhook requests from Slack API';"
  },
  {
    "path": "prisma/migrations/20260219214321_add_license_fields/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"SystemConfig\" ADD COLUMN     \"sponsorLastVerified\" TIMESTAMP(3),\nADD COLUMN     \"sponsorLicenseKey\" TEXT,\nADD COLUMN     \"sponsorLicenseValid\" BOOLEAN NOT NULL DEFAULT false,\nADD COLUMN     \"sponsorPayload\" TEXT,\nADD COLUMN     \"sponsorProof\" TEXT;\n"
  },
  {
    "path": "prisma/migrations/20260219215954_fix_changelog_request_cascade_delete/migration.sql",
    "content": "-- DropForeignKey\nALTER TABLE \"ChangelogRequest\" DROP CONSTRAINT \"ChangelogRequest_projectId_fkey\";\n\n-- AddForeignKey\nALTER TABLE \"ChangelogRequest\" ADD CONSTRAINT \"ChangelogRequest_projectId_fkey\" FOREIGN KEY (\"projectId\") REFERENCES \"Project\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations/20260220120000_add_timezone_config/migration.sql",
    "content": "-- AlterTable: Add timezone configuration to SystemConfig\nALTER TABLE \"SystemConfig\" ADD COLUMN \"timezone\" TEXT NOT NULL DEFAULT 'UTC';\nALTER TABLE \"SystemConfig\" ADD COLUMN \"allowUserTimezone\" BOOLEAN NOT NULL DEFAULT true;\n\n-- AlterTable: Add per-user timezone override to Settings\nALTER TABLE \"Settings\" ADD COLUMN \"timezone\" TEXT;\n"
  },
  {
    "path": "prisma/migrations/20260220140000_add_custom_date_templates/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"SystemConfig\" ADD COLUMN \"customDateTemplates\" JSONB;\n"
  },
  {
    "path": "prisma/migrations/migration_lock.toml",
    "content": "# Please do not edit this file manually\n# It should be added in your version-control system (e.g., Git)\nprovider = \"postgresql\"\n"
  },
  {
    "path": "prisma/schema/README.md",
    "content": "# Prisma Schema Organization\n\nThis directory contains the Prisma schema files organized by domain using Prisma's multi-file schema feature (GA in 6.7.0).\n\n## File Structure\n\n### `base.prisma`\nContains the fundamental configuration:\n- Database datasource configuration\n- Prisma Client generator configuration\n- Output path specification\n\n### `enums.prisma`\nAll enum type definitions:\n- `Role` - User roles (ADMIN, STAFF, VIEWER)\n- `TwoFactorMode` - Two-factor authentication modes\n- `TelemetryState` - Telemetry consent states\n- `ScheduledJobType` - Types of scheduled background jobs\n- `JobStatus` - Status of scheduled jobs\n- `RequestType` - Types of changelog requests\n- `RequestStatus` - Status of requests\n- `emailType` - Email notification types\n- `SubscriptionType` - Subscription preferences\n\n### `users.prisma`\nUser authentication and authorization:\n- `User` - Core user model\n- `RefreshToken` - JWT refresh token management\n- `OAuthProvider` - OAuth provider configuration\n- `OAuthConnection` - User OAuth connections\n- `Settings` - User preferences and settings\n- `Passkey` - WebAuthn passkey authentication\n- `PasswordReset` - Password reset tokens\n- `TwoFactorSession` - Two-factor authentication sessions\n- `InvitationLink` - User invitation system\n- `CliAuthCode` - CLI authentication codes\n\n### `projects.prisma`\nProject and changelog management:\n- `Project` - Core project model\n- `Changelog` - Project changelog container\n- `ChangelogEntry` - Individual changelog entries\n- `ChangelogTag` - Tags for categorizing entries\n- `ChangelogRequest` - Approval workflow for changes\n- `Widget` - Embeddable changelog widgets\n- `CustomDomain` - Custom domain configuration\n\n### `integrations.prisma`\nThird-party service integrations:\n- `EmailConfig` - SMTP email configuration\n- `EmailLog` - Email send history\n- `EmailSubscriber` - Email subscription management\n- `ProjectSubscription` - Project-specific subscriptions\n- `SlackIntegration` - Slack workspace integration\n- `GitHubIntegration` - GitHub repository integration\n- `ProjectSyncMetadata` - Git sync metadata\n- `SyncedCommit` - Synced commit history\n\n### `system.prisma`\nSystem-level configuration and management:\n- `ApiKey` - API key management (global and project-scoped)\n- `AuditLog` - System audit trail\n- `SystemConfig` - Global system configuration\n- `PublicChangelogAnalytics` - Public changelog view analytics\n- `ScheduledJob` - Background job scheduling\n\n## Adding New Models\n\nWhen adding new models:\n\n1. Determine the appropriate domain file\n2. If creating a new domain, create a new `.prisma` file\n3. Ensure all references to other models are valid\n4. Run `npx prisma format` to validate syntax\n5. Run `npx prisma generate` to update the client\n6. Create and apply migrations as needed\n\n## Benefits of This Structure\n\n- **Improved Maintainability**: Each file focuses on a specific domain\n- **Better Collaboration**: Reduces merge conflicts when multiple developers work on schema\n- **Easier Navigation**: Find models quickly based on domain\n- **Clear Organization**: Domain boundaries are explicit\n- **Scalability**: Easy to add new domains as the application grows\n\n## Schema Validation\n\nTo validate the entire schema:\n```bash\nnpx prisma format\nnpx prisma validate\n```\n\n## Generating Client\n\nThe Prisma Client is automatically generated from all schema files:\n```bash\nnpx prisma generate\n```\n\n## Migrations\n\nMigrations work the same way with multi-file schemas:\n```bash\nnpx prisma migrate dev --name description_of_change\nnpx prisma migrate deploy\n```\n"
  },
  {
    "path": "prisma/schema/base.prisma",
    "content": "// Base configuration for the database and client generator\n// This file contains datasource and generator definitions\n\ndatasource db {\n  provider = \"postgresql\"\n  url      = env(\"DATABASE_URL\")\n}\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  output          = \"../../node_modules/.prisma/client\"\n  previewFeatures = [\"fullTextSearchPostgres\"]\n}\n"
  },
  {
    "path": "prisma/schema/enums.prisma",
    "content": "// All enum definitions for the application\n// Enums are used throughout the schema for type-safe values\n\nenum Role {\n  ADMIN\n  STAFF\n  VIEWER\n}\n\nenum RequestType {\n  DELETE_ENTRY\n  DELETE_TAG\n  DELETE_PROJECT\n  ALLOW_PUBLISH\n  ALLOW_SCHEDULE\n}\n\nenum RequestStatus {\n  PENDING\n  APPROVED\n  REJECTED\n}\n\nenum emailType {\n  SINGLE_UPDATE\n  DIGEST\n}\n\nenum SubscriptionType {\n  ALL_UPDATES // Receive all updates\n  MAJOR_ONLY // Only receive major version updates\n  DIGEST_ONLY // Only receive digest emails\n}\n\nenum TwoFactorMode {\n  NONE // No additional security\n  PASSKEY_PLUS_PASSWORD // Passkey login requires password\n  PASSWORD_PLUS_PASSKEY // Password login requires passkey\n}\n\nenum ScheduledJobType {\n  PUBLISH_CHANGELOG_ENTRY\n  UNPUBLISH_CHANGELOG_ENTRY\n  DELETE_CHANGELOG_ENTRY\n  SEND_EMAIL_NOTIFICATION\n  TELEMETRY_SEND\n  RENEW_SSL_CERTIFICATE\n}\n\nenum JobStatus {\n  PENDING\n  RUNNING\n  COMPLETED\n  FAILED\n  CANCELLED\n}\n\nenum TelemetryState {\n  PROMPT\n  ENABLED\n  DISABLED\n}\n\nenum CertificateStatus {\n  PENDING_HTTP01\n  PENDING_DNS01\n  ISSUED\n  EXPIRED\n  FAILED\n  REVOKED\n}\n\nenum ChallengeType {\n  HTTP01\n  DNS01\n}\n\nenum SslMode {\n  LETS_ENCRYPT\n  EXTERNAL\n  NONE\n}\n\nenum BrowserRuleType {\n  BLOCK\n  ALLOW\n}\n"
  },
  {
    "path": "prisma/schema/integrations.prisma",
    "content": "// Third-party integrations\n// Includes email, Slack, GitHub, and other external service integrations\n\nmodel EmailConfig {\n  id             String    @id @default(cuid())\n  projectId      String    @unique\n  project        Project   @relation(fields: [projectId], references: [id], onDelete: Cascade)\n  enabled        Boolean   @default(false)\n  smtpHost       String\n  smtpPort       Int\n  smtpUser       String?\n  smtpPassword   String?\n  smtpSecure     Boolean   @default(true)\n  fromEmail      String\n  fromName       String?\n  replyToEmail   String?\n  defaultSubject String?\n  createdAt      DateTime  @default(now())\n  updatedAt      DateTime  @updatedAt\n  lastTestedAt   DateTime?\n  testStatus     String? // \"success\", \"failed\", error message\n}\n\nmodel EmailLog {\n  id         String   @id @default(cuid())\n  projectId  String\n  project    Project  @relation(fields: [projectId], references: [id], onDelete: Cascade)\n  recipients String[]\n  subject    String\n  messageId  String?\n  type       String // \"SINGLE_UPDATE\" or \"DIGEST\"\n  entryIds   String[]\n  createdAt  DateTime @default(now())\n}\n\nmodel EmailSubscriber {\n  id               String                @id @default(cuid())\n  email            String\n  name             String?\n  isActive         Boolean               @default(true)\n  unsubscribeToken String                @unique\n  createdAt        DateTime              @default(now())\n  updatedAt        DateTime              @updatedAt\n  lastEmailSentAt  DateTime?\n  subscriptions    ProjectSubscription[]\n\n  @@unique([email])\n  @@index([email])\n  @@index([unsubscribeToken])\n}\n\nmodel ProjectSubscription {\n  id               String           @id @default(cuid())\n  projectId        String\n  customDomain     String? // domain subscribed from ( optional )\n  project          Project          @relation(fields: [projectId], references: [id], onDelete: Cascade)\n  subscriberId     String\n  subscriber       EmailSubscriber  @relation(fields: [subscriberId], references: [id], onDelete: Cascade)\n  subscriptionType SubscriptionType @default(ALL_UPDATES)\n  createdAt        DateTime         @default(now())\n  updatedAt        DateTime         @updatedAt\n\n  @@unique([projectId, subscriberId])\n  @@index([projectId])\n  @@index([customDomain])\n  @@index([subscriberId])\n}\n\nmodel SlackIntegration {\n  id               String    @id @default(cuid())\n  projectId        String    @unique\n  project          Project   @relation(fields: [projectId], references: [id], onDelete: Cascade)\n  // OAuth credentials\n  accessToken      String // Slack bot token (encrypted)\n  refreshToken     String? // Slack app refresh token if using refresh tokens\n  tokenExpiresAt   DateTime?\n  // Workspace info\n  teamId           String // Slack workspace ID\n  teamName         String? // Slack workspace name\n  botUserId        String // Slack bot user ID\n  botUsername      String? // Slack bot username\n  // Configuration\n  channelId        String // Default channel to post updates to\n  channelName      String? // Channel name for display\n  autoSend         Boolean   @default(true) // Auto-post on publish\n  enabled          Boolean   @default(true)\n  // Logging\n  lastSyncAt       DateTime?\n  lastErrorMessage String?\n  postCount        Int       @default(0)\n  createdAt        DateTime  @default(now())\n  updatedAt        DateTime  @updatedAt\n\n  @@index([projectId])\n  @@index([teamId])\n}\n\nmodel GitHubIntegration {\n  id                     String    @id @default(cuid())\n  projectId              String    @unique\n  project                Project   @relation(fields: [projectId], references: [id], onDelete: Cascade)\n  repositoryUrl          String\n  accessToken            String // Encrypted access token\n  defaultBranch          String    @default(\"main\")\n  lastSyncAt             DateTime?\n  lastCommitSha          String?\n  includeBreakingChanges Boolean   @default(true)\n  includeFixes           Boolean   @default(true)\n  includeFeatures        Boolean   @default(true)\n  includeChores          Boolean   @default(false)\n  customCommitTypes      String[]  @default([])\n  enabled                Boolean   @default(true)\n  createdAt              DateTime  @default(now())\n  updatedAt              DateTime  @updatedAt\n\n  @@index([projectId])\n  @@index([enabled])\n}\n\nmodel ProjectSyncMetadata {\n  id                 String    @id @default(cuid())\n  projectId          String    @unique\n  lastSyncHash       String?\n  lastSyncedAt       DateTime?\n  totalCommitsSynced Int       @default(0)\n  repositoryUrl      String?\n  branch             String    @default(\"main\")\n  createdAt          DateTime  @default(now())\n  updatedAt          DateTime  @updatedAt\n  project            Project   @relation(fields: [projectId], references: [id], onDelete: Cascade)\n\n  @@index([projectId])\n  @@index([lastSyncedAt])\n}\n\nmodel SyncedCommit {\n  id                String   @id @default(cuid())\n  projectId         String\n  commitHash        String\n  commitMessage     String\n  commitAuthor      String\n  commitEmail       String\n  commitDate        DateTime\n  commitFiles       String[] // Array of file paths\n  conventionalType  String? // feat, fix, docs, etc.\n  conventionalScope String? // Optional scope from conventional commits\n  isBreaking        Boolean  @default(false)\n  commitBody        String? // Optional commit body\n  commitFooter      String? // Optional commit footer\n  syncedAt          DateTime @default(now())\n  branch            String   @default(\"main\")\n  project           Project  @relation(fields: [projectId], references: [id], onDelete: Cascade)\n\n  @@unique([projectId, commitHash]) // Prevent duplicate commits per project\n  @@index([projectId])\n  @@index([commitHash])\n  @@index([syncedAt])\n  @@index([conventionalType])\n  @@index([branch])\n}\n"
  },
  {
    "path": "prisma/schema/projects.prisma",
    "content": "// Project management models\n// Includes projects, changelogs, and related features\n\nmodel Project {\n  id                  String                     @id @default(cuid())\n  name                String\n  isPublic            Boolean                    @default(false)\n  allowAutoPublish    Boolean                    @default(false)\n  requireApproval     Boolean                    @default(true)\n  defaultTags         String[]                   @default([])\n  changelog           Changelog?\n  createdAt           DateTime                   @default(now())\n  updatedAt           DateTime                   @updatedAt\n  changelogRequests   ChangelogRequest[]\n  emailConfig         EmailConfig?\n  emailLogs           EmailLog[]\n  slackIntegration    SlackIntegration?\n  ProjectSubscription ProjectSubscription[]\n  gitHubIntegration   GitHubIntegration?\n  analyticsViews      PublicChangelogAnalytics[]\n  customDomains       CustomDomain[]\n  syncMetadata        ProjectSyncMetadata?\n  syncedCommits       SyncedCommit[]\n  apiKeys             ApiKey[]\n  widgets             Widget[]\n}\n\nmodel Changelog {\n  id        String           @id @default(cuid())\n  project   Project          @relation(fields: [projectId], references: [id], onDelete: Cascade)\n  projectId String           @unique\n  entries   ChangelogEntry[]\n  createdAt DateTime         @default(now())\n  updatedAt DateTime         @updatedAt\n}\n\nmodel ChangelogEntry {\n  id             String                     @id @default(cuid())\n  title          String\n  content        String                     @db.Text\n  excerpt        String?                    @db.Text // First ~300 chars for list views (auto-generated)\n  version        String?\n  publishedAt    DateTime?\n  scheduledAt    DateTime?\n  createdAt      DateTime                   @default(now())\n  updatedAt      DateTime                   @updatedAt\n  tags           ChangelogTag[]\n  changelog      Changelog                  @relation(fields: [changelogId], references: [id], onDelete: Cascade)\n  changelogId    String\n  requests       ChangelogRequest[]\n  analyticsViews PublicChangelogAnalytics[]\n  scheduledJobs  ScheduledJob[]\n\n  @@index([changelogId])\n  @@index([scheduledAt])\n  @@index([scheduledAt, publishedAt])\n}\n\nmodel ChangelogTag {\n  id        String             @id @default(cuid())\n  name      String             @unique\n  color     String?\n  entries   ChangelogEntry[]\n  requests  ChangelogRequest[]\n  createdAt DateTime           @default(now())\n  updatedAt DateTime           @updatedAt\n}\n\nmodel ChangelogRequest {\n  id               String          @id @default(cuid())\n  type             String // DELETE_PROJECT, DELETE_TAG, DELETE_ENTRY\n  status           String          @default(\"PENDING\") // PENDING, APPROVED, REJECTED\n  staffId          String? // Made nullable to allow user deletion\n  staff            User?           @relation(\"StaffRequests\", fields: [staffId], references: [id], onDelete: SetNull)\n  adminId          String?\n  admin            User?           @relation(\"AdminResponses\", fields: [adminId], references: [id], onDelete: SetNull)\n  projectId        String\n  project          Project         @relation(fields: [projectId], references: [id], onDelete: Cascade)\n  targetId         String? // For specific items being deleted (e.g., tag id)\n  createdAt        DateTime        @default(now())\n  reviewedAt       DateTime?\n  ChangelogEntry   ChangelogEntry? @relation(fields: [changelogEntryId], references: [id])\n  changelogEntryId String?\n  ChangelogTag     ChangelogTag?   @relation(fields: [changelogTagId], references: [id])\n  changelogTagId   String?\n  metadata         Json? // Store request-specific data (e.g., custom publishedAt for ALLOW_PUBLISH)\n\n  @@index([staffId])\n  @@index([adminId])\n  @@index([projectId])\n  @@index([status])\n}\n\nmodel Widget {\n  id        String   @id @default(cuid())\n  projectId String\n  project   Project  @relation(fields: [projectId], references: [id], onDelete: Cascade)\n  name      String // \"Homepage Widget\", \"Dashboard Popup\", etc.\n  variant   String // 'classic', 'floating', 'modal', 'announcement'\n  settings  Json     @default(\"{}\") // { position: \"bottom-right\", maxEntries: 5, ... }\n  customCSS String?  @db.Text\n  isActive  Boolean  @default(true)\n  createdAt DateTime @default(now())\n  updatedAt DateTime @updatedAt\n\n  @@index([projectId])\n  @@index([projectId, isActive])\n}\n\nmodel CustomDomain {\n  id                String                @id @default(cuid())\n  domain            String                @unique\n  projectId         String\n  verificationToken String                @unique\n  verified          Boolean               @default(false)\n  createdAt         DateTime              @default(now())\n  verifiedAt        DateTime?\n  userId            String? // Optional: link to user who owns this domain\n  forceHttps        Boolean               @default(false)\n  sslMode           SslMode               @default(NONE)\n  project           Project               @relation(fields: [projectId], references: [id], onDelete: Cascade)\n  certificates      DomainCertificate[]\n  browserRules      DomainBrowserRule[]\n  throttleConfig    DomainThrottleConfig?\n\n  @@index([domain])\n  @@index([projectId])\n  @@index([userId])\n  @@index([verified])\n  @@map(\"custom_domains\")\n}\n\nmodel DomainCertificate {\n  id               String            @id @default(cuid())\n  domainId         String\n  domain           CustomDomain      @relation(fields: [domainId], references: [id], onDelete: Cascade)\n  status           CertificateStatus\n  challengeType    ChallengeType\n  privateKeyPem    String            @db.Text // Encrypted before writing, never plaintext\n  certificatePem   String?           @db.Text\n  fullChainPem     String?           @db.Text\n  csrPem           String            @db.Text\n  issuedAt         DateTime?\n  expiresAt        DateTime?\n  acmeOrderUrl     String?\n  challengeToken   String?\n  challengeKeyAuth String?\n  dnsTxtValue      String?\n  lastError        String?           @db.Text\n  renewalAttempts  Int               @default(0)\n  createdAt        DateTime          @default(now())\n  updatedAt        DateTime          @updatedAt\n\n  @@index([domainId])\n  @@index([status])\n  @@index([expiresAt])\n  @@index([domainId, status])\n  @@index([challengeToken])\n}\n\nmodel DomainBrowserRule {\n  id               String          @id @default(cuid())\n  domainId         String\n  domain           CustomDomain    @relation(fields: [domainId], references: [id], onDelete: Cascade)\n  userAgentPattern String // Regex string\n  ruleType         BrowserRuleType\n  isEnabled        Boolean         @default(true)\n  createdAt        DateTime        @default(now())\n  updatedAt        DateTime        @updatedAt\n\n  @@index([domainId, isEnabled])\n}\n\nmodel DomainThrottleConfig {\n  id                String       @id @default(cuid())\n  domainId          String       @unique\n  domain            CustomDomain @relation(fields: [domainId], references: [id], onDelete: Cascade)\n  enabled           Boolean      @default(false)\n  requestsPerSecond Int          @default(60)\n  burstSize         Int          @default(20)\n  createdAt         DateTime     @default(now())\n  updatedAt         DateTime     @updatedAt\n}\n\nmodel AcmeAccount {\n  id            String   @id @default(\"global\")\n  accountKeyPem String   @db.Text // Encrypted, never plaintext\n  accountUrl    String\n  email         String\n  createdAt     DateTime @default(now())\n  updatedAt     DateTime @updatedAt\n}\n"
  },
  {
    "path": "prisma/schema/system.prisma",
    "content": "// System-level configuration and management\n// Includes API keys, audit logs, system config, analytics, and scheduled jobs\n\nmodel ApiKey {\n  id          String    @id @default(cuid())\n  name        String\n  key         String    @unique\n  lastUsed    DateTime?\n  createdAt   DateTime  @default(now())\n  expiresAt   DateTime?\n  userId      String\n  user        User      @relation(fields: [userId], references: [id])\n  projectId   String? // null = global access, set = project-specific\n  project     Project?  @relation(fields: [projectId], references: [id], onDelete: Cascade)\n  permissions String[] // Array of permitted actions\n  isRevoked   Boolean   @default(false)\n  isGlobal    Boolean   @default(false) // true = visible to admins, false = private to owner only\n\n  @@index([userId])\n  @@index([key])\n  @@index([projectId])\n}\n\nmodel AuditLog {\n  id           String   @id @default(cuid())\n  action       String\n  userId       String? // Nullable to allow user deletion\n  targetUserId String?\n  details      Json?\n  createdAt    DateTime @default(now())\n  user         User?    @relation(\"AuditLogUser\", fields: [userId], references: [id], onDelete: SetNull)\n  targetUser   User?    @relation(\"AuditLogTarget\", fields: [targetUserId], references: [id], onDelete: SetNull)\n\n  @@index([userId])\n  @@index([targetUserId])\n  @@index([action])\n  @@index([createdAt])\n}\n\nmodel SystemConfig {\n  id                            Int            @id @default(1)\n  defaultInvitationExpiry       Int            @default(7)\n  requireApprovalForChangelogs  Boolean        @default(true)\n  maxChangelogEntriesPerProject Int            @default(100)\n  enableAnalytics               Boolean        @default(true)\n  enableNotifications           Boolean        @default(true)\n  enablePasswordReset           Boolean        @default(false)\n  enableAIAssistant             Boolean        @default(false) // Enable/disable AI assistant globally\n  aiApiKey                      String? // Global API key for AI service\n  aiApiProvider                 String?        @default(\"secton\") // AI provider\n  aiDefaultModel                String?        @default(\"copilot-zero\") // Default model to use\n  smtpHost                      String?\n  smtpPort                      Int?\n  smtpUser                      String?\n  smtpPassword                  String?\n  smtpSecure                    Boolean?\n  systemEmail                   String?\n  allowTelemetry                TelemetryState @default(PROMPT)\n  telemetryInstanceId           String?        @unique\n  adminOnlyApiKeyCreation       Boolean        @default(false) // Require admin role to create API keys\n  timezone                      String         @default(\"UTC\") // IANA timezone for date-based version templates\n  allowUserTimezone             Boolean        @default(true) // Allow users to set their own timezone\n  customDateTemplates           Json? // JSON array of custom date templates for version selector\n  // License Configuration\n  sponsorLicenseKey             String?\n  sponsorLicenseValid           Boolean        @default(false)\n  sponsorLastVerified           DateTime?\n  sponsorProof                  String?        @db.Text\n  sponsorPayload                String?        @db.Text\n  // Slack OAuth Configuration\n  slackOAuthClientId            String? // Slack app client ID (encrypted)\n  slackOAuthClientSecret        String? // Slack app client secret (encrypted)\n  slackOAuthEnabled             Boolean        @default(false) // Enable/disable Slack integration\n  slackSigningSecret            String? // Slack app signing secret (encrypted) - for API verification\n  // IP Whitelisting for panel access\n  panelIpWhitelistEnabled       Boolean        @default(false)\n  panelIpWhitelist              String[]\n  createdAt                     DateTime       @default(now())\n  updatedAt                     DateTime       @updatedAt\n}\n\nmodel PublicChangelogAnalytics {\n  id               String          @id @default(cuid())\n  projectId        String\n  changelogEntryId String?\n  ipHash           String // Hashed IP for privacy\n  country          String? // Country from IP geolocation\n  userAgent        String? // Browser info\n  referrer         String? // Referring URL\n  viewedAt         DateTime        @default(now())\n  sessionHash      String // Session identifier for unique visits\n  project          Project         @relation(fields: [projectId], references: [id], onDelete: Cascade)\n  changelogEntry   ChangelogEntry? @relation(fields: [changelogEntryId], references: [id], onDelete: SetNull)\n\n  @@index([projectId])\n  @@index([changelogEntryId])\n  @@index([viewedAt])\n  @@index([country])\n  @@index([sessionHash])\n}\n\nmodel ScheduledJob {\n  id               String           @id @default(cuid())\n  type             ScheduledJobType\n  entityId         String // ID of the entity (e.g., ChangelogEntry ID)\n  scheduledAt      DateTime // When to execute\n  executedAt       DateTime? // When it was actually executed\n  status           JobStatus        @default(PENDING)\n  errorMessage     String? // Error details if failed\n  retryCount       Int              @default(0)\n  maxRetries       Int              @default(3)\n  createdAt        DateTime         @default(now())\n  updatedAt        DateTime         @updatedAt\n  ChangelogEntry   ChangelogEntry?  @relation(fields: [changelogEntryId], references: [id], onDelete: SetNull)\n  changelogEntryId String?\n\n  @@index([type])\n  @@index([entityId])\n  @@index([scheduledAt])\n  @@index([status])\n  @@index([scheduledAt, status])\n}\n"
  },
  {
    "path": "prisma/schema/users.prisma",
    "content": "// User authentication and authorization models\n// Includes users, OAuth, passkeys, and related authentication features\n\nmodel User {\n  id                String             @id @default(cuid())\n  email             String             @unique\n  password          String\n  name              String?\n  role              Role               @default(STAFF)\n  refreshTokens     RefreshToken[]\n  createdAt         DateTime           @default(now())\n  updatedAt         DateTime           @updatedAt\n  lastLoginAt       DateTime?\n  settings          Settings?\n  staffRequests     ChangelogRequest[] @relation(\"StaffRequests\")\n  adminResponses    ChangelogRequest[] @relation(\"AdminResponses\")\n  ApiKey            ApiKey[]\n  auditLogs         AuditLog[]         @relation(\"AuditLogUser\")\n  targetAuditLogs   AuditLog[]         @relation(\"AuditLogTarget\")\n  oauthConnections  OAuthConnection[]\n  samlConnections   SAMLConnection[]\n  passwordResets    PasswordReset[]\n  passkeys          Passkey[]\n  lastChallenge     String?\n  twoFactorMode     TwoFactorMode?     @default(NONE)\n  twoFactorSessions TwoFactorSession[]\n  CliAuthCode       CliAuthCode[]\n}\n\nmodel RefreshToken {\n  id          String   @id @default(cuid())\n  token       String   @unique\n  userId      String\n  user        User     @relation(fields: [userId], references: [id], onDelete: Cascade)\n  createdAt   DateTime @default(now())\n  expiresAt   DateTime\n  invalidated Boolean  @default(false)\n\n  @@index([userId])\n}\n\nmodel OAuthProvider {\n  id                  String            @id @default(cuid())\n  name                String\n  clientId            String\n  clientSecret        String\n  authorizationUrl    String\n  tokenUrl            String\n  userInfoUrl         String\n  callbackUrl         String\n  scopes              String[]\n  enabled             Boolean           @default(true)\n  isDefault           Boolean           @default(false)\n  allowedEmailDomains String[]          @default([])\n  blockExistingUsers  Boolean           @default(false)\n  requiredClaims      Json?             @default(\"{}\")\n  createdAt           DateTime          @default(now())\n  updatedAt           DateTime          @updatedAt\n  connections         OAuthConnection[]\n}\n\nmodel OAuthConnection {\n  id             String        @id @default(cuid())\n  providerId     String\n  provider       OAuthProvider @relation(fields: [providerId], references: [id], onDelete: Cascade)\n  userId         String\n  user           User          @relation(fields: [userId], references: [id], onDelete: Cascade)\n  providerUserId String\n  accessToken    String?\n  refreshToken   String?\n  expiresAt      DateTime?\n  createdAt      DateTime      @default(now())\n  updatedAt      DateTime      @updatedAt\n\n  @@unique([providerId, providerUserId])\n  @@unique([providerId, userId])\n  @@index([userId])\n  @@index([providerId])\n}\n\nmodel Settings {\n  id                  String   @id @default(cuid())\n  userId              String   @unique\n  user                User     @relation(fields: [userId], references: [id], onDelete: Cascade)\n  theme               String   @default(\"light\")\n  enableNotifications Boolean  @default(true)\n  timezone            String? // User-preferred IANA timezone override (null = use system default)\n  createdAt           DateTime @default(now())\n  updatedAt           DateTime @updatedAt\n\n  @@index([userId])\n}\n\nmodel Passkey {\n  id           String    @id @default(cuid())\n  userId       String\n  user         User      @relation(fields: [userId], references: [id], onDelete: Cascade)\n  name         String // User-friendly name for the passkey\n  credentialId String    @unique\n  publicKey    String // Base64 encoded public key\n  counter      Int       @default(0)\n  transports   String[] // Transport types (usb, nfc, ble, internal)\n  createdAt    DateTime  @default(now())\n  lastUsedAt   DateTime?\n\n  @@index([userId])\n  @@index([credentialId])\n}\n\nmodel PasswordReset {\n  id        String    @id @default(cuid())\n  token     String    @unique\n  userId    String\n  user      User      @relation(fields: [userId], references: [id], onDelete: Cascade)\n  email     String\n  expiresAt DateTime\n  createdAt DateTime  @default(now())\n  usedAt    DateTime?\n\n  @@index([token])\n  @@index([userId])\n  @@index([email])\n}\n\nmodel TwoFactorSession {\n  id        String        @id @default(cuid())\n  userId    String\n  user      User          @relation(fields: [userId], references: [id], onDelete: Cascade)\n  type      TwoFactorMode\n  expiresAt DateTime\n  createdAt DateTime      @default(now())\n\n  @@index([userId])\n  @@index([expiresAt])\n}\n\nmodel InvitationLink {\n  id        String    @id @default(cuid())\n  token     String    @unique\n  role      Role\n  email     String\n  createdBy String\n  expiresAt DateTime\n  usedAt    DateTime?\n  createdAt DateTime  @default(now())\n\n  @@index([token])\n  @@index([email])\n}\n\nmodel CliAuthCode {\n  id          String    @id @default(cuid())\n  code        String    @unique\n  userId      String\n  callbackUrl String\n  expiresAt   DateTime\n  usedAt      DateTime?\n  createdAt   DateTime  @default(now())\n  user        User      @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n  @@index([code])\n  @@index([userId])\n  @@index([expiresAt])\n}\n\nmodel SAMLProvider {\n  id                  String           @id @default(cuid())\n  name                String           @unique\n  entityId            String\n  ssoUrl              String\n  certificate         String\n  spEntityId          String?\n  nameIdFormat        String           @default(\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\")\n  emailAttribute      String           @default(\"email\")\n  nameAttribute       String           @default(\"name\")\n  enabled             Boolean          @default(true)\n  isDefault           Boolean          @default(false)\n  allowedEmailDomains String[]         @default([])\n  blockExistingUsers  Boolean          @default(false)\n  requiredClaims      Json?            @default(\"{}\")\n  createdAt           DateTime         @default(now())\n  updatedAt           DateTime         @updatedAt\n  connections         SAMLConnection[]\n}\n\nmodel SAMLConnection {\n  id           String       @id @default(cuid())\n  providerId   String\n  provider     SAMLProvider @relation(fields: [providerId], references: [id], onDelete: Cascade)\n  userId       String\n  user         User         @relation(fields: [userId], references: [id], onDelete: Cascade)\n  nameId       String\n  sessionIndex String?\n  createdAt    DateTime     @default(now())\n  updatedAt    DateTime     @updatedAt\n\n  @@unique([providerId, userId])\n  @@unique([providerId, nameId])\n  @@index([userId])\n  @@index([providerId])\n}\n"
  },
  {
    "path": "prisma/schema.prisma.backup",
    "content": "datasource db {\n  provider = \"postgresql\"\n  url      = env(\"DATABASE_URL\")\n}\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"fullTextSearchPostgres\"]\n}\n\nmodel User {\n  id                String             @id @default(cuid())\n  email             String             @unique\n  password          String\n  name              String?\n  role              Role               @default(STAFF)\n  refreshTokens     RefreshToken[]\n  createdAt         DateTime           @default(now())\n  updatedAt         DateTime           @updatedAt\n  lastLoginAt       DateTime?\n  settings          Settings?\n  staffRequests     ChangelogRequest[] @relation(\"StaffRequests\")\n  adminResponses    ChangelogRequest[] @relation(\"AdminResponses\")\n  ApiKey            ApiKey[]\n  auditLogs         AuditLog[]         @relation(\"AuditLogUser\")\n  targetAuditLogs   AuditLog[]         @relation(\"AuditLogTarget\")\n  oauthConnections  OAuthConnection[]\n  passwordResets    PasswordReset[]\n  passkeys          Passkey[]\n  lastChallenge     String?\n  twoFactorMode     TwoFactorMode?     @default(NONE)\n  twoFactorSessions TwoFactorSession[]\n  CliAuthCode       CliAuthCode[]\n}\n\nmodel RefreshToken {\n  id          String   @id @default(cuid())\n  token       String   @unique\n  userId      String\n  user        User     @relation(fields: [userId], references: [id], onDelete: Cascade)\n  createdAt   DateTime @default(now())\n  expiresAt   DateTime\n  invalidated Boolean  @default(false)\n\n  @@index([userId])\n}\n\nmodel OAuthProvider {\n  id               String   @id @default(cuid())\n  name             String\n  clientId         String\n  clientSecret     String\n  authorizationUrl String\n  tokenUrl         String\n  userInfoUrl      String\n  callbackUrl      String\n  scopes           String[]\n  enabled          Boolean  @default(true)\n  isDefault        Boolean  @default(false)\n  createdAt        DateTime @default(now())\n  updatedAt        DateTime @updatedAt\n\n  connections OAuthConnection[]\n}\n\nmodel OAuthConnection {\n  id             String        @id @default(cuid())\n  providerId     String\n  provider       OAuthProvider @relation(fields: [providerId], references: [id], onDelete: Cascade)\n  userId         String\n  user           User          @relation(fields: [userId], references: [id], onDelete: Cascade)\n  providerUserId String\n  accessToken    String?\n  refreshToken   String?\n  expiresAt      DateTime?\n  createdAt      DateTime      @default(now())\n  updatedAt      DateTime      @updatedAt\n\n  @@unique([providerId, providerUserId])\n  @@unique([providerId, userId])\n  @@index([userId])\n  @@index([providerId])\n}\n\nmodel Settings {\n  id                  String   @id @default(cuid())\n  userId              String   @unique\n  user                User     @relation(fields: [userId], references: [id], onDelete: Cascade)\n  theme               String   @default(\"light\")\n  enableNotifications Boolean  @default(true)\n  timezone            String?  // User-preferred IANA timezone override (null = use system default)\n  createdAt           DateTime @default(now())\n  updatedAt           DateTime @updatedAt\n\n  @@index([userId])\n}\n\nmodel InvitationLink {\n  id        String    @id @default(cuid())\n  token     String    @unique\n  role      Role\n  email     String\n  createdBy String\n  expiresAt DateTime\n  usedAt    DateTime?\n  createdAt DateTime  @default(now())\n\n  @@index([token])\n  @@index([email])\n}\n\nmodel Project {\n  id                  String                     @id @default(cuid())\n  name                String\n  isPublic            Boolean                    @default(false)\n  allowAutoPublish    Boolean                    @default(false)\n  requireApproval     Boolean                    @default(true)\n  defaultTags         String[]                   @default([])\n  changelog           Changelog?\n  createdAt           DateTime                   @default(now())\n  updatedAt           DateTime                   @updatedAt\n  changelogRequests   ChangelogRequest[]\n  emailConfig         EmailConfig?\n  emailLogs           EmailLog[]\n  slackIntegration    SlackIntegration?\n  ProjectSubscription ProjectSubscription[]\n  gitHubIntegration   GitHubIntegration?\n  analyticsViews      PublicChangelogAnalytics[]\n  customDomains       CustomDomain[]\n  syncMetadata        ProjectSyncMetadata?\n  syncedCommits       SyncedCommit[]\n  apiKeys             ApiKey[]\n  widgets             Widget[]\n}\n\nmodel Widget {\n  id        String   @id @default(cuid())\n  projectId String\n  project   Project  @relation(fields: [projectId], references: [id], onDelete: Cascade)\n\n  name      String // \"Homepage Widget\", \"Dashboard Popup\", etc.\n  variant   String // 'classic', 'floating', 'modal', 'announcement'\n  settings  Json   @default(\"{}\") // { position: \"bottom-right\", maxEntries: 5, ... }\n  customCSS String? @db.Text\n  isActive  Boolean @default(true)\n\n  createdAt DateTime @default(now())\n  updatedAt DateTime @updatedAt\n\n  @@index([projectId])\n  @@index([projectId, isActive])\n}\n\nmodel Passkey {\n  id           String    @id @default(cuid())\n  userId       String\n  user         User      @relation(fields: [userId], references: [id], onDelete: Cascade)\n  name         String // User-friendly name for the passkey\n  credentialId String    @unique\n  publicKey    String // Base64 encoded public key\n  counter      Int       @default(0)\n  transports   String[] // Transport types (usb, nfc, ble, internal)\n  createdAt    DateTime  @default(now())\n  lastUsedAt   DateTime?\n\n  @@index([userId])\n  @@index([credentialId])\n}\n\nmodel Changelog {\n  id        String           @id @default(cuid())\n  project   Project          @relation(fields: [projectId], references: [id], onDelete: Cascade)\n  projectId String           @unique\n  entries   ChangelogEntry[]\n  createdAt DateTime         @default(now())\n  updatedAt DateTime         @updatedAt\n}\n\nmodel ChangelogEntry {\n  id             String                     @id @default(cuid())\n  title          String\n  content        String                     @db.Text\n  excerpt        String?                    @db.Text // First ~300 chars for list views (auto-generated)\n  version        String?\n  publishedAt    DateTime?\n  scheduledAt    DateTime?\n  createdAt      DateTime                   @default(now())\n  updatedAt      DateTime                   @updatedAt\n  tags           ChangelogTag[]\n  changelog      Changelog                  @relation(fields: [changelogId], references: [id], onDelete: Cascade)\n  changelogId    String\n  requests       ChangelogRequest[]\n  analyticsViews PublicChangelogAnalytics[]\n  scheduledJobs  ScheduledJob[]\n\n  @@index([changelogId])\n  @@index([scheduledAt])\n  @@index([scheduledAt, publishedAt])\n}\n\nmodel ChangelogTag {\n  id        String             @id @default(cuid())\n  name      String             @unique\n  color     String?\n  entries   ChangelogEntry[]\n  requests  ChangelogRequest[]\n  createdAt DateTime           @default(now())\n  updatedAt DateTime           @updatedAt\n}\n\nmodel ChangelogRequest {\n  id               String          @id @default(cuid())\n  type             String // DELETE_PROJECT, DELETE_TAG, DELETE_ENTRY\n  status           String          @default(\"PENDING\") // PENDING, APPROVED, REJECTED\n  staffId          String? // Made nullable to allow user deletion\n  staff            User?           @relation(\"StaffRequests\", fields: [staffId], references: [id], onDelete: SetNull)\n  adminId          String?\n  admin            User?           @relation(\"AdminResponses\", fields: [adminId], references: [id], onDelete: SetNull)\n  projectId        String\n  project          Project         @relation(fields: [projectId], references: [id], onDelete: Cascade)\n  targetId         String? // For specific items being deleted (e.g., tag id)\n  createdAt        DateTime        @default(now())\n  reviewedAt       DateTime?\n  ChangelogEntry   ChangelogEntry? @relation(fields: [changelogEntryId], references: [id])\n  changelogEntryId String?\n  ChangelogTag     ChangelogTag?   @relation(fields: [changelogTagId], references: [id])\n  changelogTagId   String?\n  metadata         Json? // Store request-specific data (e.g., custom publishedAt for ALLOW_PUBLISH)\n\n  @@index([staffId])\n  @@index([adminId])\n  @@index([projectId])\n  @@index([status])\n}\n\nmodel ApiKey {\n  id          String    @id @default(cuid())\n  name        String\n  key         String    @unique\n  lastUsed    DateTime?\n  createdAt   DateTime  @default(now())\n  expiresAt   DateTime?\n  userId      String\n  user        User      @relation(fields: [userId], references: [id])\n  projectId   String?   // null = global access, set = project-specific\n  project     Project?  @relation(fields: [projectId], references: [id], onDelete: Cascade)\n  permissions String[]  // Array of permitted actions\n  isRevoked   Boolean   @default(false)\n  isGlobal    Boolean   @default(false) // true = visible to admins, false = private to owner only\n\n  @@index([userId])\n  @@index([key])\n  @@index([projectId])\n}\n\nmodel AuditLog {\n  id           String   @id @default(cuid())\n  action       String\n  userId       String? // Nullable to allow user deletion\n  targetUserId String?\n  details      Json?\n  createdAt    DateTime @default(now())\n  user         User?    @relation(\"AuditLogUser\", fields: [userId], references: [id], onDelete: SetNull)\n  targetUser   User?    @relation(\"AuditLogTarget\", fields: [targetUserId], references: [id], onDelete: SetNull)\n\n  @@index([userId])\n  @@index([targetUserId])\n  @@index([action])\n  @@index([createdAt])\n}\n\nmodel SystemConfig {\n  id                            Int            @id @default(1)\n  defaultInvitationExpiry       Int            @default(7)\n  requireApprovalForChangelogs  Boolean        @default(true)\n  maxChangelogEntriesPerProject Int            @default(100)\n  enableAnalytics               Boolean        @default(true)\n  enableNotifications           Boolean        @default(true)\n  enablePasswordReset           Boolean        @default(false)\n  enableAIAssistant             Boolean        @default(false) // Enable/disable AI assistant globally\n  aiApiKey                      String? // Global API key for AI service\n  aiApiProvider                 String?        @default(\"secton\") // AI provider\n  aiDefaultModel                String?        @default(\"copilot-zero\") // Default model to use\n  smtpHost                      String?\n  smtpPort                      Int?\n  smtpUser                      String?\n  smtpPassword                  String?\n  smtpSecure                    Boolean?\n  systemEmail                   String?\n  allowTelemetry                TelemetryState @default(PROMPT)\n  telemetryInstanceId           String?        @unique\n  adminOnlyApiKeyCreation       Boolean        @default(false) // Require admin role to create API keys\n  timezone                      String         @default(\"UTC\") // IANA timezone for date-based version templates\n  allowUserTimezone             Boolean        @default(true) // Allow users to set their own timezone\n  customDateTemplates           Json?          // JSON array of custom date templates for version selector\n\n  // License Configuration\n  sponsorLicenseKey             String?\n  sponsorLicenseValid           Boolean        @default(false)\n  sponsorLastVerified           DateTime?\n  sponsorProof                  String?        @db.Text\n  sponsorPayload                String?        @db.Text\n\n  // Slack OAuth Configuration\n  slackOAuthClientId            String? // Slack app client ID (encrypted)\n  slackOAuthClientSecret        String? // Slack app client secret (encrypted)\n  slackOAuthEnabled             Boolean        @default(false) // Enable/disable Slack integration\n  slackSigningSecret            String? // Slack app signing secret (encrypted) - for API verification\n\n  createdAt                     DateTime       @default(now())\n  updatedAt                     DateTime       @updatedAt\n}\n\nmodel EmailConfig {\n  id             String    @id @default(cuid())\n  projectId      String    @unique\n  project        Project   @relation(fields: [projectId], references: [id], onDelete: Cascade)\n  enabled        Boolean   @default(false)\n  smtpHost       String\n  smtpPort       Int\n  smtpUser       String?\n  smtpPassword   String?\n  smtpSecure     Boolean   @default(true)\n  fromEmail      String\n  fromName       String?\n  replyToEmail   String?\n  defaultSubject String?\n  createdAt      DateTime  @default(now())\n  updatedAt      DateTime  @updatedAt\n  lastTestedAt   DateTime?\n  testStatus     String? // \"success\", \"failed\", error message\n}\n\nmodel EmailLog {\n  id         String   @id @default(cuid())\n  projectId  String\n  project    Project  @relation(fields: [projectId], references: [id], onDelete: Cascade)\n  recipients String[]\n  subject    String\n  messageId  String?\n  type       String // \"SINGLE_UPDATE\" or \"DIGEST\"\n  entryIds   String[]\n  createdAt  DateTime @default(now())\n}\n\nmodel SlackIntegration {\n  id                 String    @id @default(cuid())\n  projectId          String    @unique\n  project            Project   @relation(fields: [projectId], references: [id], onDelete: Cascade)\n\n  // OAuth credentials\n  accessToken        String    // Slack bot token (encrypted)\n  refreshToken       String?   // Slack app refresh token if using refresh tokens\n  tokenExpiresAt     DateTime?\n\n  // Workspace info\n  teamId             String    // Slack workspace ID\n  teamName           String?   // Slack workspace name\n  botUserId          String    // Slack bot user ID\n  botUsername        String?   // Slack bot username\n\n  // Configuration\n  channelId          String    // Default channel to post updates to\n  channelName        String?   // Channel name for display\n  autoSend           Boolean   @default(true) // Auto-post on publish\n  enabled            Boolean   @default(true)\n\n  // Logging\n  lastSyncAt         DateTime?\n  lastErrorMessage   String?\n  postCount          Int       @default(0)\n\n  createdAt          DateTime  @default(now())\n  updatedAt          DateTime  @updatedAt\n\n  @@index([projectId])\n  @@index([teamId])\n}\n\nmodel EmailSubscriber {\n  id               String                @id @default(cuid())\n  email            String\n  name             String?\n  isActive         Boolean               @default(true)\n  unsubscribeToken String                @unique\n  createdAt        DateTime              @default(now())\n  updatedAt        DateTime              @updatedAt\n  lastEmailSentAt  DateTime?\n  subscriptions    ProjectSubscription[]\n\n  @@unique([email])\n  @@index([email])\n  @@index([unsubscribeToken])\n}\n\nmodel ProjectSubscription {\n  id               String           @id @default(cuid())\n  projectId        String\n  customDomain     String? // domain subscribed from ( optional )\n  project          Project          @relation(fields: [projectId], references: [id], onDelete: Cascade)\n  subscriberId     String\n  subscriber       EmailSubscriber  @relation(fields: [subscriberId], references: [id], onDelete: Cascade)\n  subscriptionType SubscriptionType @default(ALL_UPDATES)\n  createdAt        DateTime         @default(now())\n  updatedAt        DateTime         @updatedAt\n\n  @@unique([projectId, subscriberId])\n  @@index([projectId])\n  @@index([customDomain])\n  @@index([subscriberId])\n}\n\nmodel PasswordReset {\n  id        String    @id @default(cuid())\n  token     String    @unique\n  userId    String\n  user      User      @relation(fields: [userId], references: [id], onDelete: Cascade)\n  email     String\n  expiresAt DateTime\n  createdAt DateTime  @default(now())\n  usedAt    DateTime?\n\n  @@index([token])\n  @@index([userId])\n  @@index([email])\n}\n\nmodel TwoFactorSession {\n  id        String        @id @default(cuid())\n  userId    String\n  user      User          @relation(fields: [userId], references: [id], onDelete: Cascade)\n  type      TwoFactorMode\n  expiresAt DateTime\n  createdAt DateTime      @default(now())\n\n  @@index([userId])\n  @@index([expiresAt])\n}\n\nmodel GitHubIntegration {\n  id                     String    @id @default(cuid())\n  projectId              String    @unique\n  project                Project   @relation(fields: [projectId], references: [id], onDelete: Cascade)\n  repositoryUrl          String\n  accessToken            String // Encrypted access token\n  defaultBranch          String    @default(\"main\")\n  lastSyncAt             DateTime?\n  lastCommitSha          String?\n  includeBreakingChanges Boolean   @default(true)\n  includeFixes           Boolean   @default(true)\n  includeFeatures        Boolean   @default(true)\n  includeChores          Boolean   @default(false)\n  customCommitTypes      String[]  @default([])\n  enabled                Boolean   @default(true)\n  createdAt              DateTime  @default(now())\n  updatedAt              DateTime  @updatedAt\n\n  @@index([projectId])\n  @@index([enabled])\n}\n\nmodel PublicChangelogAnalytics {\n  id               String   @id @default(cuid())\n  projectId        String\n  changelogEntryId String?\n  ipHash           String // Hashed IP for privacy\n  country          String? // Country from IP geolocation\n  userAgent        String? // Browser info\n  referrer         String? // Referring URL\n  viewedAt         DateTime @default(now())\n  sessionHash      String // Session identifier for unique visits\n\n  // Relations\n  project        Project         @relation(fields: [projectId], references: [id], onDelete: Cascade)\n  changelogEntry ChangelogEntry? @relation(fields: [changelogEntryId], references: [id], onDelete: SetNull)\n\n  @@index([projectId])\n  @@index([changelogEntryId])\n  @@index([viewedAt])\n  @@index([country])\n  @@index([sessionHash])\n}\n\nmodel CustomDomain {\n  id                String    @id @default(cuid())\n  domain            String    @unique\n  projectId         String\n  verificationToken String    @unique\n  verified          Boolean   @default(false)\n  createdAt         DateTime  @default(now())\n  verifiedAt        DateTime?\n  userId            String? // Optional: link to user who owns this domain\n\n  // Relations\n  project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n\n  // Indexes\n  @@index([domain])\n  @@index([projectId])\n  @@index([userId])\n  @@index([verified])\n  @@map(\"custom_domains\")\n}\n\nmodel ScheduledJob {\n  id               String           @id @default(cuid())\n  type             ScheduledJobType\n  entityId         String // ID of the entity (e.g., ChangelogEntry ID)\n  scheduledAt      DateTime // When to execute\n  executedAt       DateTime? // When it was actually executed\n  status           JobStatus        @default(PENDING)\n  errorMessage     String? // Error details if failed\n  retryCount       Int              @default(0)\n  maxRetries       Int              @default(3)\n  createdAt        DateTime         @default(now())\n  updatedAt        DateTime         @updatedAt\n  ChangelogEntry   ChangelogEntry?  @relation(fields: [changelogEntryId], references: [id], onDelete: SetNull)\n  changelogEntryId String?\n\n  // Relations\n  @@index([type])\n  @@index([entityId])\n  @@index([scheduledAt])\n  @@index([status])\n  @@index([scheduledAt, status])\n}\n\nmodel CliAuthCode {\n  id          String    @id @default(cuid())\n  code        String    @unique\n  userId      String\n  callbackUrl String\n  expiresAt   DateTime\n  usedAt      DateTime?\n  createdAt   DateTime  @default(now())\n\n  user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n  @@index([code])\n  @@index([userId])\n  @@index([expiresAt])\n}\n\nmodel ProjectSyncMetadata {\n  id                 String    @id @default(cuid())\n  projectId          String    @unique\n  lastSyncHash       String?\n  lastSyncedAt       DateTime?\n  totalCommitsSynced Int       @default(0)\n  repositoryUrl      String?\n  branch             String    @default(\"main\")\n  createdAt          DateTime  @default(now())\n  updatedAt          DateTime  @updatedAt\n\n  project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n\n  @@index([projectId])\n  @@index([lastSyncedAt])\n}\n\nmodel SyncedCommit {\n  id                String   @id @default(cuid())\n  projectId         String\n  commitHash        String\n  commitMessage     String\n  commitAuthor      String\n  commitEmail       String\n  commitDate        DateTime\n  commitFiles       String[] // Array of file paths\n  conventionalType  String? // feat, fix, docs, etc.\n  conventionalScope String? // Optional scope from conventional commits\n  isBreaking        Boolean  @default(false)\n  commitBody        String? // Optional commit body\n  commitFooter      String? // Optional commit footer\n  syncedAt          DateTime @default(now())\n  branch            String   @default(\"main\")\n\n  project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n\n  @@unique([projectId, commitHash]) // Prevent duplicate commits per project\n  @@index([projectId])\n  @@index([commitHash])\n  @@index([syncedAt])\n  @@index([conventionalType])\n  @@index([branch])\n}\n\nenum ScheduledJobType {\n  PUBLISH_CHANGELOG_ENTRY\n  UNPUBLISH_CHANGELOG_ENTRY\n  DELETE_CHANGELOG_ENTRY\n  SEND_EMAIL_NOTIFICATION\n  TELEMETRY_SEND\n}\n\nenum JobStatus {\n  PENDING\n  RUNNING\n  COMPLETED\n  FAILED\n  CANCELLED\n}\n\nenum TelemetryState {\n  PROMPT\n  ENABLED\n  DISABLED\n}\n\nenum Role {\n  ADMIN\n  STAFF\n  VIEWER\n}\n\nenum RequestType {\n  DELETE_ENTRY\n  DELETE_TAG\n  DELETE_PROJECT\n  ALLOW_PUBLISH\n  ALLOW_SCHEDULE\n}\n\nenum RequestStatus {\n  PENDING\n  APPROVED\n  REJECTED\n}\n\nenum emailType {\n  SINGLE_UPDATE\n  DIGEST\n}\n\nenum SubscriptionType {\n  ALL_UPDATES // Receive all updates\n  MAJOR_ONLY // Only receive major version updates\n  DIGEST_ONLY // Only receive digest emails\n}\n\nenum TwoFactorMode {\n  NONE // No additional security\n  PASSKEY_PLUS_PASSWORD // Passkey login requires password\n  PASSWORD_PLUS_PASSKEY // Password login requires passkey\n}\n"
  },
  {
    "path": "prisma/seed.ts",
    "content": "import { PrismaClient, Role } from '@prisma/client'\nimport { hashPassword } from '../lib/auth/password'\nimport { faker } from '@faker-js/faker'\n\nconst prisma = new PrismaClient()\n\n// Helper function to generate sequential version numbers\nfunction generateVersionSequence(count: number): string[] {\n    const versions: string[] = []\n    let major = 1\n    let minor = 0\n    let patch = 0\n\n    for (let i = 0; i < count; i++) {\n        const rand = Math.random()\n        if (rand < 0.1 && i > 0) {\n            major++\n            minor = 0\n            patch = 0\n        } else if (rand < 0.3) {\n            minor++\n            patch = 0\n        } else {\n            patch++\n        }\n        versions.push(`v${major}.${minor}.${patch}`)\n    }\n\n    return versions.sort((a, b) => {\n        const [aMajor, aMinor, aPatch] = a.slice(1).split('.').map(Number)\n        const [bMajor, bMinor, bPatch] = b.slice(1).split('.').map(Number)\n\n        if (aMajor !== bMajor) return aMajor - bMajor\n        if (aMinor !== bMinor) return aMinor - bMinor\n        return aPatch - bPatch\n    })\n}\n\n// Helper function to generate sequential dates\nfunction generateSequentialDates(count: number, startDate = new Date('2024-01-01')): Date[] {\n    const dates: Date[] = []\n    const currentDate = new Date(startDate)\n\n    for (let i = 0; i < count; i++) {\n        currentDate.setDate(currentDate.getDate() + Math.floor(Math.random() * 14) + 1)\n        dates.push(new Date(currentDate))\n    }\n\n    return dates.sort((a, b) => a.getTime() - b.getTime())\n}\n\n// Helper function to generate markdown content\nfunction generateMarkdownContent(): string {\n    const features = [\n        'Authentication system',\n        'User dashboard',\n        'API endpoints',\n        'Search functionality',\n        'Export feature',\n        'Dark mode',\n        'Notification system',\n        'Analytics dashboard',\n        'User roles',\n        'Multi-factor authentication',\n        'Performance optimizations',\n        'Database indexing',\n        'Caching system',\n        'File upload system',\n        'Error tracking'\n    ]\n\n    const impacts = [\n        'Improves overall system security',\n        'Enhances user experience',\n        'Reduces server load',\n        'Optimizes database performance',\n        'Streamlines workflow',\n        'Increases reliability'\n    ]\n\n    const sections = [\n        {\n            title: '### Changes',\n            bullets: Math.floor(Math.random() * 3) + 2\n        },\n        {\n            title: '### Technical Details',\n            bullets: Math.floor(Math.random() * 2) + 1\n        },\n        {\n            title: '### Migration Guide',\n            bullets: Math.random() > 0.5 ? Math.floor(Math.random() * 2) + 1 : 0\n        }\n    ]\n\n    return sections\n        .map(section => {\n            if (section.bullets === 0) return ''\n            const bulletPoints = Array(section.bullets)\n                .fill(null)\n                .map(() => {\n                    const feature = faker.helpers.arrayElement(features)\n                    const impact = faker.helpers.arrayElement(impacts)\n                    return `- **${feature}**: ${faker.lorem.sentence()} ${impact}.`\n                })\n            return `${section.title}\\n\\n${bulletPoints.join('\\n')}`\n        })\n        .filter(Boolean)\n        .join('\\n\\n')\n}\n\nasync function main() {\n    // Clean the database\n    await prisma.changelogTag.deleteMany()\n    await prisma.changelogEntry.deleteMany()\n    await prisma.changelog.deleteMany()\n    await prisma.project.deleteMany()\n    await prisma.refreshToken.deleteMany()\n    await prisma.settings.deleteMany()\n    await prisma.user.deleteMany()\n    await prisma.invitationLink.deleteMany()\n\n    // Create admin user\n    const adminPassword = await hashPassword('password123')\n    const admin = await prisma.user.create({\n        data: {\n            email: 'admin@changerawr.com',\n            password: adminPassword,\n            name: 'Admin User',\n            role: Role.ADMIN,\n            settings: {\n                create: {\n                    theme: 'light'\n                }\n            }\n        }\n    })\n\n    // Create multiple staff users\n    const staffMembers = await Promise.all(\n        Array(3).fill(null).map(async (_, index) => {\n            const staffPassword = await hashPassword('password123')\n            return prisma.user.create({\n                data: {\n                    email: `staff${index + 1}@changerawr.com`,\n                    password: staffPassword,\n                    name: `Staff User ${index + 1}`,\n                    role: Role.STAFF,\n                    settings: {\n                        create: {\n                            theme: faker.helpers.arrayElement(['light', 'dark', 'system'])\n                        }\n                    }\n                }\n            })\n        })\n    )\n\n    // Create multiple invitation links\n    const invitationEmails = ['dev@changerawr.com', 'qa@changerawr.com', 'pm@changerawr.com']\n    await Promise.all(\n        invitationEmails.map(email => {\n            const expiryDate = new Date()\n            expiryDate.setDate(expiryDate.getDate() + faker.number.int({ min: 1, max: 7 }))\n\n            return prisma.invitationLink.create({\n                data: {\n                    token: faker.string.alphanumeric(32),\n                    email,\n                    role: Role.STAFF,\n                    createdBy: admin.id,\n                    expiresAt: expiryDate\n                }\n            })\n        })\n    )\n\n    // Create multiple projects\n    const projects = await Promise.all(\n        Array(3).fill(null).map((_, index) => {\n            return prisma.project.create({\n                data: {\n                    name: faker.helpers.arrayElement([\n                        'Mobile App',\n                        'Web Dashboard',\n                        'API Gateway',\n                        'Analytics Platform',\n                        'Admin Panel'\n                    ]),\n                    isPublic: faker.datatype.boolean(),\n                    allowAutoPublish: faker.datatype.boolean(),\n                    requireApproval: faker.datatype.boolean(),\n                    defaultTags: ['feature', 'bugfix', 'improvement', 'breaking'],\n                    changelog: {\n                        create: {}\n                    }\n                }\n            })\n        })\n    )\n\n    // Create tags\n    const tags = await Promise.all([\n        prisma.changelogTag.create({ data: { name: 'Feature' } }),\n        prisma.changelogTag.create({ data: { name: 'Bug Fix' } }),\n        prisma.changelogTag.create({ data: { name: 'Improvement' } }),\n        prisma.changelogTag.create({ data: { name: 'Breaking Change' } }),\n        prisma.changelogTag.create({ data: { name: 'Security' } }),\n        prisma.changelogTag.create({ data: { name: 'Performance' } }),\n        prisma.changelogTag.create({ data: { name: 'Documentation' } })\n    ])\n\n    // Generate versions and dates for each project\n    const entriesPerProject = 20\n    const projectVersions = projects.reduce((acc, project) => ({\n        ...acc,\n        [project.id]: generateVersionSequence(entriesPerProject)\n    }), {})\n\n    const projectDates = projects.reduce((acc, project) => ({\n        ...acc,\n        [project.id]: generateSequentialDates(entriesPerProject)\n    }), {})\n\n    // Create changelog entries for each project\n    const entries = await Promise.all(\n        projects.flatMap(project =>\n            Array(entriesPerProject).fill(null).map((_, index) => {\n                const isPublished = index < entriesPerProject - 3 // Keep last 3 unpublished\n                return prisma.changelogEntry.create({\n                    data: {\n                        title: faker.helpers.arrayElement([\n                            'Implemented new authentication system',\n                            'Enhanced security measures',\n                            'Improved system performance',\n                            'Updated user interface',\n                            'Added dark mode support',\n                            'Fixed critical issues',\n                            'Expanded API functionality',\n                            'Optimized search capabilities',\n                            'Introduced export features',\n                            'Revamped documentation'\n                        ]),\n                        content: generateMarkdownContent(),\n                        version: (projectVersions as { [key: string]: any })[project.id][index],\n                        publishedAt: isPublished ? (projectDates as { [key: string]: any })[project.id][index] : null,\n                        changelog: {\n                            connect: {\n                                projectId: project.id\n                            }\n                        },\n                        tags: {\n                            connect: Array(faker.number.int({ min: 1, max: 3 }))\n                                .fill(null)\n                                .map(() => ({\n                                    id: faker.helpers.arrayElement(tags).id\n                                }))\n                        }\n                    }\n                })\n            })\n        )\n    )\n\n    console.log('Seeding completed:', {\n        admin: { email: admin.email, role: admin.role },\n        staffCount: staffMembers.length,\n        invitationCount: invitationEmails.length,\n        projects: projects.map(p => ({ id: p.id, name: p.name })),\n        entriesCount: entries.length,\n        tagsCount: tags.length\n    })\n}\n\nmain()\n    .catch((e) => {\n        console.error(e)\n        process.exit(1)\n    })\n    .finally(async () => {\n        await prisma.$disconnect()\n    })"
  },
  {
    "path": "proxy.ts",
    "content": "import {NextResponse} from 'next/server'\nimport type {NextRequest} from 'next/server'\nimport {verifyAccessToken} from '@/lib/auth/tokens'\nimport {getAppDomain} from '@/lib/custom-domains/utils'\nimport {db} from '@/lib/db'\n\n// ─── IP Whitelist ────────────────────────────────────────────────────────────\n\ninterface IpConfig { enabled: boolean; whitelist: string[] }\nlet cachedIpConfig: IpConfig = { enabled: false, whitelist: [] }\nlet ipCacheExpiry = 0\nconst IP_CACHE_TTL_MS = 30_000\n\nasync function getIpConfig(baseUrl: string): Promise<IpConfig> {\n    const now = Date.now()\n    if (now < ipCacheExpiry) return cachedIpConfig\n    const secret = process.env.INTERNAL_API_SECRET\n    if (!secret) return { enabled: false, whitelist: [] }\n    try {\n        const res = await fetch(`${baseUrl}/api/internal/ip-config`, {\n            headers: { 'x-internal-secret': secret },\n            signal: AbortSignal.timeout(3000),\n        })\n        if (res.ok) {\n            cachedIpConfig = await res.json() as IpConfig\n            ipCacheExpiry = now + IP_CACHE_TTL_MS\n        }\n    } catch { /* fail open */ }\n    return cachedIpConfig\n}\n\nfunction getClientIp(req: NextRequest): string {\n    return req.headers.get('x-forwarded-for')?.split(',')[0].trim()\n        ?? req.headers.get('x-real-ip')\n        ?? '127.0.0.1'\n}\n\nfunction ipMatchesCidr(ip: string, cidr: string): boolean {\n    const entry = cidr.trim()\n    if (!entry) return false\n    if (!entry.includes('/')) return ip === entry\n    if (!ip.includes('.')) return false\n    const [network, prefixStr] = entry.split('/')\n    const prefix = parseInt(prefixStr, 10)\n    if (isNaN(prefix) || prefix < 0 || prefix > 32) return false\n    const ipToU32 = (s: string) => s.split('.').reduce((a, p) => (a * 256 + parseInt(p, 10)), 0) >>> 0\n    const mask = prefix === 0 ? 0 : (~0 << (32 - prefix)) >>> 0\n    return (ipToU32(ip) & mask) === (ipToU32(network) & mask)\n}\n\nconst IP_WHITELIST_PUBLIC_PREFIXES = [\n    '/api/internal/', '/api/health', '/api/check-setup', '/api/system/version',\n    '/api/config/', '/api/setup/', '/api/auth/', '/api/changelog/',\n    '/api/integrations/widget/', '/api/avatar/', '/api/analytics/track',\n    '/_next/', '/favicon.ico',\n]\n\nfunction isIpProtectedPath(pathname: string): boolean {\n    for (const p of IP_WHITELIST_PUBLIC_PREFIXES) {\n        if (pathname.startsWith(p)) return false\n    }\n    return pathname.startsWith('/dashboard') || pathname.startsWith('/api/')\n}\n\nconst ALWAYS_PUBLIC_PATHS = [\n    '/_next/',\n    '/favicon.ico',\n    '/public/',\n    '/static/',\n    '/_chrverify/',\n    '/changerawr-domain-verification/',\n    '/widget.css',\n    '/widget-bundle.js'\n]\n\n// Server action honeypot tracking\nconst attemptTracker = new Map<string, number>()\nconst FUCK_OFF_THRESHOLD = 5 // I love this variable, sorry professionalists!\n\n// Default allowed external domains for CDN resources\nconst DEFAULT_ALLOWED_EXTERNAL_DOMAINS = [\n    'cloudflareinsights.com',\n    'static.cloudflareinsights.com',\n    'cdnjs.cloudflare.com',\n    'unpkg.com',\n    'cdn.jsdelivr.net',\n    'fonts.googleapis.com',\n    'fonts.gstatic.com'\n]\n\n// Get additional allowed domains from environment variable\nfunction getAllowedExternalDomains(): string[] {\n    const envDomains = process.env.ALLOWED_EXTERNAL_DOMAINS || ''\n    const additionalDomains = envDomains.split(',').map(d => d.trim()).filter(Boolean)\n    return [...DEFAULT_ALLOWED_EXTERNAL_DOMAINS, ...additionalDomains]\n}\n\n// Domain security config cache (60 second TTL)\ninterface DomainSecurityConfig {\n    forceHttps: boolean\n    fetchedAt: number\n}\n\nconst domainSecurityCache = new Map<string, DomainSecurityConfig>()\nconst CACHE_TTL_MS = 60_000 // 60 seconds\n\nasync function getDomainSecurityConfig(hostname: string): Promise<DomainSecurityConfig | null> {\n    // Check cache first\n    const cached = domainSecurityCache.get(hostname)\n    if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {\n        return cached\n    }\n\n    try {\n        const domain = await db.customDomain.findUnique({\n            where: { domain: hostname },\n            select: {\n                forceHttps: true,\n            },\n        })\n\n        if (!domain) {\n            return null\n        }\n\n        const config: DomainSecurityConfig = {\n            forceHttps: domain.forceHttps,\n            fetchedAt: Date.now(),\n        }\n\n        domainSecurityCache.set(hostname, config)\n        return config\n    } catch (error) {\n        console.error('[proxy] Error fetching domain security config:', error)\n        return null\n    }\n}\n\nconst PUBLIC_API_PATHS = [\n    '/api/auth/',\n    '/api/setup/',\n    '/api/check-setup',\n    '/api/auth/oauth/',\n]\n\nconst AUTH_ROUTES = ['/login', '/register', '/setup']\n\nconst PUBLIC_CONTENT_PATHS = [\n    '/reset-password/',\n    '/changelog/',\n    '/unsubscribed',\n    '/experiments/',\n    '/forgot-password',\n    '/two-factor',\n    '/cli/auth',\n]\n\nfunction isAlwaysPublicPath(pathname: string): boolean {\n    return ALWAYS_PUBLIC_PATHS.some(path => pathname.startsWith(path)) ||\n        pathname.includes('.')\n}\n\nfunction isPublicApiPath(pathname: string): boolean {\n    return PUBLIC_API_PATHS.some(path => pathname.startsWith(path))\n}\n\nfunction isPublicContentPath(pathname: string): boolean {\n    return PUBLIC_CONTENT_PATHS.some(path => pathname.startsWith(path))\n}\n\nfunction isAllowedExternalDomain(hostname: string): boolean {\n    const allowedDomains = getAllowedExternalDomains()\n    return allowedDomains.some(domain =>\n        hostname === domain || hostname.endsWith(`.${domain}`)\n    )\n}\n\nfunction isCustomDomain(hostname: string): boolean {\n    try {\n        const appDomain = normalizeHostname(getAppDomain())\n\n        if (hostname.includes('localhost') || hostname.includes('127.0.0.1')) {\n            return false\n        }\n\n        return hostname !== appDomain && !hostname.endsWith(`.${appDomain}`)\n    } catch {\n        return false\n    }\n}\n\n\nfunction normalizeHostname(rawHost: string): string {\n    const firstHost = rawHost.split(',')[0]?.trim().toLowerCase() || ''\n    return firstHost.replace(/:\\d+$/, '')\n}\n\nfunction handleCustomDomain(request: NextRequest, hostname: string, pathname: string): NextResponse {\n    const url = request.nextUrl.clone()\n    url.pathname = `/changelog/custom-domain/${encodeURIComponent(hostname)}${pathname === '/' ? '' : pathname}`\n    return NextResponse.rewrite(url)\n}\n\nasync function isSetupComplete(): Promise<boolean> {\n    // Check environment variable first to avoid excessive API calls\n    // Set SETUP_COMPLETE=true in your .env after initial setup is done\n    if (process.env.SETUP_COMPLETE === 'true') {\n        return true\n    }\n\n    const headers = new Headers({'x-middleware-check': 'true'})\n    try {\n        const baseUrl = process.env.NEXT_PUBLIC_APP_URL\n        // Support both HTTP and HTTPS URLs\n        if (!baseUrl) return false\n\n        const response = await fetch(`${baseUrl}/api/check-setup`, {headers})\n        if (!response.ok) return false\n        const data = await response.json()\n        return !!data.isComplete\n    } catch {\n        return false\n    }\n}\n\nexport async function proxy(request: NextRequest) {\n    const {pathname} = request.nextUrl\n    const rawHost = request.headers.get('x-forwarded-host') || request.headers.get('host') || ''\n    const hostname = normalizeHostname(rawHost)\n\n    // 🍯 HONEYPOT: Catch server action exploit attempts\n    const nextAction = request.headers.get('next-action')\n    if (nextAction) {\n        const ip = request.headers.get('x-forwarded-for') ||\n                   request.headers.get('x-real-ip') ||\n                   'unknown'\n        const userAgent = request.headers.get('user-agent') || 'unknown'\n\n        // Track attempts per IP\n        const currentAttempts = (attemptTracker.get(ip) || 0) + 1\n        attemptTracker.set(ip, currentAttempts)\n\n        // Log the attempt\n        console.warn('🔒 [SECURITY] Server action exploit attempt:', {\n            action: nextAction,\n            ip,\n            userAgent,\n            attempts: currentAttempts,\n            path: pathname,\n            timestamp: new Date().toISOString(),\n        })\n\n        // If they've tried too many times, tell them to fuck off\n        if (currentAttempts >= FUCK_OFF_THRESHOLD) {\n            console.error(`🚫 [SECURITY] IP ${ip} exceeded attempt threshold (${currentAttempts} attempts)`)\n            return new NextResponse(\n                JSON.stringify({ error: 'Access denied. Stop trying to exploit this server, fuck off.' }),\n                { status: 403, headers: { 'content-type': 'application/json' } }\n            )\n        }\n\n        // Return a realistic unauthorized error\n        return new NextResponse(\n            JSON.stringify({ error: 'Unauthorized: Insufficient permissions' }),\n            { status: 401, headers: { 'content-type': 'application/json' } }\n        )\n    }\n\n    // ACME HTTP-01 challenge passthrough — MUST be first, before ANY redirects or auth checks.\n    // Let's Encrypt validates challenges over both HTTP and HTTPS.\n    if (pathname.startsWith('/.well-known/acme-challenge/')) {\n        console.log(`[proxy] 🔐 ACME challenge request: ${hostname}${pathname}`)\n        console.log(`[proxy]    Protocol: ${request.headers.get('x-forwarded-proto') || 'unknown'}`)\n        return NextResponse.next()\n    }\n\n    // IP whitelist check — only applies to dashboard + API paths, never custom domains\n    if (!isCustomDomain(hostname) && isIpProtectedPath(pathname)) {\n        const baseUrl = process.env.NEXT_PUBLIC_APP_URL ?? request.nextUrl.origin\n        const ipConfig = await getIpConfig(baseUrl)\n        if (ipConfig.enabled) {\n            const clientIp = getClientIp(request)\n            const allowed = ipConfig.whitelist.length === 0\n                || ipConfig.whitelist.some(entry => ipMatchesCidr(clientIp, entry))\n            if (!allowed) {\n                return new NextResponse('Access denied', {\n                    status: 403,\n                    headers: { 'Content-Type': 'text/plain' },\n                })\n            }\n        }\n    }\n\n    // Force HTTPS redirect for custom domains (production only)\n    if (isCustomDomain(hostname) && process.env.NODE_ENV === 'production') {\n        const securityConfig = await getDomainSecurityConfig(hostname)\n\n        if (securityConfig?.forceHttps) {\n            const proto = request.headers.get('x-forwarded-proto')\n\n            // Redirect HTTP to HTTPS with 308 (preserves POST method)\n            if (proto === 'http') {\n                const url = request.nextUrl.clone()\n                url.protocol = 'https:'\n                // Ensure we redirect to the external custom hostname without internal app ports.\n                url.host = hostname\n                url.port = ''\n                return NextResponse.redirect(url, 308)\n            }\n        }\n    }\n\n    // Allow requests to allowed external domains (CDNs, Cloudflare, etc.)\n    // This fixes CORS issues with external scripts and resources\n    if (isAllowedExternalDomain(hostname)) {\n        const response = NextResponse.next()\n        response.headers.set('Access-Control-Allow-Origin', '*')\n        response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')\n        response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')\n        return response\n    }\n\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    const isApiRequest = pathname.startsWith('/api/')\n\n    if (isCustomDomain(hostname) && pathname === '/rss.xml') {\n        const url = request.nextUrl.clone()\n        url.pathname = `/changelog/custom-domain/${encodeURIComponent(hostname)}/rss.xml`\n        return NextResponse.rewrite(url)\n    }\n\n    if (isAlwaysPublicPath(pathname)) {\n        return NextResponse.next()\n    }\n\n    if (isCustomDomain(hostname)) {\n        const encodedHostname = encodeURIComponent(hostname)\n\n        // Prevent rewrite loops when middleware sees an already-rewritten internal pathname.\n        if (pathname.startsWith(`/changelog/custom-domain/${encodedHostname}`)) {\n            return NextResponse.next()\n        }\n\n        // ACME challenges must pass through for ALL domains (already handled above, but double-check)\n        if (pathname.startsWith('/.well-known/acme-challenge/')) {\n            return NextResponse.next()\n        }\n\n        if (pathname.startsWith('/api/')) {\n            if (pathname.startsWith('/api/changelog/')) {\n                return NextResponse.next()\n            }\n            return new NextResponse(null, {status: 404})\n        }\n\n        if (pathname === '/rss.xml') {\n            const url = request.nextUrl.clone()\n            url.pathname = `/changelog/custom-domain/${encodedHostname}/rss.xml`\n            return NextResponse.rewrite(url)\n        }\n\n        return handleCustomDomain(request, hostname, pathname)\n    }\n\n    if (pathname === '/api/check-setup') {\n        return NextResponse.next()\n    }\n\n    if (isPublicApiPath(pathname)) {\n        return NextResponse.next()\n    }\n\n    if (pathname.startsWith('/api/')) {\n        return NextResponse.next()\n    }\n\n    if (isPublicContentPath(pathname)) {\n        return NextResponse.next()\n    }\n\n    if (pathname === '/setup') {\n        const setupComplete = await isSetupComplete()\n        if (setupComplete) {\n            return NextResponse.redirect(new URL('/login', request.url))\n        }\n        return NextResponse.next()\n    }\n\n    const setupComplete = await isSetupComplete()\n    if (!setupComplete) {\n        return NextResponse.redirect(new URL('/setup', request.url))\n    }\n\n    const accessToken = request.cookies.get('accessToken')?.value\n    const refreshToken = request.cookies.get('refreshToken')?.value\n\n    if (AUTH_ROUTES.some(route => pathname.startsWith(route))) {\n        if (accessToken) {\n            try {\n                const userId = await verifyAccessToken(accessToken)\n                if (userId) {\n                    return NextResponse.redirect(new URL('/dashboard', request.url))\n                }\n            } catch {\n            }\n        }\n        return NextResponse.next()\n    }\n\n    if (!accessToken) {\n        if (refreshToken) {\n            return NextResponse.next()\n        }\n        const url = new URL('/login', request.url)\n        url.searchParams.set('from', pathname)\n        return NextResponse.redirect(url)\n    }\n\n    try {\n        const userId = await verifyAccessToken(accessToken)\n        if (!userId) {\n            if (refreshToken) {\n                return NextResponse.next()\n            }\n            const url = new URL('/login', request.url)\n            url.searchParams.set('from', pathname)\n            return NextResponse.redirect(url)\n        }\n\n        const response = NextResponse.next()\n        response.headers.set('x-user-id', userId)\n        return response\n    } catch {\n        if (refreshToken) {\n            return NextResponse.next()\n        }\n        const url = new URL('/login', request.url)\n        url.searchParams.set('from', pathname)\n        return NextResponse.redirect(url)\n    }\n}\n\nexport const config = {\n    matcher: [\n        '/((?!_next/static|_next/image|favicon.ico).*)',\n    ],\n}\n"
  },
  {
    "path": "public/swagger.json",
    "content": "{\n  \"openapi\": \"3.0.0\",\n  \"info\": {\n    \"title\": \"Changerawr API Documentation\",\n    \"version\": \"1.0.5\",\n    \"description\": \"The official documentation for the Changerawr API. rawr\",\n    \"contact\": {\n      \"name\": \"API Support\",\n      \"url\": \"http://localhost:3000\"\n    }\n  },\n  \"servers\": [\n    {\n      \"url\": \"http://localhost:3000\",\n      \"description\": \"API Server\"\n    }\n  ],\n  \"paths\": {\n    \"/api/subscribers\": {\n      \"get\": {\n        \"tags\": [\n          \"Subscribers\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"List subscribers for a project with pagination and search functionality\"\n      },\n      \"post\": {\n        \"tags\": [\n          \"Subscribers\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Creates or updates a subscription for a user to receive email notifications for a project\"\n      }\n    },\n    \"/api/setup\": {\n      \"get\": {\n        \"tags\": [\n          \"Setup\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Get current setup progress including team invitations\"\n      }\n    },\n    \"/api/requests\": {\n      \"get\": {\n        \"tags\": [\n          \"Requests\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"requests\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"type\": {\n                            \"type\": \"string\"\n                          },\n                          \"status\": {\n                            \"type\": \"string\"\n                          },\n                          \"createdAt\": {\n                            \"type\": \"string\",\n                            \"format\": \"date-time\"\n                          },\n                          \"reviewedAt\": {\n                            \"type\": \"string\",\n                            \"format\": \"date-time\"\n                          },\n                          \"project\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"id\": {\n                                \"type\": \"string\"\n                              },\n                              \"name\": {\n                                \"type\": \"string\"\n                              }\n                            }\n                          },\n                          \"entry\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"id\": {\n                                \"type\": \"string\"\n                              },\n                              \"title\": {\n                                \"type\": \"string\"\n                              }\n                            }\n                          },\n                          \"tag\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"id\": {\n                                \"type\": \"string\"\n                              },\n                              \"name\": {\n                                \"type\": \"string\"\n                              }\n                            }\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - Please log in\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [\n          {\n            \"cookieAuth\": []\n          }\n        ],\n        \"description\": \"Retrieves changelog requests for the current user\"\n      }\n    },\n    \"/api/projects\": {\n      \"get\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"id\": {\n                        \"type\": \"string\"\n                      },\n                      \"name\": {\n                        \"type\": \"string\"\n                      },\n                      \"createdAt\": {\n                        \"type\": \"string\",\n                        \"format\": \"date-time\"\n                      },\n                      \"entryCount\": {\n                        \"type\": \"number\"\n                      },\n                      \"latestEntry\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"version\": {\n                            \"type\": \"string\"\n                          },\n                          \"createdAt\": {\n                            \"type\": \"string\",\n                            \"format\": \"date-time\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while fetching projects\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Fetches a list of projects, including their latest changelog entry and entry count\"\n      },\n      \"post\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"201\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"createdAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"changelog\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid input - Project name is required\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while creating the project\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Creates a new project and its associated changelog\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/health\": {\n      \"get\": {\n        \"tags\": [\n          \"Health\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"status\": {\n                      \"type\": \"string\"\n                    },\n                    \"timestamp\": {\n                      \"type\": \"string\"\n                    },\n                    \"services\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"database\": {\n                          \"type\": \"string\"\n                        },\n                        \"application\": {\n                          \"type\": \"string\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"503\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"status\": {\n                      \"type\": \"string\"\n                    },\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Health check endpoint to verify application readiness\"\n      }\n    },\n    \"/api/system/update-status\": {\n      \"get\": {\n        \"tags\": [\n          \"System\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"available\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"currentVersion\": {\n                      \"type\": \"string\"\n                    },\n                    \"latestVersion\": {\n                      \"type\": \"string\"\n                    },\n                    \"canAutoUpdate\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"easypanelConfigured\": {\n                      \"type\": \"boolean\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - Authentication required\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden - Admin access required\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal server error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [\n          {\n            \"cookieAuth\": []\n          }\n        ],\n        \"description\": \"Checks for available updates and Easypanel configuration status\"\n      }\n    },\n    \"/api/system/perform-update\": {\n      \"post\": {\n        \"tags\": [\n          \"System\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"message\": {\n                      \"type\": \"string\"\n                    },\n                    \"fromVersion\": {\n                      \"type\": \"string\"\n                    },\n                    \"toVersion\": {\n                      \"type\": \"string\"\n                    },\n                    \"imageUsed\": {\n                      \"type\": \"string\"\n                    },\n                    \"estimatedRestartTime\": {\n                      \"type\": \"number\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid request or version\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - Authentication required\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden - Admin access required\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal server error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"503\": {\n            \"description\": \"Easypanel not configured\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [\n          {\n            \"cookieAuth\": []\n          }\n        ],\n        \"description\": \"Automatically updates the application using Easypanel API\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/subscribers/{subscriberId}\": {\n      \"delete\": {\n        \"tags\": [\n          \"Subscribers\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Removes a subscriber from a project or deletes them entirely\"\n      },\n      \"patch\": {\n        \"tags\": [\n          \"Subscribers\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Updates a subscriber's information or subscription preferences\"\n      }\n    },\n    \"/api/subscribers/generate-mock\": {\n      \"post\": {\n        \"tags\": [\n          \"Subscribers\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Development-only endpoint to generate mock subscribers for testing\"\n      }\n    },\n    \"/api/setup/status\": {\n      \"get\": {\n        \"tags\": [\n          \"Setup\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"isComplete\": {\n                      \"type\": \"boolean\",\n                      \"description\": \"Indicates whether the setup has been completed\"\n                    },\n                    \"setupState\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"adminCreated\": {\n                          \"type\": \"boolean\"\n                        },\n                        \"systemConfigured\": {\n                          \"type\": \"boolean\"\n                        },\n                        \"oauthConfigured\": {\n                          \"type\": \"boolean\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while checking setup status\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Checks if the initial setup has been completed and provides setup state information\"\n      }\n    },\n    \"/api/setup/settings\": {\n      \"get\": {\n        \"tags\": [\n          \"Setup\"\n        ],\n        \"summary\": \"Method Not Allowed\",\n        \"responses\": {\n          \"405\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\",\n                      \"example\": \"Method not allowed\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"This endpoint only accepts POST requests for system configuration\"\n      },\n      \"post\": {\n        \"tags\": [\n          \"Setup\"\n        ],\n        \"summary\": \"Initialize System Settings\",\n        \"responses\": {\n          \"201\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"message\": {\n                      \"type\": \"string\",\n                      \"example\": \"System settings configured successfully\"\n                    },\n                    \"config\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"integer\",\n                          \"description\": \"System configuration ID\",\n                          \"example\": 1\n                        },\n                        \"defaultInvitationExpiry\": {\n                          \"type\": \"integer\",\n                          \"description\": \"Configured invitation expiry in days\",\n                          \"example\": 7\n                        },\n                        \"requireApprovalForChangelogs\": {\n                          \"type\": \"boolean\",\n                          \"description\": \"Whether changelog approval is required\",\n                          \"example\": true\n                        },\n                        \"maxChangelogEntriesPerProject\": {\n                          \"type\": \"integer\",\n                          \"description\": \"Maximum changelog entries per project\",\n                          \"example\": 100\n                        },\n                        \"enableAnalytics\": {\n                          \"type\": \"boolean\",\n                          \"description\": \"Analytics status\",\n                          \"example\": true\n                        },\n                        \"enableNotifications\": {\n                          \"type\": \"boolean\",\n                          \"description\": \"Notifications status\",\n                          \"example\": true\n                        }\n                      },\n                      \"required\": [\n                        \"id\",\n                        \"defaultInvitationExpiry\",\n                        \"requireApprovalForChangelogs\",\n                        \"maxChangelogEntriesPerProject\",\n                        \"enableAnalytics\",\n                        \"enableNotifications\"\n                      ]\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\", \\\"example\\\": \\\"Validation failed\\\" }, \\\"details\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"path\\\": { \\\"type\\\": \\\"string\\\", \\\"example\\\": \\\"defaultInvitationExpiry\\\" }, \\\"message\\\": { \\\"type\\\": \\\"string\\\", \\\"example\\\": \\\"Number must be between 1 and 30\\\" } } } } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\",\n                      \"example\": \"Validation failed\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\",\n                            \"example\": \"defaultInvitationExpiry\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\",\n                            \"example\": \"Number must be between 1 and 30\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\", \\\"example\\\": \\\"An unexpected error occurred during configuration\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\",\n                      \"example\": \"An unexpected error occurred during configuration\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Sets up the initial system configuration. This endpoint can only be called once, before any system settings are configured.\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/setup/oauth\": {\n      \"post\": {\n        \"tags\": [\n          \"Setup\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Sets up an OAuth provider during initial system setup\"\n      }\n    },\n    \"/api/setup/invitations\": {\n      \"get\": {\n        \"tags\": [\n          \"Setup\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"invitations\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"email\": {\n                            \"type\": \"string\"\n                          },\n                          \"role\": {\n                            \"type\": \"string\"\n                          },\n                          \"expiresAt\": {\n                            \"type\": \"string\",\n                            \"format\": \"date-time\"\n                          },\n                          \"createdAt\": {\n                            \"type\": \"string\",\n                            \"format\": \"date-time\"\n                          },\n                          \"usedAt\": {\n                            \"type\": \"string\",\n                            \"format\": \"date-time\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"List all active invitations (for admin use)\"\n      },\n      \"post\": {\n        \"tags\": [\n          \"Setup\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"201\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"token\": {\n                      \"type\": \"string\"\n                    },\n                    \"email\": {\n                      \"type\": \"string\"\n                    },\n                    \"role\": {\n                      \"type\": \"string\"\n                    },\n                    \"expiresAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"createdAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Validation failed - Invalid input data\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"Invitation already exists for this email\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Create a new invitation link for team setup\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/setup/admin\": {\n      \"get\": {\n        \"tags\": [\n          \"Setup\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"405\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\",\n                      \"example\": \"Method not allowed\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Method not allowed - Admin user setup endpoint only accepts POST requests\"\n      },\n      \"post\": {\n        \"tags\": [\n          \"Setup\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"message\": {\n                      \"type\": \"string\",\n                      \"example\": \"Admin account created successfully\"\n                    },\n                    \"user\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"email\": {\n                          \"type\": \"string\"\n                        },\n                        \"name\": {\n                          \"type\": \"string\"\n                        },\n                        \"role\": {\n                          \"type\": \"string\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Validation failed - Invalid input data\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Setup already completed - Cannot run setup more than once\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred during setup\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Creates a new admin user for the application\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/projects/{projectId}\": {\n      \"get\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"createdAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"changelog\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"version\": {\n                            \"type\": \"string\"\n                          },\n                          \"createdAt\": {\n                            \"type\": \"string\",\n                            \"format\": \"date-time\"\n                          },\n                          \"tags\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                              \"type\": \"string\"\n                            }\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Project not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while fetching the project\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Fetches a specific project, including its changelog and tags\"\n      },\n      \"delete\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"204\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"description\": \"\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Project not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while deleting the project\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Deletes a specific project\"\n      },\n      \"patch\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"createdAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"changelog\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"version\": {\n                            \"type\": \"string\"\n                          },\n                          \"createdAt\": {\n                            \"type\": \"string\",\n                            \"format\": \"date-time\"\n                          },\n                          \"tags\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                              \"type\": \"string\"\n                            }\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Project not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while updating the project\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Updates a specific project\"\n      }\n    },\n    \"/api/dashboard/stats\": {\n      \"get\": {\n        \"tags\": [\n          \"Dashboard\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"projectPreviews\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"name\": {\n                            \"type\": \"string\"\n                          },\n                          \"lastUpdated\": {\n                            \"type\": \"string\",\n                            \"format\": \"date-time\"\n                          },\n                          \"changelogCount\": {\n                            \"type\": \"number\"\n                          }\n                        }\n                      }\n                    },\n                    \"totalProjects\": {\n                      \"type\": \"number\"\n                    },\n                    \"totalChangelogs\": {\n                      \"type\": \"number\"\n                    },\n                    \"recentActivity\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"type\": {\n                            \"type\": \"string\",\n                            \"enum\": [\n                              \"CHANGELOG_ENTRY\"\n                            ]\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          },\n                          \"timestamp\": {\n                            \"type\": \"string\",\n                            \"format\": \"date-time\"\n                          },\n                          \"projectId\": {\n                            \"type\": \"string\"\n                          },\n                          \"projectName\": {\n                            \"type\": \"string\"\n                          },\n                          \"updatedAt\": {\n                            \"type\": \"string\",\n                            \"format\": \"date-time\"\n                          }\n                        }\n                      }\n                    },\n                    \"adminStats\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"pendingApprovals\": {\n                          \"type\": \"number\"\n                        }\n                      },\n                      \"additionalProperties\": false\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - User not authenticated\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while fetching dashboard statistics\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Retrieves dashboard statistics for the authenticated user, including recent projects, total project and changelog counts, and recent activity\"\n      }\n    },\n    \"/api/changelog/subscribe\": {\n      \"post\": {\n        \"tags\": [\n          \"Changelog\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Creates a subscription for a user to receive email notifications for changelog updates\"\n      }\n    },\n    \"/api/changelog/requests\": {\n      \"get\": {\n        \"tags\": [\n          \"Changelog\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"id\": {\n                        \"type\": \"string\"\n                      },\n                      \"type\": {\n                        \"type\": \"string\",\n                        \"enum\": [\n                          \"DELETE_PROJECT\",\n                          \"DELETE_TAG\",\n                          \"DELETE_ENTRY\",\n                          \"ALLOW_PUBLISH\",\n                          \"ALLOW_SCHEDULE\"\n                        ]\n                      },\n                      \"status\": {\n                        \"type\": \"string\",\n                        \"enum\": [\n                          \"PENDING\",\n                          \"APPROVED\",\n                          \"REJECTED\"\n                        ]\n                      },\n                      \"staffId\": {\n                        \"type\": \"string\"\n                      },\n                      \"targetId\": {\n                        \"type\": \"string\"\n                      },\n                      \"staff\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"email\": {\n                            \"type\": \"string\"\n                          },\n                          \"name\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      },\n                      \"project\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"name\": {\n                            \"type\": \"string\"\n                          },\n                          \"defaultTags\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                              \"type\": \"string\"\n                            }\n                          }\n                        }\n                      },\n                      \"ChangelogEntry\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"title\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      },\n                      \"ChangelogTag\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"name\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - User not authenticated\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while fetching requests\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Retrieves pending changelog requests for authenticated user\"\n      },\n      \"post\": {\n        \"tags\": [\n          \"Changelog\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"201\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"type\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"DELETE_PROJECT\",\n                        \"DELETE_TAG\",\n                        \"DELETE_ENTRY\",\n                        \"ALLOW_PUBLISH\",\n                        \"ALLOW_SCHEDULE\"\n                      ]\n                    },\n                    \"status\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"PENDING\"\n                      ]\n                    },\n                    \"staffId\": {\n                      \"type\": \"string\"\n                    },\n                    \"staff\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"email\": {\n                          \"type\": \"string\"\n                        },\n                        \"name\": {\n                          \"type\": \"string\"\n                        }\n                      }\n                    },\n                    \"project\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"name\": {\n                          \"type\": \"string\"\n                        },\n                        \"defaultTags\": {\n                          \"type\": \"array\",\n                          \"items\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request - Invalid request data\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - User not authenticated\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden - Only staff members can create requests\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while creating the request\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Creates a new pending changelog request\"\n      }\n    },\n    \"/api/analytics/track\": {\n      \"post\": {\n        \"tags\": [\n          \"Analytics\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid request body\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Failed to track view\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Track a changelog view (cookieless, GDPR compliant)\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/auth/validate\": {\n      \"get\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"valid\": {\n                      \"type\": \"boolean\",\n                      \"example\": true\n                    },\n                    \"user\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"email\": {\n                          \"type\": \"string\"\n                        },\n                        \"name\": {\n                          \"type\": \"string\"\n                        },\n                        \"role\": {\n                          \"type\": \"string\"\n                        }\n                      }\n                    },\n                    \"expires_in\": {\n                      \"type\": \"number\",\n                      \"description\": \"Seconds until token expires\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Invalid or expired token\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"User not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal server error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"description\": \"Validate JWT access token and return user information\"\n      }\n    },\n    \"/api/auth/settings\": {\n      \"get\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"userId\": {\n                      \"type\": \"string\"\n                    },\n                    \"theme\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"light\",\n                        \"dark\"\n                      ]\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"enableNotifications\": {\n                      \"type\": \"boolean\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - User not authenticated\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while fetching settings\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Retrieves or creates the user's settings\"\n      },\n      \"patch\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"userId\": {\n                      \"type\": \"string\"\n                    },\n                    \"theme\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"light\",\n                        \"dark\"\n                      ]\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"enableNotifications\": {\n                      \"type\": \"boolean\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid request data\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - User not authenticated\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while updating settings\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Updates the user's settings\"\n      }\n    },\n    \"/api/auth/register\": {\n      \"post\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\",\n                      \"example\": true\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid or expired invitation\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"User already exists\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred during registration\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Registers a new user using an invitation token\"\n      }\n    },\n    \"/api/auth/refresh\": {\n      \"post\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"user\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"name\": {\n                          \"type\": \"string\"\n                        },\n                        \"email\": {\n                          \"type\": \"string\"\n                        },\n                        \"role\": {\n                          \"type\": \"string\"\n                        },\n                        \"createdAt\": {\n                          \"type\": \"string\",\n                          \"format\": \"date-time\"\n                        }\n                      }\n                    },\n                    \"accessToken\": {\n                      \"type\": \"string\"\n                    },\n                    \"refreshToken\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid or expired refresh token\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - No refresh token provided\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while refreshing the token\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Refreshes the access token by providing a valid refresh token\"\n      }\n    },\n    \"/api/auth/preview\": {\n      \"post\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Creates a preview of a user's information\"\n      }\n    },\n    \"/api/auth/me\": {\n      \"get\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"email\": {\n                      \"type\": \"string\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"role\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - Invalid or expired access token\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"User not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred during authentication\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Verifies the access token and retrieves the user's data\"\n      }\n    },\n    \"/api/auth/logout\": {\n      \"post\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\",\n                      \"example\": true\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while logging out\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Clears the access and refresh tokens, and optionally invalidates the refresh token in the database\"\n      }\n    },\n    \"/api/auth/login\": {\n      \"get\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"405\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\",\n                      \"example\": \"Method not allowed\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Method not allowed - Login endpoint only accepts POST requests\"\n      },\n      \"post\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"user\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"email\": {\n                          \"type\": \"string\"\n                        },\n                        \"name\": {\n                          \"type\": \"string\"\n                        },\n                        \"role\": {\n                          \"type\": \"string\"\n                        },\n                        \"lastLoginAt\": {\n                          \"type\": \"string\",\n                          \"format\": \"date-time\"\n                        }\n                      }\n                    },\n                    \"accessToken\": {\n                      \"type\": \"string\"\n                    },\n                    \"refreshToken\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Validation failed - Invalid input format\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - Invalid credentials\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\",\n                      \"example\": \"password_breached\"\n                    },\n                    \"message\": {\n                      \"type\": \"string\"\n                    },\n                    \"breachCount\": {\n                      \"type\": \"number\"\n                    },\n                    \"resetUrl\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal server error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [\n          {\n            \"cookieAuth\": []\n          }\n        ],\n        \"description\": \"Authenticates user with email and password, checks for password breach\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/auth/forgot-password\": {\n      \"post\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"message\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\", \\\"example\\\": \\\"Invalid email format\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\",\n                      \"example\": \"Invalid email format\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\", \\\"example\\\": \\\"Failed to initiate password reset\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\",\n                      \"example\": \"Failed to initiate password reset\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Initiates the password reset process by sending a reset email\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/auth/connections\": {\n      \"get\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"connections\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"providerId\": {\n                            \"type\": \"string\"\n                          },\n                          \"provider\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"id\": {\n                                \"type\": \"string\"\n                              },\n                              \"name\": {\n                                \"type\": \"string\"\n                              },\n                              \"enabled\": {\n                                \"type\": \"boolean\"\n                              },\n                              \"isDefault\": {\n                                \"type\": \"boolean\"\n                              }\n                            }\n                          },\n                          \"providerUserId\": {\n                            \"type\": \"string\"\n                          },\n                          \"expiresAt\": {\n                            \"type\": \"string\"\n                          },\n                          \"createdAt\": {\n                            \"type\": \"string\"\n                          },\n                          \"updatedAt\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    },\n                    \"allProviders\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"name\": {\n                            \"type\": \"string\"\n                          },\n                          \"enabled\": {\n                            \"type\": \"boolean\"\n                          },\n                          \"isDefault\": {\n                            \"type\": \"boolean\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - User not authenticated\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal server error - Database query failed\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [\n          {\n            \"cookieAuth\": []\n          }\n        ],\n        \"description\": \"Retrieves user's OAuth connections and available providers\"\n      }\n    },\n    \"/api/auth/change-password\": {\n      \"post\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid input - Validation error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - Invalid current password\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while changing the password\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Changes the password for a logged-in user\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/ai/settings\": {\n      \"get\": {\n        \"tags\": [\n          \"Ai\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"enableAIAssistant\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"aiApiKey\": {\n                      \"type\": \"string\"\n                    },\n                    \"aiDefaultModel\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Retrieve the system AI settings with encrypted API key for the editor\"\n      }\n    },\n    \"/api/ai/decrypt\": {\n      \"post\": {\n        \"tags\": [\n          \"Ai\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"decryptedKey\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Decrypt an encrypted API key on the server side\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/admin/users\": {\n      \"get\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"id\": {\n                        \"type\": \"string\"\n                      },\n                      \"email\": {\n                        \"type\": \"string\"\n                      },\n                      \"name\": {\n                        \"type\": \"string\"\n                      },\n                      \"role\": {\n                        \"type\": \"string\"\n                      },\n                      \"createdAt\": {\n                        \"type\": \"string\",\n                        \"format\": \"date-time\"\n                      },\n                      \"lastLoginAt\": {\n                        \"type\": \"string\",\n                        \"format\": \"date-time\"\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - User not authorized to access this endpoint\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while fetching users\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Retrieves a list of users with their email, name, role, creation date, and last login date. Only admins have access to this endpoint.\"\n      },\n      \"post\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"message\": {\n                      \"type\": \"string\"\n                    },\n                    \"invitation\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"email\": {\n                          \"type\": \"string\"\n                        },\n                        \"role\": {\n                          \"type\": \"string\"\n                        },\n                        \"expiresAt\": {\n                          \"type\": \"string\",\n                          \"format\": \"date-time\"\n                        },\n                        \"url\": {\n                          \"type\": \"string\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"An active invitation already exists for this email\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while creating the invitation link\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Creates an invitation link for user registration. Only admins have access to this endpoint.\"\n      }\n    },\n    \"/api/admin/dashboard\": {\n      \"get\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"userCount\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"total\": {\n                          \"type\": \"number\"\n                        },\n                        \"admins\": {\n                          \"type\": \"number\"\n                        },\n                        \"staff\": {\n                          \"type\": \"number\"\n                        }\n                      }\n                    },\n                    \"invitations\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"total\": {\n                          \"type\": \"number\"\n                        },\n                        \"pending\": {\n                          \"type\": \"number\"\n                        }\n                      }\n                    },\n                    \"changelog\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"totalEntries\": {\n                          \"type\": \"number\"\n                        },\n                        \"entriesThisMonth\": {\n                          \"type\": \"number\"\n                        }\n                      }\n                    },\n                    \"systemHealth\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"databaseConnected\": {\n                          \"type\": \"boolean\"\n                        },\n                        \"lastDataSync\": {\n                          \"type\": \"string\",\n                          \"format\": \"date-time\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User does not have 'ADMIN' role\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while fetching dashboard data\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Validates that the authenticated user has 'ADMIN' role\"\n      }\n    },\n    \"/api/admin/config\": {\n      \"get\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Fetches the system configuration for the authenticated user\"\n      },\n      \"patch\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Updates the system configuration for the authenticated user\"\n      }\n    },\n    \"/api/admin/audit-logs\": {\n      \"get\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"logs\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"action\": {\n                            \"type\": \"string\"\n                          },\n                          \"performer\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"name\": {\n                                \"type\": \"string\"\n                              },\n                              \"email\": {\n                                \"type\": \"string\"\n                              },\n                              \"isDeleted\": {\n                                \"type\": \"boolean\"\n                              }\n                            }\n                          },\n                          \"performer_email\": {\n                            \"type\": \"string\"\n                          },\n                          \"target\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"name\": {\n                                \"type\": \"string\"\n                              },\n                              \"email\": {\n                                \"type\": \"string\"\n                              },\n                              \"isDeleted\": {\n                                \"type\": \"boolean\"\n                              }\n                            }\n                          },\n                          \"target_email\": {\n                            \"type\": \"string\"\n                          },\n                          \"details\": {\n                            \"type\": \"object\"\n                          },\n                          \"createdAt\": {\n                            \"type\": \"string\",\n                            \"format\": \"date-time\"\n                          }\n                        }\n                      }\n                    },\n                    \"total\": {\n                      \"type\": \"number\"\n                    },\n                    \"pages\": {\n                      \"type\": \"number\"\n                    },\n                    \"nextCursor\": {\n                      \"type\": \"string\"\n                    },\n                    \"hasMore\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"currentCount\": {\n                      \"type\": \"number\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User does not have 'ADMIN' role\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while fetching audit logs\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Fetches audit logs based on filters and pagination or chunking\"\n      }\n    },\n    \"/api/admin/api-keys\": {\n      \"get\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"id\": {\n                        \"type\": \"string\"\n                      },\n                      \"name\": {\n                        \"type\": \"string\"\n                      },\n                      \"key\": {\n                        \"type\": \"string\"\n                      },\n                      \"lastUsed\": {\n                        \"type\": \"string\",\n                        \"format\": \"date-time\"\n                      },\n                      \"createdAt\": {\n                        \"type\": \"string\",\n                        \"format\": \"date-time\"\n                      },\n                      \"expiresAt\": {\n                        \"type\": \"string\",\n                        \"format\": \"date-time\"\n                      },\n                      \"isRevoked\": {\n                        \"type\": \"boolean\"\n                      },\n                      \"permissions\": {\n                        \"type\": \"array\",\n                        \"items\": {\n                          \"type\": \"string\"\n                        }\n                      },\n                      \"user\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"email\": {\n                            \"type\": \"string\"\n                          },\n                          \"name\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User does not have 'ADMIN' role\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while fetching API keys\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Fetches a list of API keys for the authenticated user\"\n      },\n      \"post\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"201\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"key\": {\n                      \"type\": \"string\"\n                    },\n                    \"lastUsed\": {\n                      \"type\": \"null\"\n                    },\n                    \"createdAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"expiresAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"isRevoked\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"permissions\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"string\"\n                      }\n                    },\n                    \"user\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"email\": {\n                          \"type\": \"string\"\n                        },\n                        \"name\": {\n                          \"type\": \"string\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid request data\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User does not have 'ADMIN' role\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while creating an API key\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Creates a new API key for the authenticated user\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/admin/analytics\": {\n      \"get\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"data\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"totalViews\": {\n                          \"type\": \"number\"\n                        },\n                        \"uniqueVisitors\": {\n                          \"type\": \"number\"\n                        },\n                        \"topCountries\": {\n                          \"type\": \"array\",\n                          \"items\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"country\": {\n                                \"type\": \"string\"\n                              },\n                              \"count\": {\n                                \"type\": \"number\"\n                              }\n                            }\n                          }\n                        },\n                        \"dailyViews\": {\n                          \"type\": \"array\",\n                          \"items\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"date\": {\n                                \"type\": \"string\"\n                              },\n                              \"views\": {\n                                \"type\": \"number\"\n                              },\n                              \"uniqueVisitors\": {\n                                \"type\": \"number\"\n                              }\n                            }\n                          }\n                        },\n                        \"topProjects\": {\n                          \"type\": \"array\",\n                          \"items\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"projectId\": {\n                                \"type\": \"string\"\n                              },\n                              \"projectName\": {\n                                \"type\": \"string\"\n                              },\n                              \"views\": {\n                                \"type\": \"number\"\n                              },\n                              \"uniqueVisitors\": {\n                                \"type\": \"number\"\n                              }\n                            }\n                          }\n                        },\n                        \"topEntries\": {\n                          \"type\": \"array\",\n                          \"items\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"entryId\": {\n                                \"type\": \"string\"\n                              },\n                              \"title\": {\n                                \"type\": \"string\"\n                              },\n                              \"views\": {\n                                \"type\": \"number\"\n                              },\n                              \"uniqueVisitors\": {\n                                \"type\": \"number\"\n                              }\n                            }\n                          }\n                        },\n                        \"topReferrers\": {\n                          \"type\": \"array\",\n                          \"items\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"referrer\": {\n                                \"type\": \"string\"\n                              },\n                              \"count\": {\n                                \"type\": \"number\"\n                              }\n                            }\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid parameters\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User does not have admin role\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal server error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Get system-wide analytics data (admin only)\"\n      }\n    },\n    \"/api/admin/ai-settings\": {\n      \"get\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"enableAIAssistant\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"aiApiKey\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"aiDefaultModel\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - You must be logged in as an admin\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden - Insufficient permissions\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [\n          {\n            \"cookieAuth\": []\n          }\n        ],\n        \"description\": \"Retrieve the current AI assistant settings\"\n      },\n      \"post\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"message\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request - Invalid request body\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - You must be logged in as an admin\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden - Insufficient permissions\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [\n          {\n            \"cookieAuth\": []\n          }\n        ],\n        \"description\": \"Update AI assistant settings\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/system/easypanel/status\": {\n      \"get\": {\n        \"tags\": [\n          \"System\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"configured\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"connected\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"config\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"projectId\": {\n                          \"type\": \"string\"\n                        },\n                        \"serviceId\": {\n                          \"type\": \"string\"\n                        },\n                        \"panelUrl\": {\n                          \"type\": \"string\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - Authentication required\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden - Admin access required\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal server error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [\n          {\n            \"cookieAuth\": []\n          }\n        ],\n        \"description\": \"Checks Easypanel configuration and tests connection\"\n      }\n    },\n    \"/api/setup/oauth/debug\": {\n      \"get\": {\n        \"tags\": [\n          \"Setup\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"description\": \"Debug information about OAuth setup\"\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Debug endpoint to check OAuth auto setup configuration\"\n      },\n      \"post\": {\n        \"tags\": [\n          \"Setup\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Test the auto setup schema validation\"\n      }\n    },\n    \"/api/setup/oauth/auto\": {\n      \"get\": {\n        \"tags\": [\n          \"Setup\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"available\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"connected\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"serverInfo\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"serverUrl\": {\n                          \"type\": \"string\"\n                        },\n                        \"hasApiKey\": {\n                          \"type\": \"boolean\"\n                        },\n                        \"isConfigured\": {\n                          \"type\": \"boolean\"\n                        }\n                      }\n                    },\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal server error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Check auto OAuth setup availability and status\"\n      },\n      \"post\": {\n        \"tags\": [\n          \"Setup\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"client\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"name\": {\n                          \"type\": \"string\"\n                        },\n                        \"clientId\": {\n                          \"type\": \"string\"\n                        },\n                        \"redirectUri\": {\n                          \"type\": \"string\"\n                        }\n                      }\n                    },\n                    \"message\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Validation failed or setup error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal server error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"503\": {\n            \"description\": \"Auto OAuth setup not configured\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Automatically create and configure OAuth client with remote server\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/projects/{projectId}/versions\": {\n      \"get\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"number\"\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid request data\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User does not have 'ADMIN' role\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while fetching the versions\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Fetches the versions of a given project\"\n      }\n    },\n    \"/api/projects/{projectId}/settings\": {\n      \"get\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"isPublic\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"allowAutoPublish\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"requireApproval\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"defaultTags\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"string\"\n                      }\n                    },\n                    \"updatedAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid request data\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User does not have 'ADMIN' role\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Project not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while fetching the project settings\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Retrieves the project settings for a given project\"\n      },\n      \"patch\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"isPublic\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"allowAutoPublish\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"requireApproval\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"defaultTags\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"string\"\n                      }\n                    },\n                    \"updatedAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid request data\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User does not have 'ADMIN' role\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Project not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while updating the project settings\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Updates the project settings for a given project\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/projects/{projectId}/changelog\": {\n      \"get\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"entries\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"title\": {\n                            \"type\": \"string\"\n                          },\n                          \"content\": {\n                            \"type\": \"string\"\n                          },\n                          \"version\": {\n                            \"type\": \"number\"\n                          },\n                          \"createdAt\": {\n                            \"type\": \"string\",\n                            \"format\": \"date-time\"\n                          },\n                          \"tags\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                              \"type\": \"object\",\n                              \"properties\": {\n                                \"id\": {\n                                  \"type\": \"string\"\n                                },\n                                \"name\": {\n                                  \"type\": \"string\"\n                                }\n                              }\n                            }\n                          }\n                        }\n                      }\n                    },\n                    \"pagination\": {\n                      \"type\": \"object\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User does not have 'ADMIN' role\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Project not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while fetching the changelog entries\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Fetches the changelog entries for a given project\"\n      },\n      \"post\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"201\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"title\": {\n                      \"type\": \"string\"\n                    },\n                    \"content\": {\n                      \"type\": \"string\"\n                    },\n                    \"version\": {\n                      \"type\": \"number\"\n                    },\n                    \"createdAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"tags\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"name\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User does not have 'ADMIN' role\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Project not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while creating the changelog entry\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Creates a new changelog entry for a given project\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/projects/{projectId}/catch-up\": {\n      \"get\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"fromDate\": {\n                      \"type\": \"string\",\n                      \"description\": \"The date we started looking from\",\n                      \"format\": \"date-time\"\n                    },\n                    \"fromVersion\": {\n                      \"type\": \"string\",\n                      \"description\": \"Version we started from (if applicable)\"\n                    },\n                    \"toVersion\": {\n                      \"type\": \"string\",\n                      \"description\": \"Latest version in the range\"\n                    },\n                    \"totalEntries\": {\n                      \"type\": \"integer\",\n                      \"description\": \"Total number of changelog entries found\"\n                    },\n                    \"summary\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"features\": {\n                          \"type\": \"integer\"\n                        },\n                        \"fixes\": {\n                          \"type\": \"integer\"\n                        },\n                        \"other\": {\n                          \"type\": \"integer\"\n                        }\n                      }\n                    },\n                    \"entries\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"title\": {\n                            \"type\": \"string\"\n                          },\n                          \"content\": {\n                            \"type\": \"string\"\n                          },\n                          \"version\": {\n                            \"type\": \"string\"\n                          },\n                          \"publishedAt\": {\n                            \"type\": \"string\",\n                            \"format\": \"date-time\"\n                          },\n                          \"tags\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                              \"type\": \"object\",\n                              \"properties\": {\n                                \"id\": {\n                                  \"type\": \"string\"\n                                },\n                                \"name\": {\n                                  \"type\": \"string\"\n                                },\n                                \"color\": {\n                                  \"type\": \"string\"\n                                }\n                              }\n                            }\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid query parameters\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Project not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal server error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Get changelog catch-up data for a project since a specified date/version\"\n      }\n    },\n    \"/api/projects/{projectId}/analytics\": {\n      \"get\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"data\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"totalViews\": {\n                          \"type\": \"number\"\n                        },\n                        \"uniqueVisitors\": {\n                          \"type\": \"number\"\n                        },\n                        \"topCountries\": {\n                          \"type\": \"array\",\n                          \"items\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"country\": {\n                                \"type\": \"string\"\n                              },\n                              \"count\": {\n                                \"type\": \"number\"\n                              }\n                            }\n                          }\n                        },\n                        \"dailyViews\": {\n                          \"type\": \"array\",\n                          \"items\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"date\": {\n                                \"type\": \"string\"\n                              },\n                              \"views\": {\n                                \"type\": \"number\"\n                              },\n                              \"uniqueVisitors\": {\n                                \"type\": \"number\"\n                              }\n                            }\n                          }\n                        },\n                        \"topEntries\": {\n                          \"type\": \"array\",\n                          \"items\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"entryId\": {\n                                \"type\": \"string\"\n                              },\n                              \"title\": {\n                                \"type\": \"string\"\n                              },\n                              \"views\": {\n                                \"type\": \"number\"\n                              },\n                              \"uniqueVisitors\": {\n                                \"type\": \"number\"\n                              }\n                            }\n                          }\n                        },\n                        \"topReferrers\": {\n                          \"type\": \"array\",\n                          \"items\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"referrer\": {\n                                \"type\": \"string\"\n                              },\n                              \"count\": {\n                                \"type\": \"number\"\n                              }\n                            }\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid parameters\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User not authorized to access project\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Project not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal server error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Get analytics data for a specific project\"\n      }\n    },\n    \"/api/integrations/widget/{projectId}\": {\n      \"get\": {\n        \"tags\": [\n          \"Integrations\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"description\": \"Widget loader script content\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Project is not public\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Project not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Gets the widget loader script for a public project\"\n      }\n    },\n    \"/api/changelog/{projectId}/entries\": {\n      \"get\": {\n        \"tags\": [\n          \"Changelog\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"project\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"name\": {\n                          \"type\": \"string\"\n                        }\n                      }\n                    },\n                    \"items\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"title\": {\n                            \"type\": \"string\"\n                          },\n                          \"content\": {\n                            \"type\": \"string\"\n                          },\n                          \"version\": {\n                            \"type\": \"string\"\n                          },\n                          \"publishedAt\": {\n                            \"type\": \"string\",\n                            \"format\": \"date-time\"\n                          },\n                          \"tags\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                              \"type\": \"object\",\n                              \"properties\": {\n                                \"id\": {\n                                  \"type\": \"string\"\n                                },\n                                \"name\": {\n                                  \"type\": \"string\"\n                                },\n                                \"color\": {\n                                  \"type\": \"string\"\n                                }\n                              }\n                            }\n                          }\n                        }\n                      }\n                    },\n                    \"nextCursor\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid request data\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User does not have 'ADMIN' role or the project is not public\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Project not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while fetching the changelog entries\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Fetches the changelog entries for a given public project, with filtering, searching and pagination\"\n      }\n    },\n    \"/api/changelog/requests/{requestId}\": {\n      \"patch\": {\n        \"tags\": [\n          \"Changelog\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"title\": {\n                      \"type\": \"string\"\n                    },\n                    \"content\": {\n                      \"type\": \"string\"\n                    },\n                    \"version\": {\n                      \"type\": \"number\"\n                    },\n                    \"status\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"PENDING\",\n                        \"APPROVED\",\n                        \"REJECTED\"\n                      ]\n                    },\n                    \"createdAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"changelogId\": {\n                      \"type\": \"string\"\n                    },\n                    \"adminId\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid request data\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User does not have 'ADMIN' role\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Request not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while processing the request\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Updates the status of a changelog request\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/changelog/entries/{entryId}\": {\n      \"get\": {\n        \"tags\": [\n          \"Changelog\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"project\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"name\": {\n                          \"type\": \"string\"\n                        },\n                        \"description\": {\n                          \"type\": \"string\"\n                        }\n                      }\n                    },\n                    \"entry\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"title\": {\n                          \"type\": \"string\"\n                        },\n                        \"content\": {\n                          \"type\": \"string\"\n                        },\n                        \"excerpt\": {\n                          \"type\": \"string\"\n                        },\n                        \"version\": {\n                          \"type\": \"string\"\n                        },\n                        \"publishedAt\": {\n                          \"type\": \"string\"\n                        },\n                        \"createdAt\": {\n                          \"type\": \"string\"\n                        },\n                        \"updatedAt\": {\n                          \"type\": \"string\"\n                        },\n                        \"changelogId\": {\n                          \"type\": \"string\"\n                        },\n                        \"tags\": {\n                          \"type\": \"array\",\n                          \"items\": {\n                            \"type\": \"object\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Entry not found or not published\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Failed to fetch entry\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Fetches a single public changelog entry by its ID\"\n      },\n      \"put\": {\n        \"tags\": [\n          \"Changelog\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"data\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"title\": {\n                          \"type\": \"string\"\n                        },\n                        \"content\": {\n                          \"type\": \"string\"\n                        },\n                        \"version\": {\n                          \"type\": \"string\"\n                        },\n                        \"tags\": {\n                          \"type\": \"array\",\n                          \"items\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"id\": {\n                                \"type\": \"string\"\n                              },\n                              \"name\": {\n                                \"type\": \"string\"\n                              }\n                            }\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\", \\\"example\\\": \\\"Invalid request data\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\",\n                      \"example\": \"Invalid request data\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\", \\\"example\\\": \\\"Unauthorized\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\",\n                      \"example\": \"Unauthorized\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\", \\\"example\\\": \\\"Forbidden\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\",\n                      \"example\": \"Forbidden\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\", \\\"example\\\": \\\"Changelog entry not found\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\",\n                      \"example\": \"Changelog entry not found\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\", \\\"example\\\": \\\"Internal server error\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\",\n                      \"example\": \"Internal server error\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Updates a changelog entry by its ID. Only authorized users with role 'STAFF' or 'ADMIN' can update an entry.\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"tags\": [\n          \"Changelog\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"data\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\", \\\"example\\\": \\\"Invalid request data\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\",\n                      \"example\": \"Invalid request data\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\", \\\"example\\\": \\\"Unauthorized\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\",\n                      \"example\": \"Unauthorized\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\", \\\"example\\\": \\\"Forbidden\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\",\n                      \"example\": \"Forbidden\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\", \\\"example\\\": \\\"Changelog entry not found\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\",\n                      \"example\": \"Changelog entry not found\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\", \\\"example\\\": \\\"Internal server error\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\",\n                      \"example\": \"Internal server error\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Deletes a changelog entry by its ID. Only authorized users with role 'ADMIN' can delete an entry.\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/auth/reset-password/{token}\": {\n      \"get\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"valid\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"email\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"valid\\\": { \\\"type\\\": \\\"boolean\\\", \\\"example\\\": false }, \\\"message\\\": { \\\"type\\\": \\\"string\\\", \\\"example\\\": \\\"Invalid or expired reset token\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"valid\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"message\": {\n                      \"type\": \"string\",\n                      \"example\": \"Invalid or expired reset token\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Validates a password reset token\"\n      },\n      \"post\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"message\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Resets a user's password using a valid token\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/auth/reset-password/request\": {\n      \"post\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"message\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - User not authenticated\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while initiating password reset\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Initiates a password reset for the currently logged-in user\"\n      }\n    },\n    \"/api/auth/oauth/providers\": {\n      \"get\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"providers\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"name\": {\n                            \"type\": \"string\"\n                          },\n                          \"urlName\": {\n                            \"type\": \"string\"\n                          },\n                          \"isDefault\": {\n                            \"type\": \"boolean\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while fetching providers\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Retrieves a list of available OAuth providers\"\n      }\n    },\n    \"/api/auth/invitation/{token}\": {\n      \"get\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"email\": {\n                      \"type\": \"string\"\n                    },\n                    \"role\": {\n                      \"type\": \"string\"\n                    },\n                    \"expiresAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"message\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"message\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"message\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"message\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"message\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"message\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Validates an invitation token and returns the associated user email, role, and expiration date.\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/auth/cli/token\": {\n      \"post\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"access_token\": {\n                      \"type\": \"string\",\n                      \"description\": \"JWT access token\"\n                    },\n                    \"refresh_token\": {\n                      \"type\": \"string\",\n                      \"description\": \"JWT refresh token\"\n                    },\n                    \"expires_in\": {\n                      \"type\": \"number\",\n                      \"description\": \"Access token expiration time in seconds\"\n                    },\n                    \"token_type\": {\n                      \"type\": \"string\",\n                      \"example\": \"Bearer\"\n                    },\n                    \"user\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"email\": {\n                          \"type\": \"string\"\n                        },\n                        \"name\": {\n                          \"type\": \"string\"\n                        },\n                        \"role\": {\n                          \"type\": \"string\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"description\": \"- Invalid request or expired code\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"description\": \"- Internal server error\"\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Exchange CLI authorization code for JWT tokens\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/auth/cli/refresh\": {\n      \"post\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"access_token\": {\n                      \"type\": \"string\",\n                      \"description\": \"New JWT access token\"\n                    },\n                    \"refresh_token\": {\n                      \"type\": \"string\",\n                      \"description\": \"New JWT refresh token\"\n                    },\n                    \"expires_in\": {\n                      \"type\": \"number\",\n                      \"description\": \"Access token expiration time in seconds\"\n                    },\n                    \"token_type\": {\n                      \"type\": \"string\",\n                      \"example\": \"Bearer\"\n                    },\n                    \"user\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"email\": {\n                          \"type\": \"string\"\n                        },\n                        \"name\": {\n                          \"type\": \"string\"\n                        },\n                        \"role\": {\n                          \"type\": \"string\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"description\": \"- Invalid refresh token\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"description\": \"- Expired or revoked refresh token\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"description\": \"- Internal server error\"\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Refresh CLI access token using refresh token\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/auth/cli/generate\": {\n      \"post\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"code\": {\n                      \"type\": \"string\",\n                      \"description\": \"Temporary authorization code\"\n                    },\n                    \"expires\": {\n                      \"type\": \"number\",\n                      \"description\": \"Expiration timestamp in milliseconds\"\n                    },\n                    \"expiresAt\": {\n                      \"type\": \"string\",\n                      \"description\": \"Expiration date in ISO format\",\n                      \"format\": \"date-time\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid callback URL\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"User not authenticated\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal server error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [\n          {\n            \"cookieAuth\": []\n          }\n        ],\n        \"description\": \"Generate a temporary authorization code for CLI authentication\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/admin/users/{userId}\": {\n      \"delete\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"message\": {\n                      \"type\": \"string\"\n                    },\n                    \"preservedData\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"auditLogs\": {\n                          \"type\": \"number\"\n                        },\n                        \"changelogRequests\": {\n                          \"type\": \"number\"\n                        },\n                        \"apiKeys\": {\n                          \"type\": \"number\"\n                        },\n                        \"invitations\": {\n                          \"type\": \"number\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Cannot delete own account or last admin\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - Only admins can delete users\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"User not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Failed to delete user\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Safely deletes a user account by setting their references to NULL in related data. This preserves data integrity while removing the user from the system. Only admins can perform this action.\"\n      },\n      \"patch\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"message\": {\n                      \"type\": \"string\"\n                    },\n                    \"user\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"email\": {\n                          \"type\": \"string\"\n                        },\n                        \"name\": {\n                          \"type\": \"string\"\n                        },\n                        \"role\": {\n                          \"type\": \"string\"\n                        },\n                        \"createdAt\": {\n                          \"type\": \"string\",\n                          \"format\": \"date-time\"\n                        },\n                        \"lastLoginAt\": {\n                          \"type\": \"string\",\n                          \"format\": \"date-time\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid request data or cannot modify own role\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - Only admins can update users\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"User not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Failed to update user\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Updates a user's name and/or role. Only admins can perform this action.\"\n      }\n    },\n    \"/api/admin/users/invitations\": {\n      \"get\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"id\": {\n                        \"type\": \"string\"\n                      },\n                      \"token\": {\n                        \"type\": \"string\"\n                      },\n                      \"email\": {\n                        \"type\": \"string\"\n                      },\n                      \"role\": {\n                        \"type\": \"string\"\n                      },\n                      \"expiresAt\": {\n                        \"type\": \"string\",\n                        \"format\": \"date-time\"\n                      },\n                      \"usedAt\": {\n                        \"type\": \"string\",\n                        \"format\": \"date-time\"\n                      },\n                      \"createdAt\": {\n                        \"type\": \"string\",\n                        \"format\": \"date-time\"\n                      },\n                      \"updatedAt\": {\n                        \"type\": \"string\",\n                        \"format\": \"date-time\"\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Retrieves a list of all invitation links in descending order by creation date. Requires admin permissions.\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/admin/oauth/providers\": {\n      \"get\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"providers\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"name\": {\n                            \"type\": \"string\"\n                          },\n                          \"clientId\": {\n                            \"type\": \"string\"\n                          },\n                          \"clientSecret\": {\n                            \"type\": \"string\"\n                          },\n                          \"authorizationUrl\": {\n                            \"type\": \"string\"\n                          },\n                          \"tokenUrl\": {\n                            \"type\": \"string\"\n                          },\n                          \"userInfoUrl\": {\n                            \"type\": \"string\"\n                          },\n                          \"callbackUrl\": {\n                            \"type\": \"string\"\n                          },\n                          \"scopes\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"enabled\": {\n                            \"type\": \"boolean\"\n                          },\n                          \"isDefault\": {\n                            \"type\": \"boolean\"\n                          },\n                          \"createdAt\": {\n                            \"type\": \"string\",\n                            \"format\": \"date-time\"\n                          },\n                          \"updatedAt\": {\n                            \"type\": \"string\",\n                            \"format\": \"date-time\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User not authorized to access this endpoint\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while fetching providers\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Retrieves all OAuth providers\"\n      },\n      \"post\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"201\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"enabled\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"isDefault\": {\n                      \"type\": \"boolean\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid input - Validation error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User not authorized to access this endpoint\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while creating provider\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Creates a new OAuth provider with support for custom URLs\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/admin/config/system-email\": {\n      \"get\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"enablePasswordReset\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"smtpHost\": {\n                      \"type\": \"string\"\n                    },\n                    \"smtpPort\": {\n                      \"type\": \"number\"\n                    },\n                    \"smtpUser\": {\n                      \"type\": \"string\"\n                    },\n                    \"smtpSecure\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"systemEmail\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User does not have 'ADMIN' role\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while fetching system email configuration\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Retrieves the system email configuration\"\n      },\n      \"post\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"message\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid input - Validation error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User does not have 'ADMIN' role\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while updating system email configuration\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Updates the system email configuration\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      },\n      \"patch\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"message\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid input - Validation error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User does not have 'ADMIN' role\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while testing system email configuration\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Tests the system email configuration by sending a test email\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/admin/audit-logs/actions\": {\n      \"get\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"actions\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"action\": {\n                            \"type\": \"string\"\n                          },\n                          \"count\": {\n                            \"type\": \"number\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User does not have 'ADMIN' role\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while fetching actions\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Fetches all unique audit log actions for filter dropdown\"\n      }\n    },\n    \"/api/admin/api-keys/{keyId}\": {\n      \"get\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"lastUsed\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"createdAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"expiresAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"isRevoked\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"permissions\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"string\"\n                      }\n                    },\n                    \"user\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"email\": {\n                          \"type\": \"string\"\n                        },\n                        \"name\": {\n                          \"type\": \"string\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Returns the details of an API key, including its name, last used date, created date, expiration date, revocation status, and permissions. Requires admin permissions.\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"object\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": \\\"Unauthorized\\\" } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"object\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": \\\"API key not found\\\" } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"object\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Revokes an API key, making it unable to be used for authentication. Requires admin permissions.\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      },\n      \"patch\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"lastUsed\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"createdAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"expiresAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"isRevoked\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"permissions\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"string\"\n                      }\n                    },\n                    \"user\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"email\": {\n                          \"type\": \"string\"\n                        },\n                        \"name\": {\n                          \"type\": \"string\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" }, \\\"details\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"message\\\": { \\\"type\\\": \\\"string\\\" }, \\\"path\\\": { \\\"type\\\": \\\"string\\\" } } } } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"message\": {\n                            \"type\": \"string\"\n                          },\n                          \"path\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Updates an API key's name, revocation status, or expiration date. Requires admin permissions.\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/admin/ai-settings/test-key\": {\n      \"post\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"valid\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"message\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request - Invalid request body\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - You must be logged in as an admin\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden - Insufficient permissions\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [\n          {\n            \"cookieAuth\": []\n          }\n        ],\n        \"description\": \"Test the validity of a Secton AI API key\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/projects/{projectId}/integrations/github\": {\n      \"get\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Get GitHub integration settings for a project\"\n      },\n      \"post\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Create or update GitHub integration\"\n      },\n      \"delete\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Remove GitHub integration\"\n      }\n    },\n    \"/api/projects/{projectId}/integrations/email\": {\n      \"get\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Retrieves the email configuration for a project\"\n      },\n      \"post\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Creates or updates email configuration for a project\"\n      }\n    },\n    \"/api/projects/{projectId}/cli/unlink\": {\n      \"post\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"data\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"success\": {\n                          \"type\": \"boolean\"\n                        },\n                        \"message\": {\n                          \"type\": \"string\"\n                        },\n                        \"unlinkedAt\": {\n                          \"type\": \"string\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid request data\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Project not found or not linked\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"description\": \"Unlink a project from its Git repository\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/projects/{projectId}/cli/sync\": {\n      \"post\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"data\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"success\": {\n                          \"type\": \"boolean\"\n                        },\n                        \"processed\": {\n                          \"type\": \"number\"\n                        },\n                        \"skipped\": {\n                          \"type\": \"number\"\n                        },\n                        \"errors\": {\n                          \"type\": \"array\",\n                          \"items\": {\n                            \"type\": \"string\"\n                          }\n                        },\n                        \"warnings\": {\n                          \"type\": \"array\",\n                          \"items\": {\n                            \"type\": \"string\"\n                          }\n                        },\n                        \"newSyncHash\": {\n                          \"type\": \"string\"\n                        },\n                        \"syncedAt\": {\n                          \"type\": \"string\"\n                        },\n                        \"nextSyncRecommendedAt\": {\n                          \"type\": \"string\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid request data\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Project not found or not linked\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"description\": \"Sync commits from CLI to Changerawr project\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/projects/{projectId}/cli/link\": {\n      \"post\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"data\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"success\": {\n                          \"type\": \"boolean\"\n                        },\n                        \"message\": {\n                          \"type\": \"string\"\n                        },\n                        \"linkId\": {\n                          \"type\": \"string\"\n                        },\n                        \"linkedAt\": {\n                          \"type\": \"string\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid request data\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Project not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"Project already linked\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"description\": \"Link a project to a Git repository for CLI integration\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/projects/{projectId}/changelog/{entryId}\": {\n      \"get\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"title\": {\n                      \"type\": \"string\"\n                    },\n                    \"content\": {\n                      \"type\": \"string\"\n                    },\n                    \"version\": {\n                      \"type\": \"number\"\n                    },\n                    \"tags\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"name\": {\n                            \"type\": \"string\"\n                          },\n                          \"color\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    },\n                    \"createdAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"updatedAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"projectId\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Returns the details of a changelog entry by its ID, including its title, content, version, tags, and creation/update timestamps. Requires user authentication and permission to view the project.\",\n        \"parameters\": [\n          {\n            \"name\": \"projectId\",\n            \"in\": \"query\",\n            \"description\": \"- The ID of the project the entry belongs to.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"name\": \"entryId\",\n            \"in\": \"query\",\n            \"description\": \"- The ID of the changelog entry to retrieve.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ]\n      },\n      \"put\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"title\": {\n                      \"type\": \"string\"\n                    },\n                    \"content\": {\n                      \"type\": \"string\"\n                    },\n                    \"version\": {\n                      \"type\": \"number\"\n                    },\n                    \"tags\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"name\": {\n                            \"type\": \"string\"\n                          },\n                          \"color\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    },\n                    \"createdAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"updatedAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"projectId\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" }, \\\"details\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"message\\\": { \\\"type\\\": \\\"string\\\" }, \\\"path\\\": { \\\"type\\\": \\\"string\\\" } } } } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"message\": {\n                            \"type\": \"string\"\n                          },\n                          \"path\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Updates the title, content, version, and tags of a changelog entry by its ID. Requires user authentication and permission to edit the project.\",\n        \"parameters\": [\n          {\n            \"name\": \"projectId\",\n            \"in\": \"query\",\n            \"description\": \"- The ID of the project the entry belongs to.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"name\": \"entryId\",\n            \"in\": \"query\",\n            \"description\": \"- The ID of the changelog entry to update.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ]\n      },\n      \"delete\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"title\": {\n                      \"type\": \"string\"\n                    },\n                    \"content\": {\n                      \"type\": \"string\"\n                    },\n                    \"version\": {\n                      \"type\": \"number\"\n                    },\n                    \"projectId\": {\n                      \"type\": \"string\"\n                    },\n                    \"createdAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"updatedAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"202\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"message\": {\n                      \"type\": \"string\"\n                    },\n                    \"request\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"type\": {\n                          \"type\": \"string\",\n                          \"enum\": [\n                            \"DELETE_ENTRY\"\n                          ]\n                        },\n                        \"status\": {\n                          \"type\": \"string\",\n                          \"enum\": [\n                            \"PENDING\"\n                          ]\n                        },\n                        \"staffId\": {\n                          \"type\": \"string\"\n                        },\n                        \"projectId\": {\n                          \"type\": \"string\"\n                        },\n                        \"changelogEntryId\": {\n                          \"type\": \"string\"\n                        },\n                        \"createdAt\": {\n                          \"type\": \"string\",\n                          \"format\": \"date-time\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Deletes a changelog entry if the user is an admin, or creates a deletion request if the user is staff. Requires user authentication and appropriate permissions.\",\n        \"parameters\": [\n          {\n            \"name\": \"projectId\",\n            \"in\": \"query\",\n            \"description\": \"- The ID of the project the entry belongs to.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"name\": \"entryId\",\n            \"in\": \"query\",\n            \"description\": \"- The ID of the changelog entry to delete.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ]\n      },\n      \"patch\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"title\": {\n                      \"type\": \"string\"\n                    },\n                    \"content\": {\n                      \"type\": \"string\"\n                    },\n                    \"version\": {\n                      \"type\": \"number\"\n                    },\n                    \"tags\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"name\": {\n                            \"type\": \"string\"\n                          },\n                          \"color\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    },\n                    \"createdAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"updatedAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    },\n                    \"projectId\": {\n                      \"type\": \"string\"\n                    },\n                    \"publishedAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" }, \\\"details\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"message\\\": { \\\"type\\\": \\\"string\\\" }, \\\"path\\\": { \\\"type\\\": \\\"string\\\" } } } } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"message\": {\n                            \"type\": \"string\"\n                          },\n                          \"path\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Updates the status (published/unpublished) of a changelog entry by its ID. Requires user authentication and permission to edit the project.\",\n        \"parameters\": [\n          {\n            \"name\": \"projectId\",\n            \"in\": \"query\",\n            \"description\": \"- The ID of the project the entry belongs to.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"name\": \"entryId\",\n            \"in\": \"query\",\n            \"description\": \"- The ID of the changelog entry to update.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ]\n      }\n    },\n    \"/api/projects/{projectId}/changelog/tags\": {\n      \"get\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"tags\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"name\": {\n                            \"type\": \"string\"\n                          },\n                          \"color\": {\n                            \"type\": \"string\"\n                          },\n                          \"_count\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"entries\": {\n                                \"type\": \"number\"\n                              }\n                            }\n                          }\n                        }\n                      }\n                    },\n                    \"pagination\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"page\": {\n                          \"type\": \"number\"\n                        },\n                        \"limit\": {\n                          \"type\": \"number\"\n                        },\n                        \"totalCount\": {\n                          \"type\": \"number\"\n                        },\n                        \"totalPages\": {\n                          \"type\": \"number\"\n                        },\n                        \"hasMore\": {\n                          \"type\": \"boolean\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Project not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Failed to fetch tags\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Fetches a list of tags specifically for a project's changelog along with pagination metadata. Users must be authenticated to access this endpoint.\",\n        \"parameters\": [\n          {\n            \"name\": \"projectId\",\n            \"in\": \"query\",\n            \"description\": \"- Project ID from the URL params\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ]\n      },\n      \"post\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"201\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"color\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Validation failed - Invalid input format\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - Please log in\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden - Insufficient permissions\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Project not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"Tag already exists\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Server error - Failed to create tag\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [\n          {\n            \"cookieAuth\": []\n          }\n        ],\n        \"description\": \"Creates a new tag for a project's changelog\",\n        \"parameters\": [\n          {\n            \"name\": \"projectId\",\n            \"in\": \"query\",\n            \"description\": \"- Project ID from the URL params\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"message\": {\n                      \"type\": \"string\"\n                    },\n                    \"entriesAffected\": {\n                      \"type\": \"number\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Missing tagId parameter\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Tag or project not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Server error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Deletes a tag and removes it from all entries in the project\",\n        \"parameters\": [\n          {\n            \"name\": \"projectId\",\n            \"in\": \"query\",\n            \"description\": \"- Project ID from the URL params\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ]\n      },\n      \"patch\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"color\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Validation failed\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Tag or project not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"Tag name already exists\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Server error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Updates an existing tag's properties\",\n        \"parameters\": [\n          {\n            \"name\": \"projectId\",\n            \"in\": \"query\",\n            \"description\": \"- Project ID from the URL params\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/projects/{projectId}/catch-up/ai-summary\": {\n      \"post\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"summary\": {\n                      \"type\": \"string\",\n                      \"description\": \"AI-generated narrative summary\"\n                    },\n                    \"highlights\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"string\"\n                      },\n                      \"description\": \"Key highlights extracted from the changes\"\n                    },\n                    \"readingTime\": {\n                      \"type\": \"number\",\n                      \"description\": \"Estimated reading time in minutes\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid request data\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Project not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"AI not configured or generation failed\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Generate an AI-powered catch-up summary for changelog entries\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/projects/{projectId}/analytics/export\": {\n      \"get\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"description\": \"CSV or JSON file download\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Project not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Export failed\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Export project analytics data\"\n      }\n    },\n    \"/api/integrations/widget/{projectId}/{widgetId}\": {\n      \"get\": {\n        \"tags\": [\n          \"Integrations\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Get widget - serves loader script (public) or JSON (auth required based on Accept header)\"\n      },\n      \"post\": {\n        \"tags\": [\n          \"Integrations\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Create a new widget (auth required)\"\n      },\n      \"put\": {\n        \"tags\": [\n          \"Integrations\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Update a widget (auth required)\"\n      },\n      \"delete\": {\n        \"tags\": [\n          \"Integrations\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Delete a widget (auth required)\"\n      }\n    },\n    \"/api/integrations/widget/{projectId}/list\": {\n      \"get\": {\n        \"tags\": [\n          \"Integrations\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"List all widgets for a project (auth required)\"\n      }\n    },\n    \"/api/changelog/{projectId}/entries/all\": {\n      \"get\": {\n        \"tags\": [\n          \"Changelog\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Fetches the changelog entries with FULL CONTENT for a given public project\"\n      }\n    },\n    \"/api/auth/oauth/callback/{providerName}\": {\n      \"get\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"302\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"description\": \"Redirect to dashboard or specified redirect URL\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid request\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Authentication failed\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Handles the OAuth callback and completes authentication\",\n        \"parameters\": [\n          {\n            \"name\": \"providerName\",\n            \"in\": \"query\",\n            \"description\": \"- The name of the OAuth provider\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ]\n      }\n    },\n    \"/api/auth/oauth/authorize/{providerName}\": {\n      \"get\": {\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"302\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"description\": \"Redirect to OAuth provider\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Provider name is required\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Provider not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Redirects to OAuth provider authorization URL\",\n        \"parameters\": [\n          {\n            \"name\": \"providerName\",\n            \"in\": \"query\",\n            \"description\": \"- The name of the OAuth provider\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ]\n      }\n    },\n    \"/api/admin/users/{userId}/role\": {\n      \"patch\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"message\": {\n                      \"type\": \"string\"\n                    },\n                    \"user\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"email\": {\n                          \"type\": \"string\"\n                        },\n                        \"name\": {\n                          \"type\": \"string\"\n                        },\n                        \"role\": {\n                          \"type\": \"string\"\n                        },\n                        \"createdAt\": {\n                          \"type\": \"string\",\n                          \"format\": \"date-time\"\n                        },\n                        \"lastLoginAt\": {\n                          \"type\": \"string\",\n                          \"format\": \"date-time\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" }, \\\"details\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"message\\\": { \\\"type\\\": \\\"string\\\" }, \\\"path\\\": { \\\"type\\\": \\\"string\\\" } } } } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"message\": {\n                            \"type\": \"string\"\n                          },\n                          \"path\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Updates a user's role to either 'ADMIN' or 'STAFF'. Only admins can perform this action. Requires user authentication.\"\n      }\n    },\n    \"/api/admin/users/invitations/{id}\": {\n      \"delete\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"message\": {\n                      \"type\": \"string\"\n                    },\n                    \"invitation\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"email\": {\n                          \"type\": \"string\"\n                        },\n                        \"role\": {\n                          \"type\": \"string\"\n                        },\n                        \"expiresAt\": {\n                          \"type\": \"string\",\n                          \"format\": \"date-time\"\n                        },\n                        \"usedAt\": {\n                          \"type\": \"string\",\n                          \"format\": \"date-time\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Revokes an invitation by marking it as used. Only admins have the permission to revoke invitations.\"\n      }\n    },\n    \"/api/admin/oauth/providers/{id}\": {\n      \"delete\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User not authorized to access this endpoint\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Provider not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while deleting provider\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Deletes an OAuth provider\",\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"query\",\n            \"description\": \"- The ID of the OAuth provider to delete\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ]\n      },\n      \"patch\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"enabled\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"isDefault\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"updatedAt\": {\n                      \"type\": \"string\",\n                      \"format\": \"date-time\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Invalid input - Validation error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Unauthorized - User not authorized to access this endpoint\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Provider not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"An unexpected error occurred while updating provider\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Updates an existing OAuth provider with support for custom URLs\",\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"query\",\n            \"description\": \"- The ID of the OAuth provider to update\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/projects/{projectId}/integrations/github/test\": {\n      \"post\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Test GitHub connection with provided credentials\"\n      }\n    },\n    \"/api/projects/{projectId}/integrations/github/tags\": {\n      \"get\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Get available tags and releases from GitHub repository\"\n      }\n    },\n    \"/api/projects/{projectId}/integrations/github/generate\": {\n      \"post\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Generate changelog content from GitHub commits with optional AI analysis\"\n      }\n    },\n    \"/api/projects/{projectId}/integrations/email/test\": {\n      \"post\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Tests SMTP connection and sends a test email\"\n      }\n    },\n    \"/api/projects/{projectId}/integrations/email/send\": {\n      \"post\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {},\n        \"security\": [],\n        \"description\": \"Sends a changelog email to specified recipients or subscribers\"\n      }\n    },\n    \"/api/projects/{projectId}/cli/sync/status\": {\n      \"get\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"data\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"success\": {\n                          \"type\": \"boolean\"\n                        },\n                        \"lastSync\": {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"syncHash\": {\n                              \"type\": \"string\"\n                            },\n                            \"syncedAt\": {\n                              \"type\": \"string\"\n                            },\n                            \"commitCount\": {\n                              \"type\": \"number\"\n                            },\n                            \"branch\": {\n                              \"type\": \"string\"\n                            }\n                          }\n                        },\n                        \"pendingCommits\": {\n                          \"type\": \"number\"\n                        },\n                        \"totalCommits\": {\n                          \"type\": \"number\"\n                        },\n                        \"repositoryInfo\": {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"url\": {\n                              \"type\": \"string\"\n                            },\n                            \"branch\": {\n                              \"type\": \"string\"\n                            },\n                            \"lastCommitHash\": {\n                              \"type\": \"string\"\n                            },\n                            \"linkedAt\": {\n                              \"type\": \"string\"\n                            }\n                          }\n                        },\n                        \"syncSettings\": {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"autoSync\": {\n                              \"type\": \"boolean\"\n                            },\n                            \"lastSyncInterval\": {\n                              \"type\": \"number\"\n                            },\n                            \"maxCommitsPerSync\": {\n                              \"type\": \"number\"\n                            }\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Project not found or not linked\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"description\": \"Get sync status for a project\"\n      }\n    },\n    \"/api/projects/{projectId}/changelog/{entryId}/schedule\": {\n      \"get\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"entry\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"title\": {\n                          \"type\": \"string\"\n                        },\n                        \"scheduledAt\": {\n                          \"type\": \"string\",\n                          \"format\": \"date-time\"\n                        },\n                        \"publishedAt\": {\n                          \"type\": \"string\",\n                          \"format\": \"date-time\"\n                        }\n                      }\n                    },\n                    \"jobs\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"type\": {\n                            \"type\": \"string\"\n                          },\n                          \"scheduledAt\": {\n                            \"type\": \"string\",\n                            \"format\": \"date-time\"\n                          },\n                          \"status\": {\n                            \"type\": \"string\"\n                          },\n                          \"errorMessage\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Retrieves information about scheduled jobs for a specific changelog entry\"\n      },\n      \"post\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"message\": {\n                      \"type\": \"string\"\n                    },\n                    \"entry\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"title\": {\n                          \"type\": \"string\"\n                        },\n                        \"scheduledAt\": {\n                          \"type\": \"string\",\n                          \"format\": \"date-time\"\n                        },\n                        \"publishedAt\": {\n                          \"type\": \"string\",\n                          \"format\": \"date-time\"\n                        }\n                      }\n                    },\n                    \"jobId\": {\n                      \"type\": \"string\",\n                      \"description\": \"ID of the scheduled job (when scheduling)\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"{ \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"error\\\": { \\\"type\\\": \\\"string\\\" }, \\\"details\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"message\\\": { \\\"type\\\": \\\"string\\\" }, \\\"path\\\": { \\\"type\\\": \\\"string\\\" } } } } } }\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"message\": {\n                            \"type\": \"string\"\n                          },\n                          \"path\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - User not authenticated\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden - User lacks permission to schedule entries\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Not Found - Changelog entry not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"Conflict - Entry already published or scheduling conflict\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Schedules a changelog entry to be automatically published at a specified time. Only admins and staff with appropriate permissions can schedule entries.\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/projects/{projectId}/changelog/tags/{tagId}\": {\n      \"get\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"color\": {\n                      \"type\": \"string\"\n                    },\n                    \"_count\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"entries\": {\n                          \"type\": \"number\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Tag not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Failed to fetch tag\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Get a specific tag by ID with usage statistics\",\n        \"parameters\": [\n          {\n            \"name\": \"projectId\",\n            \"in\": \"query\",\n            \"description\": \"- Project ID from the URL params\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"name\": \"tagId\",\n            \"in\": \"query\",\n            \"description\": \"- Tag ID from the URL params\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ]\n      },\n      \"delete\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"message\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Tag not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Server error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Delete a specific tag\",\n        \"parameters\": [\n          {\n            \"name\": \"projectId\",\n            \"in\": \"query\",\n            \"description\": \"- Project ID from the URL params\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"name\": \"tagId\",\n            \"in\": \"query\",\n            \"description\": \"- Tag ID from the URL params\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ]\n      },\n      \"patch\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"color\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Validation failed\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Tag not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Server error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Update a specific tag\",\n        \"parameters\": [\n          {\n            \"name\": \"projectId\",\n            \"in\": \"query\",\n            \"description\": \"- Project ID from the URL params\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"name\": \"tagId\",\n            \"in\": \"query\",\n            \"description\": \"- Tag ID from the URL params\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/projects/{projectId}/changelog/{entryId}/schedule/approval\": {\n      \"post\": {\n        \"tags\": [\n          \"Projects\"\n        ],\n        \"summary\": \"\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"message\": {\n                      \"type\": \"string\"\n                    },\n                    \"request\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"string\"\n                        },\n                        \"type\": {\n                          \"type\": \"string\"\n                        },\n                        \"status\": {\n                          \"type\": \"string\"\n                        },\n                        \"scheduledAt\": {\n                          \"type\": \"string\",\n                          \"format\": \"date-time\"\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request - Invalid input or business logic violation\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized - User not authenticated\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden - User lacks permission for the action\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Not Found - Changelog entry not found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"409\": {\n            \"description\": \"Conflict - Request already exists or invalid state\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"error\": {\n                      \"type\": \"string\"\n                    },\n                    \"details\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"path\": {\n                            \"type\": \"string\"\n                          },\n                          \"message\": {\n                            \"type\": \"string\"\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"security\": [],\n        \"description\": \"Creates, approves, or rejects a scheduled publish request for a changelog entry when approval is required\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"description\": \"\"\n              }\n            }\n          }\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"schemas\": {},\n    \"securitySchemes\": {\n      \"bearerAuth\": {\n        \"type\": \"http\",\n        \"scheme\": \"bearer\",\n        \"bearerFormat\": \"JWT\",\n        \"description\": \"JWT Bearer token authentication\"\n      },\n      \"cookieAuth\": {\n        \"type\": \"apiKey\",\n        \"in\": \"cookie\",\n        \"name\": \"accessToken\",\n        \"description\": \"Cookie-based authentication using accessToken\"\n      }\n    }\n  },\n  \"tags\": [\n    {\n      \"name\": \"Subscribers\",\n      \"description\": \"Operations related to Subscribers\"\n    },\n    {\n      \"name\": \"Setup\",\n      \"description\": \"Operations related to Setup\"\n    },\n    {\n      \"name\": \"Requests\",\n      \"description\": \"Operations related to Requests\"\n    },\n    {\n      \"name\": \"Projects\",\n      \"description\": \"Operations related to Projects\"\n    },\n    {\n      \"name\": \"Health\",\n      \"description\": \"Operations related to Health\"\n    },\n    {\n      \"name\": \"System\",\n      \"description\": \"Operations related to System\"\n    },\n    {\n      \"name\": \"Dashboard\",\n      \"description\": \"Operations related to Dashboard\"\n    },\n    {\n      \"name\": \"Changelog\",\n      \"description\": \"Operations related to Changelog\"\n    },\n    {\n      \"name\": \"Analytics\",\n      \"description\": \"Operations related to Analytics\"\n    },\n    {\n      \"name\": \"Auth\",\n      \"description\": \"Operations related to Auth\"\n    },\n    {\n      \"name\": \"Ai\",\n      \"description\": \"Operations related to Ai\"\n    },\n    {\n      \"name\": \"Admin\",\n      \"description\": \"Operations related to Admin\"\n    },\n    {\n      \"name\": \"Integrations\",\n      \"description\": \"Operations related to Integrations\"\n    }\n  ],\n  \"x-tagGroups\": [\n    {\n      \"name\": \"Subscribers\",\n      \"tags\": [\n        \"Subscribers\"\n      ]\n    },\n    {\n      \"name\": \"Setup\",\n      \"tags\": [\n        \"Setup\"\n      ]\n    },\n    {\n      \"name\": \"Requests\",\n      \"tags\": [\n        \"Requests\"\n      ]\n    },\n    {\n      \"name\": \"Projects\",\n      \"tags\": [\n        \"Projects\"\n      ]\n    },\n    {\n      \"name\": \"Health\",\n      \"tags\": [\n        \"Health\"\n      ]\n    },\n    {\n      \"name\": \"System\",\n      \"tags\": [\n        \"System\"\n      ]\n    },\n    {\n      \"name\": \"Dashboard\",\n      \"tags\": [\n        \"Dashboard\"\n      ]\n    },\n    {\n      \"name\": \"Changelog\",\n      \"tags\": [\n        \"Changelog\"\n      ]\n    },\n    {\n      \"name\": \"Analytics\",\n      \"tags\": [\n        \"Analytics\"\n      ]\n    },\n    {\n      \"name\": \"Auth\",\n      \"tags\": [\n        \"Auth\"\n      ]\n    },\n    {\n      \"name\": \"Ai\",\n      \"tags\": [\n        \"Ai\"\n      ]\n    },\n    {\n      \"name\": \"Admin\",\n      \"tags\": [\n        \"Admin\"\n      ]\n    },\n    {\n      \"name\": \"Integrations\",\n      \"tags\": [\n        \"Integrations\"\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "scripts/api/generateSwagger.js",
    "content": "\"use strict\";\nvar __assign = (this && this.__assign) || function () {\n    __assign = Object.assign || function(t) {\n        for (var s, i = 1, n = arguments.length; i < n; i++) {\n            s = arguments[i];\n            for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))\n                t[p] = s[p];\n        }\n        return t;\n    };\n    return __assign.apply(this, arguments);\n};\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {\n    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n    return new (P || (P = Promise))(function (resolve, reject) {\n        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n        function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n        step((generator = generator.apply(thisArg, _arguments || [])).next());\n    });\n};\nvar __generator = (this && this.__generator) || function (thisArg, body) {\n    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === \"function\" ? Iterator : Object).prototype);\n    return g.next = verb(0), g[\"throw\"] = verb(1), g[\"return\"] = verb(2), typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\n    function verb(n) { return function (v) { return step([n, v]); }; }\n    function step(op) {\n        if (f) throw new TypeError(\"Generator is already executing.\");\n        while (g && (g = 0, op[0] && (_ = 0)), _) try {\n            if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n            if (y = 0, t) op = [op[0] & 2, t.value];\n            switch (op[0]) {\n                case 0: case 1: t = op; break;\n                case 4: _.label++; return { value: op[1], done: false };\n                case 5: _.label++; y = op[1]; op = [0]; continue;\n                case 7: op = _.ops.pop(); _.trys.pop(); continue;\n                default:\n                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\n                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\n                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\n                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\n                    if (t[2]) _.ops.pop();\n                    _.trys.pop(); continue;\n            }\n            op = body.call(thisArg, _);\n        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\n        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\n    }\n};\nObject.defineProperty(exports, \"__esModule\", { value: true });\nvar fs = require(\"fs\");\nvar path = require(\"path\");\n// @ts-expect-error: jsdoc package does not have TypeScript types\nvar jsdoc = require(\"jsdoc\");\nvar apiRoutesDir = path.join(__dirname, 'app/api');\nvar swaggerOutputPath = path.join(__dirname, 'swagger.json');\nfunction generateSwagger() {\n    return __awaiter(this, void 0, void 0, function () {\n        var swaggerDoc, files, _i, files_1, file, filePath, doc;\n        return __generator(this, function (_a) {\n            console.log('Starting Swagger JSON generation...');\n            swaggerDoc = {\n                openapi: '3.0.0',\n                info: {\n                    title: 'API Documentation',\n                    version: '1.0.0',\n                },\n                paths: {},\n            };\n            try {\n                files = fs.readdirSync(apiRoutesDir);\n                console.log(\"Found \".concat(files.length, \" files in \").concat(apiRoutesDir));\n                for (_i = 0, files_1 = files; _i < files_1.length; _i++) {\n                    file = files_1[_i];\n                    filePath = path.join(apiRoutesDir, file);\n                    console.log(\"Processing file: \".concat(filePath));\n                    try {\n                        doc = jsdoc.explainSync({ files: filePath });\n                        doc.forEach(function (comment) {\n                            var _a, _b;\n                            var _c, _d, _e;\n                            var method = (_c = comment.tags.find(function (tag) { return tag.title === 'method'; })) === null || _c === void 0 ? void 0 : _c.value;\n                            var path = (_d = comment.tags.find(function (tag) { return tag.title === 'path'; })) === null || _d === void 0 ? void 0 : _d.value;\n                            var summary = (_e = comment.tags.find(function (tag) { return tag.title === 'desc'; })) === null || _e === void 0 ? void 0 : _e.value;\n                            var params = comment.tags.filter(function (tag) { return tag.title === 'param'; });\n                            var responses = comment.tags.filter(function (tag) { return tag.title === 'response'; });\n                            if (method && path && summary) {\n                                var swaggerPath = (_a = {},\n                                    _a[path] = (_b = {},\n                                        _b[method.toLowerCase()] = {\n                                            summary: summary,\n                                            parameters: params.map(function (param) { return ({\n                                                name: param.name,\n                                                in: 'query',\n                                                required: true,\n                                                schema: {\n                                                    type: 'string',\n                                                },\n                                            }); }),\n                                            responses: responses.reduce(function (acc, response) {\n                                                acc[response.name] = {\n                                                    description: response.description,\n                                                };\n                                                return acc;\n                                            }, {}),\n                                        },\n                                        _b),\n                                    _a);\n                                swaggerDoc.paths = __assign(__assign({}, swaggerDoc.paths), swaggerPath);\n                            }\n                        });\n                    }\n                    catch (error) {\n                        console.error(\"Error processing file \".concat(filePath, \":\"), error);\n                    }\n                }\n                fs.writeFileSync(swaggerOutputPath, JSON.stringify(swaggerDoc, null, 2));\n                console.log('Swagger JSON file generated at', swaggerOutputPath);\n            }\n            catch (error) {\n                console.error('Error reading API routes directory:', error);\n            }\n            console.log('Swagger JSON generation completed.');\n            return [2 /*return*/];\n        });\n    });\n}\ngenerateSwagger();\n"
  },
  {
    "path": "scripts/api/generateSwagger.ts",
    "content": "import fs from 'fs/promises';\nimport path from 'path';\nimport {glob} from 'glob';\nimport {parse} from 'comment-parser';\nimport type {OpenAPIV3} from 'openapi-types';\nimport chalk from 'chalk';\nimport 'dotenv/config'\nimport packageJson from \"../../package.json\";\nimport ora from 'ora';\nimport {appInfo} from \"@/lib/app-info\";\n\ninterface CommentTag {\n    tag: string;\n    name: string;\n    type: string;\n    optional: boolean;\n    description: string;\n    line: number;\n    source: string[];\n    problems: string[];\n}\n\ninterface CommentBlock {\n    description: string;\n    tags: CommentTag[];\n    source: string[];\n    problems: string[];\n    line: number;\n}\n\ninterface OpenAPIDocumentWithExtensions extends OpenAPIV3.Document {\n    'x-tagGroups'?: Array<{ name: string; tags: string[] }>;\n}\n\ntype HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch';\n\ninterface SwaggerRoute {\n    filePath: string;\n    path: string;\n    method: HttpMethod;\n    docs: CommentBlock;\n    section: string;\n}\n\ninterface RouteReport {\n    documented: string[];\n    undocumented: string[];\n    sections: Map<string, string[]>;\n}\n\nconst delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));\n\nconst randomDelay = async (min: number = 200, max: number = 800) => {\n    const delayTime = Math.floor(Math.random() * (max - min + 1) + min);\n    await delay(delayTime);\n};\n\nfunction pathToSectionTitle(path: string): string {\n    const segment = path.split('/')[0];\n    if (!segment) return 'General';\n\n    return segment\n        .split('-')\n        .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n        .join(' ');\n}\n\nfunction parseSchema(schema: Record<string, unknown>): OpenAPIV3.SchemaObject {\n    if (typeof schema !== 'object' || !schema) {\n        return {type: 'object'};\n    }\n\n    const schemaType = schema.type as OpenAPIV3.SchemaObject['type'];\n\n    if (schemaType === 'array') {\n        const result: OpenAPIV3.ArraySchemaObject = {\n            type: 'array',\n            items: schema.items ? parseSchema(schema.items as Record<string, unknown>) : {type: 'object'}\n        };\n\n        if (schema.description) {\n            result.description = schema.description as string;\n        }\n\n        if (schema.example) {\n            result.example = schema.example;\n        }\n\n        return result;\n    } else {\n        const result: OpenAPIV3.NonArraySchemaObject = {\n            type: schemaType as OpenAPIV3.NonArraySchemaObjectType || 'object'\n        };\n\n        if (schema.description) {\n            result.description = schema.description as string;\n        }\n\n        if (schema.example) {\n            result.example = schema.example;\n        }\n\n        if (schema.enum) {\n            result.enum = schema.enum as (string | number | boolean)[];\n        }\n\n        if (schema.format) {\n            result.format = schema.format as string;\n        }\n\n        if (schema.properties) {\n            result.properties = {};\n            for (const [key, value] of Object.entries(schema.properties as Record<string, unknown>)) {\n                result.properties[key] = parseSchema(value as Record<string, unknown>);\n            }\n        }\n\n        if (schema.required) {\n            result.required = schema.required as string[];\n        }\n\n        if (schema.additionalProperties !== undefined) {\n            result.additionalProperties = typeof schema.additionalProperties === 'object'\n                ? parseSchema(schema.additionalProperties as Record<string, unknown>)\n                : schema.additionalProperties as boolean;\n        }\n\n        return result;\n    }\n}\n\nfunction tryParseJSON(str: string, defaultValue: unknown = undefined): unknown {\n    try {\n        if (typeof str === 'object') return str;\n        return JSON.parse(str);\n    } catch {\n        return defaultValue;\n    }\n}\n\nfunction extractFullDescription(tag: CommentTag): string {\n    if (!tag.type && !tag.name) {\n        return tag.description;\n    }\n\n    const parts = [tag.type, tag.name, tag.description].filter(Boolean);\n    return parts.join(' ');\n}\n\nfunction processRouteOperation(route: SwaggerRoute, routeDocs: CommentBlock): OpenAPIV3.OperationObject {\n    const operation: OpenAPIV3.OperationObject = {\n        tags: [route.section],\n        summary: '',\n        responses: {},\n        security: []\n    };\n\n    if (routeDocs.description) {\n        operation.description = routeDocs.description;\n    }\n\n    for (const tag of routeDocs.tags) {\n        switch (tag.tag) {\n            case 'summary':\n                operation.summary = extractFullDescription(tag);\n                break;\n            case 'description':\n                operation.description = extractFullDescription(tag);\n                break;\n            case 'param':\n                if (!operation.parameters) {\n                    operation.parameters = [];\n                }\n                const paramLocation = tag.type.includes('body') ? 'body' :\n                    tag.type.includes('path') ? 'path' :\n                        tag.type.includes('header') ? 'header' : 'query';\n\n                if (paramLocation === 'body') {\n                    const schema = tryParseJSON(tag.description) as Record<string, unknown> | undefined;\n                    operation.requestBody = {\n                        required: !tag.optional,\n                        content: {\n                            'application/json': {\n                                schema: schema ? parseSchema(schema) : {\n                                    type: 'object' as const,\n                                    properties: {\n                                        [tag.name]: {\n                                            type: tag.type.replace('body.', '').toLowerCase() as OpenAPIV3.NonArraySchemaObjectType,\n                                            description: tag.description\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    };\n                } else {\n                    operation.parameters.push({\n                        name: tag.name,\n                        in: paramLocation as OpenAPIV3.ParameterObject['in'],\n                        description: tag.description,\n                        required: !tag.optional,\n                        schema: {\n                            type: tag.type.toLowerCase().replace(`${paramLocation}.`, '') as OpenAPIV3.NonArraySchemaObjectType\n                        }\n                    });\n                }\n                break;\n            case 'body':\n                const bodySchema = tryParseJSON(tag.description) as Record<string, unknown> | undefined;\n                operation.requestBody = {\n                    required: true,\n                    content: {\n                        'application/json': {\n                            schema: bodySchema ? parseSchema(bodySchema) : {\n                                type: 'object' as const,\n                                description: tag.description\n                            }\n                        }\n                    }\n                };\n                break;\n            case 'returns':\n            case 'response': {\n                const statusCode = tag.name || '200';\n                const responseSchema = tryParseJSON(tag.description) as Record<string, unknown> | undefined;\n                const type = (tag.type?.toLowerCase() || 'object') as OpenAPIV3.SchemaObject['type'];\n                const schemaObj = responseSchema ? parseSchema(responseSchema) : (\n                    type === 'array'\n                        ? {type: 'array' as const, items: {type: 'object' as const}}\n                        : {type: type as OpenAPIV3.NonArraySchemaObjectType, description: tag.description}\n                );\n\n                operation.responses[statusCode] = {\n                    description: (responseSchema?.description as string) || 'Successful response',\n                    content: {\n                        'application/json': {\n                            schema: schemaObj\n                        }\n                    }\n                };\n                break;\n            }\n            case 'throws':\n            case 'error': {\n                const errorCode = tag.name || '400';\n                const errorSchema = tryParseJSON(tag.description) as Record<string, unknown> | undefined;\n                operation.responses[errorCode] = {\n                    description: tag.description || 'Error response',\n                    content: {\n                        'application/json': {\n                            schema: errorSchema ? parseSchema(errorSchema) : {\n                                type: 'object' as const,\n                                properties: {\n                                    error: {\n                                        type: 'string' as const\n                                    },\n                                    details: {\n                                        type: 'array' as const,\n                                        items: {\n                                            type: 'object' as const,\n                                            properties: {\n                                                path: {type: 'string' as const},\n                                                message: {type: 'string' as const}\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                };\n                break;\n            }\n            case 'secure':\n                operation.security = [{\n                    [tag.name || 'cookieAuth']: []\n                }];\n                break;\n        }\n    }\n\n    return operation;\n}\n\nasync function processRouteFiles(\n    routeFiles: string[],\n    API_DIR: string\n): Promise<{\n    routes: SwaggerRoute[],\n    report: RouteReport,\n    sections: Map<string, Set<string>>\n}> {\n    const routes: SwaggerRoute[] = [];\n    const report: RouteReport = {\n        documented: [],\n        undocumented: [],\n        sections: new Map()\n    };\n    const sections = new Map<string, Set<string>>();\n\n    for (const file of routeFiles) {\n        const filePath = path.join(API_DIR, file);\n        const content = await fs.readFile(filePath, 'utf-8');\n        const comments = parse(content) as unknown as CommentBlock[];\n\n        const routePath = path.dirname(file)\n            .replace(/\\\\/g, '/')\n            .replace(/\\[([^\\]]+)\\]/g, '{$1}');\n\n        const section = pathToSectionTitle(routePath);\n\n        const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];\n        let hasDocumentation = false;\n\n        for (const method of methods) {\n            const methodPattern = new RegExp(`export\\\\s+async\\\\s+function\\\\s+${method}`);\n            if (methodPattern.test(content)) {\n                const routeDocs = comments.find(comment =>\n                    comment.description.includes(`@${method.toLowerCase()}`) ||\n                    comment.tags.some(tag =>\n                        (tag.tag === 'method' && tag.name.toLowerCase() === method.toLowerCase()) ||\n                        tag.tag.toLowerCase() === method.toLowerCase()\n                    )\n                );\n\n                if (routeDocs) {\n                    hasDocumentation = true;\n                    routes.push({\n                        filePath: file,\n                        path: routePath,\n                        method: method.toLowerCase() as HttpMethod,\n                        docs: routeDocs,\n                        section\n                    });\n\n                    if (!sections.has(section)) {\n                        sections.set(section, new Set());\n                    }\n                    sections.get(section)!.add(routePath);\n\n                    if (!report.sections.has(section)) {\n                        report.sections.set(section, []);\n                    }\n                    report.sections.get(section)!.push(`${routePath} [${method}]`);\n                } else {\n                    report.undocumented.push(`${routePath} [${method}]`);\n                }\n            }\n        }\n\n        if (hasDocumentation) {\n            report.documented.push(routePath);\n        }\n    }\n\n    return {routes, report, sections};\n}\n\nasync function generateSwaggerDocs() {\n    const spinner = ora('Initializing Swagger documentation generator...').start();\n\n    try {\n        const API_DIR = path.join(process.cwd(), 'app/api');\n\n        await randomDelay(500, 1000);\n        spinner.text = 'Finding route files...';\n        const routeFiles = await glob('**/route.ts', {\n            cwd: API_DIR,\n            ignore: ['**/_*.ts', '**/node_modules/**']\n        });\n\n        await randomDelay();\n        spinner.text = 'Setting up OpenAPI structure...';\n        const swagger: OpenAPIDocumentWithExtensions = {\n            openapi: '3.0.0',\n            info: {\n                title: `${appInfo.name} API Documentation`,\n                version: appInfo.version,\n                description: `The official documentation for the ${appInfo.name} API. rawr`,\n                contact: {\n                    name: 'API Support',\n                    url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'\n                }\n            },\n            servers: [\n                {\n                    url: `${process.env.NEXT_PUBLIC_APP_URL}`,\n                    description: 'API Server'\n                }\n            ],\n            paths: {},\n            components: {\n                schemas: {},\n                securitySchemes: {\n                    bearerAuth: {\n                        type: 'http',\n                        scheme: 'bearer',\n                        bearerFormat: 'JWT',\n                        description: 'JWT Bearer token authentication'\n                    },\n                    cookieAuth: {\n                        type: 'apiKey',\n                        in: 'cookie',\n                        name: 'accessToken',\n                        description: 'Cookie-based authentication using accessToken'\n                    }\n                }\n            },\n            tags: [],\n        };\n\n        swagger['x-tagGroups'] = [];\n\n        await randomDelay(1000, 2000);\n        spinner.text = 'Processing route files...';\n        const {routes, report, sections} = await processRouteFiles(routeFiles, API_DIR);\n\n        await randomDelay();\n        spinner.text = 'Organizing API sections...';\n        const tagGroups: { name: string; tags: string[] }[] = [];\n\n        sections.forEach((routes, section) => {\n            swagger.tags!.push({\n                name: section,\n                description: `Operations related to ${section}`\n            });\n\n            tagGroups.push({\n                name: section,\n                tags: [section]\n            });\n        });\n\n        swagger['x-tagGroups'] = tagGroups;\n\n        await randomDelay(800, 1500);\n        spinner.text = 'Converting routes to OpenAPI format...';\n        for (const route of routes) {\n            const apiPath = `/api/${route.path}`;\n            const pathItem: OpenAPIV3.PathItemObject = swagger.paths[apiPath] || {};\n\n            const operation = processRouteOperation(route, route.docs);\n\n            if (route.method === 'get') pathItem.get = operation;\n            else if (route.method === 'post') pathItem.post = operation;\n            else if (route.method === 'put') pathItem.put = operation;\n            else if (route.method === 'delete') pathItem.delete = operation;\n            else if (route.method === 'patch') pathItem.patch = operation;\n            swagger.paths[apiPath] = pathItem;\n\n            await delay(50);\n        }\n\n        await randomDelay(500, 1000);\n        spinner.text = 'Writing documentation file...';\n        const publicDir = path.join(process.cwd(), 'public');\n        try {\n            await fs.access(publicDir);\n        } catch {\n            await fs.mkdir(publicDir);\n        }\n\n        await fs.writeFile(\n            path.join(publicDir, 'swagger.json'),\n            JSON.stringify(swagger, null, 2)\n        );\n\n        await randomDelay(300, 600);\n        spinner.succeed('Documentation generated successfully!');\n\n        console.log('\\nAPI Documentation Report:');\n        console.log(`\\nAPI Server URL: ${chalk.blue(process.env.NEXT_PUBLIC_APP_URL + '/api')}`);\n\n        console.log('\\nDocumented Routes by Section:');\n        sections.forEach((routes, section) => {\n            console.log(`\\n${chalk.cyan(section)}:`);\n            const sectionRoutes = report.sections.get(section) || [];\n            sectionRoutes.forEach(route => {\n                console.log(chalk.green(`✓ ${route}`));\n            });\n        });\n\n        console.log('\\nUndocumented Routes:');\n        report.undocumented.forEach(route => {\n            console.log(chalk.yellow(`⚠ ${route}`));\n        });\n\n        console.log('\\nSummary:');\n        console.log(`Total Routes: ${report.documented.length + report.undocumented.length}`);\n        console.log(`Documented: ${chalk.green(report.documented.length)}`);\n        console.log(`Undocumented: ${chalk.yellow(report.undocumented.length)}`);\n        console.log(`Total Sections: ${chalk.cyan(sections.size)}`);\n\n        console.log('\\nSwagger documentation generated successfully in public/swagger.json!');\n    } catch (error) {\n        await randomDelay(200, 500);\n        spinner.fail('Error generating documentation');\n        console.error(chalk.red('Error details:'), error);\n        process.exit(1);\n    }\n}\n\ngenerateSwaggerDocs().catch(error => {\n    console.error(chalk.red('Error generating documentation:'), error);\n    process.exit(1);\n});\n"
  },
  {
    "path": "scripts/ftb/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\">\n    <title>Access Denied</title>\n    <style>\n        * {\n            margin: 0;\n            padding: 0;\n            box-sizing: border-box;\n        }\n\n        body {\n            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n            min-height: 100vh;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            color: white;\n            overflow: hidden;\n        }\n\n        .container {\n            text-align: center;\n            max-width: 600px;\n            padding: 2rem;\n            animation: fadeIn 1s ease-out;\n        }\n\n        .status-icon {\n            font-size: 4rem;\n            margin-bottom: 1rem;\n            filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));\n        }\n\n        h1 {\n            font-size: 2.5rem;\n            font-weight: 700;\n            margin-bottom: 1rem;\n            background: linear-gradient(45deg, #fff, #f0f0f0);\n            -webkit-background-clip: text;\n            -webkit-text-fill-color: transparent;\n            background-clip: text;\n        }\n\n        .subtitle {\n            font-size: 1.2rem;\n            margin-bottom: 2rem;\n            opacity: 0.9;\n            font-weight: 300;\n        }\n\n        .error-container {\n            margin: 2rem 0;\n            background: rgba(255, 255, 255, 0.1);\n            border-radius: 16px;\n            padding: 2rem;\n            backdrop-filter: blur(20px);\n            border: 1px solid rgba(255, 255, 255, 0.2);\n        }\n\n        .error-details {\n            margin-bottom: 1.5rem;\n        }\n\n        .error-message {\n            font-size: 1rem;\n            margin-bottom: 1rem;\n            opacity: 0.9;\n            line-height: 1.6;\n        }\n\n        .error-code {\n            font-size: 0.9rem;\n            opacity: 0.7;\n            font-family: 'Courier New', monospace;\n            background: rgba(0, 0, 0, 0.2);\n            padding: 0.5rem 1rem;\n            border-radius: 8px;\n            margin-bottom: 1.5rem;\n        }\n\n        /* Background animation */\n        .bg-shapes {\n            position: fixed;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            pointer-events: none;\n            z-index: -1;\n        }\n\n        .shape {\n            position: absolute;\n            opacity: 0.1;\n            animation: float 6s ease-in-out infinite;\n        }\n\n        .shape:nth-child(1) {\n            top: 20%;\n            left: 10%;\n            animation-delay: 0s;\n        }\n\n        .shape:nth-child(2) {\n            top: 60%;\n            right: 10%;\n            animation-delay: 2s;\n        }\n\n        .shape:nth-child(3) {\n            bottom: 20%;\n            left: 20%;\n            animation-delay: 4s;\n        }\n\n        @keyframes fadeIn {\n            from {\n                opacity: 0;\n                transform: translateY(20px);\n            }\n            to {\n                opacity: 1;\n                transform: translateY(0);\n            }\n        }\n\n        @keyframes float {\n            0%, 100% {\n                transform: translateY(0px) rotate(0deg);\n            }\n            50% {\n                transform: translateY(-20px) rotate(180deg);\n            }\n        }\n\n        @media (max-width: 640px) {\n            .container {\n                padding: 1rem;\n            }\n\n            .status-icon {\n                font-size: 3rem;\n            }\n\n            h1 {\n                font-size: 2rem;\n            }\n\n            .subtitle {\n                font-size: 1rem;\n            }\n\n            .error-container {\n                padding: 1.5rem;\n            }\n        }\n    </style>\n</head>\n<body>\n<div class=\"bg-shapes\">\n    <div class=\"shape\">⚠️</div>\n    <div class=\"shape\">🔒</div>\n    <div class=\"shape\">⛔</div>\n</div>\n\n<div class=\"container\">\n    <div class=\"status-icon\">🔒</div>\n    <h1>Access Denied</h1>\n    <p class=\"subtitle\">Configuration Error Detected</p>\n\n    <div class=\"error-container\">\n        <div class=\"error-details\">\n            <div class=\"error-message\">\n                You are missing environment variables that need to be properly configured.\n            </div>\n            <div class=\"error-message\">\n                Please restart Changerawr once these variables have been configured correctly.\n            </div>\n            <div class=\"error-code\">\n                ERROR_CODE: MISSING_ENV_VARS\n            </div>\n        </div>\n    </div>\n</div>\n\n<script>\n    // Add subtle interaction feedback\n    document.querySelectorAll('.action-button').forEach(button => {\n        button.addEventListener('click', function () {\n            this.style.transform = 'translateY(0px)';\n            setTimeout(() => {\n                this.style.transform = '';\n            }, 100);\n        });\n    });\n</script>\n</body>\n</html>"
  },
  {
    "path": "scripts/ftb/server.js",
    "content": "// scripts/ftb/server.js\nconst http = require('http');\nconst fs = require('fs');\nconst path = require('path');\n\nconst PORT = process.env.PORT || 3000;\n\n// Get missing environment variables from command line args\nconst missingVars = process.argv.slice(2);\n\nconst server = http.createServer((req, res) => {\n    if (req.url === '/' || req.url === '/index.html') {\n        const htmlPath = path.join(__dirname, 'index.html');\n\n        fs.readFile(htmlPath, 'utf8', (err, data) => {\n            if (err) {\n                res.writeHead(500, { 'Content-Type': 'text/plain' });\n                res.end('Error loading page');\n                return;\n            }\n\n            // Replace placeholder with actual missing variables\n            const html = data.replace('{{MISSING_VARS}}', missingVars.join(', '));\n\n            res.writeHead(200, { 'Content-Type': 'text/html' });\n            res.end(html);\n        });\n    } else {\n        res.writeHead(404, { 'Content-Type': 'text/plain' });\n        res.end('Not Found');\n    }\n});\n\nserver.listen(PORT, () => {\n    console.log(`\\n🚨 CHANGERAWR FAILURE TO BOOT (FTB) 🚨`);\n    console.log(`╔══════════════════════════════════════╗`);\n    console.log(`║  CRITICAL CONFIGURATION ERROR       ║`);\n    console.log(`║  Missing environment variables       ║`);\n    console.log(`╚══════════════════════════════════════╝`);\n    console.log(`\\nError server running at: http://localhost:${PORT}`);\n    console.log(`Missing variables: ${missingVars.join(', ')}\\n`);\n});\n\n// Handle graceful shutdown\nprocess.on('SIGINT', () => {\n    console.log('\\nShutting down error server...');\n    server.close(() => {\n        process.exit(0);\n    });\n});\n\nprocess.on('SIGTERM', () => {\n    console.log('\\nShutting down error server...');\n    server.close(() => {\n        process.exit(0);\n    });\n});"
  },
  {
    "path": "scripts/maintenance/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Changerawr is Starting Up</title>\n    <style>\n        * {\n            margin: 0;\n            padding: 0;\n            box-sizing: border-box;\n        }\n\n        body {\n            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n            min-height: 100vh;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            color: white;\n            overflow: hidden;\n        }\n\n        .container {\n            text-align: center;\n            max-width: 600px;\n            padding: 2rem;\n            animation: fadeIn 1s ease-out;\n        }\n\n        .logo {\n            font-size: 4rem;\n            margin-bottom: 1rem;\n            animation: bounce 2s infinite;\n            filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));\n        }\n\n        h1 {\n            font-size: 2.5rem;\n            font-weight: 700;\n            margin-bottom: 1rem;\n            background: linear-gradient(45deg, #fff, #f0f0f0);\n            -webkit-background-clip: text;\n            -webkit-text-fill-color: transparent;\n            background-clip: text;\n        }\n\n        .subtitle {\n            font-size: 1.2rem;\n            margin-bottom: 2rem;\n            opacity: 0.9;\n            font-weight: 300;\n        }\n\n        .loading-container {\n            margin: 2rem 0;\n        }\n\n        .loading-bar {\n            width: 100%;\n            height: 6px;\n            background: rgba(255, 255, 255, 0.2);\n            border-radius: 3px;\n            overflow: hidden;\n            margin-bottom: 1rem;\n        }\n\n        .loading-progress {\n            height: 100%;\n            background: linear-gradient(90deg, #4ade80, #22c55e);\n            border-radius: 3px;\n            animation: loading 3s infinite;\n            box-shadow: 0 0 10px rgba(74, 222, 128, 0.5);\n        }\n\n        .loading-text {\n            font-size: 0.9rem;\n            opacity: 0.8;\n            animation: pulse 2s infinite;\n        }\n\n        .status-updates {\n            margin-top: 2rem;\n            font-size: 0.9rem;\n            opacity: 0.7;\n            min-height: 1.5rem;\n        }\n\n        .dots::after {\n            content: '';\n            animation: dots 1.5s infinite;\n        }\n\n        /* Background animation */\n        .bg-shapes {\n            position: fixed;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            pointer-events: none;\n            z-index: -1;\n        }\n\n        .shape {\n            position: absolute;\n            opacity: 0.1;\n            animation: float 6s ease-in-out infinite;\n        }\n\n        .shape:nth-child(1) {\n            top: 20%;\n            left: 10%;\n            animation-delay: 0s;\n        }\n\n        .shape:nth-child(2) {\n            top: 60%;\n            right: 10%;\n            animation-delay: 2s;\n        }\n\n        .shape:nth-child(3) {\n            bottom: 20%;\n            left: 20%;\n            animation-delay: 4s;\n        }\n\n        @keyframes fadeIn {\n            from {\n                opacity: 0;\n                transform: translateY(20px);\n            }\n            to {\n                opacity: 1;\n                transform: translateY(0);\n            }\n        }\n\n        @keyframes bounce {\n            0%, 20%, 50%, 80%, 100% {\n                transform: translateY(0);\n            }\n            40% {\n                transform: translateY(-10px);\n            }\n            60% {\n                transform: translateY(-5px);\n            }\n        }\n\n        @keyframes loading {\n            0% {\n                transform: translateX(-100%);\n            }\n            50% {\n                transform: translateX(0%);\n            }\n            100% {\n                transform: translateX(100%);\n            }\n        }\n\n        @keyframes pulse {\n            0%, 100% {\n                opacity: 0.8;\n            }\n            50% {\n                opacity: 1;\n            }\n        }\n\n        @keyframes dots {\n            0% {\n                content: '';\n            }\n            25% {\n                content: '.';\n            }\n            50% {\n                content: '..';\n            }\n            75% {\n                content: '...';\n            }\n        }\n\n        @keyframes float {\n            0%, 100% {\n                transform: translateY(0px) rotate(0deg);\n            }\n            50% {\n                transform: translateY(-20px) rotate(180deg);\n            }\n        }\n\n        @media (max-width: 640px) {\n            .container {\n                padding: 1rem;\n            }\n\n            .logo {\n                font-size: 3rem;\n            }\n\n            h1 {\n                font-size: 2rem;\n            }\n\n            .subtitle {\n                font-size: 1rem;\n            }\n        }\n    </style>\n</head>\n<body>\n<div class=\"bg-shapes\">\n    <div class=\"shape\">🦖</div>\n    <div class=\"shape\">⚡</div>\n    <div class=\"shape\">🚀</div>\n</div>\n\n<div class=\"container\">\n    <div class=\"logo\">🦖</div>\n    <svg width=\"225\" height=\"50\" viewBox=\"0 0 225 50\" xmlns=\"http://www.w3.org/2000/svg\">\n        <defs>\n            <!--Diagonal gradient for rainbow effect on \"rawr\"-->\n            <linearGradient id=\"rainbowGradient\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\" gradientUnits=\"objectBoundingBox\">\n                <stop offset=\"0%\" stop-color=\"#186cb8\"/>\n                <stop offset=\"19%\" stop-color=\"#186cb8\"/>\n                <stop offset=\"20%\" stop-color=\"#2a9a9f\"/>\n                <stop offset=\"39%\" stop-color=\"#2a9a9f\"/>\n                <stop offset=\"40%\" stop-color=\"#f1b211\"/>\n                <stop offset=\"59%\" stop-color=\"#f1b211\"/>\n                <stop offset=\"60%\" stop-color=\"#e83611\"/>\n                <stop offset=\"79%\" stop-color=\"#e83611\"/>\n                <stop offset=\"80%\" stop-color=\"#f9002f\"/>\n                <stop offset=\"100%\" stop-color=\"#f9002f\"/>\n            </linearGradient>\n        </defs>\n        <text x=\"0\" y=\"40\" font-family=\"Inter, sans-serif\" font-size=\"36\" font-weight=\"bold\" fill=\"white\" letter-spacing=\"1px\" stroke=\"black\" stroke-width=\"2\" paint-order=\"stroke\">\n            Change\n        </text>\n        <text x=\"138\" y=\"40\" font-family=\"Inter, sans-serif\" font-size=\"36\" font-weight=\"bold\" fill=\"url(#rainbowGradient)\" letter-spacing=\"1px\" stroke=\"black\" stroke-width=\"2\" paint-order=\"stroke\">\n            rawr\n        </text>\n\n    </svg>\n\n    <p class=\"subtitle\">Roaring to life! We're setting up your changelog platform.</p>\n\n    <div class=\"loading-container\">\n        <div class=\"loading-bar\">\n            <div class=\"loading-progress\"></div>\n        </div>\n        <div class=\"loading-text\" id=\"loadingText\">Starting services<span class=\"dots\"></span></div>\n    </div>\n\n    <div class=\"status-updates\" id=\"statusText\">\n        Initializing database connections...\n    </div>\n</div>\n\n<script>\n    const statusMessages = [\n        \"Initializing database connections...\",\n        \"Generating Prisma client...\",\n        \"Running database migrations...\",\n        \"Building widget components...\",\n        \"Generating API documentation...\",\n        \"Starting Next.js server...\",\n        \"Almost ready to roar!\"\n    ];\n\n    const loadingMessages = [\n        \"Starting services\",\n        \"Loading components\",\n        \"Connecting database\",\n        \"Preparing interface\",\n        \"Finalizing setup\"\n    ];\n\n    let messageIndex = 0;\n    let loadingIndex = 0;\n\n    // Update status messages\n    setInterval(() => {\n        const statusElement = document.getElementById('statusText');\n        if (messageIndex < statusMessages.length) {\n            statusElement.textContent = statusMessages[messageIndex];\n            messageIndex++;\n        }\n    }, 2000);\n\n    // Update loading text\n    setInterval(() => {\n        const loadingElement = document.getElementById('loadingText');\n        if (loadingIndex < loadingMessages.length) {\n            loadingElement.innerHTML = loadingMessages[loadingIndex] + '<span class=\"dots\"></span>';\n            loadingIndex++;\n        } else {\n            loadingIndex = 0;\n        }\n    }, 3000);\n\n    // Check if main app is ready\n    const checkAppReady = async () => {\n        try {\n            const response = await fetch('/api/health', {\n                method: 'GET',\n                cache: 'no-cache'\n            });\n            if (response.ok) {\n                // App is ready, redirect to main page\n                window.location.href = '/';\n            }\n        } catch (error) {\n            // App not ready yet, keep checking\n        }\n    };\n\n    // Check every 2 seconds if the main app is ready\n    const healthCheckInterval = setInterval(checkAppReady, 2000);\n\n    // Fallback: redirect after 60 seconds regardless\n    setTimeout(() => {\n        clearInterval(healthCheckInterval);\n        window.location.href = '/';\n    }, 60000);\n\n    // Start checking immediately\n    checkAppReady();\n</script>\n</body>\n</html>"
  },
  {
    "path": "scripts/maintenance/server.js",
    "content": "const http = require('http');\nconst fs = require('fs');\nconst path = require('path');\n\nconst PORT = process.env.PORT || 3000;\nconst MAINTENANCE_HTML_PATH = path.join(__dirname, '../maintenance', 'index.html');\n\n// Read the maintenance page HTML\nconst maintenanceHTML = fs.readFileSync(MAINTENANCE_HTML_PATH, 'utf8');\n\nconst server = http.createServer((req, res) => {\n    // Set CORS headers\n    res.setHeader('Access-Control-Allow-Origin', '*');\n    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');\n    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');\n\n    // Handle OPTIONS requests\n    if (req.method === 'OPTIONS') {\n        res.writeHead(200);\n        res.end();\n        return;\n    }\n\n    // Health check endpoint returns 503 during maintenance\n    if (req.url === '/api/health') {\n        res.writeHead(503, { 'Content-Type': 'application/json' });\n        res.end(JSON.stringify({\n            status: 'starting',\n            message: 'Application is starting up'\n        }));\n        return;\n    }\n\n    // Serve maintenance page for all other requests\n    res.writeHead(200, { 'Content-Type': 'text/html' });\n    res.end(maintenanceHTML);\n});\n\nserver.listen(PORT, '0.0.0.0', () => {\n    console.log(`🦖 Maintenance server running on port ${PORT}`);\n    console.log('Waiting for Next.js to start...');\n});\n\n// Graceful shutdown\nprocess.on('SIGTERM', () => {\n    console.log('🦖 Maintenance server shutting down...');\n    server.close(() => {\n        console.log('🦖 Maintenance server stopped');\n        process.exit(0);\n    });\n});\n\nprocess.on('SIGINT', () => {\n    console.log('🦖 Maintenance server shutting down...');\n    server.close(() => {\n        console.log('🦖 Maintenance server stopped');\n        process.exit(0);\n    });\n});\n\nmodule.exports = server;"
  },
  {
    "path": "scripts/nginx-reload.sh",
    "content": "#!/bin/bash\n# nginx reload wrapper with automatic cleanup on failure\n# Used by nginx-agent to safely reload nginx after config changes\n\nSITES_ENABLED=\"/etc/nginx/sites-enabled\"\nBACKUP_DIR=\"/tmp/nginx-backup-$(date +%s)\"\n\necho \"[nginx-reload] Testing new configuration...\"\n\n# Test the configuration\nif nginx -t 2>&1; then\n    echo \"[nginx-reload] Configuration valid\"\nelse\n    echo \"[nginx-reload] ❌ Configuration test failed!\"\n\n    # Backup current configs\n    mkdir -p \"$BACKUP_DIR\"\n    if [ -n \"$(ls -A $SITES_ENABLED/*.conf 2>/dev/null)\" ]; then\n        cp \"$SITES_ENABLED\"/*.conf \"$BACKUP_DIR/\" 2>/dev/null || true\n        echo \"[nginx-reload] Backed up configs to $BACKUP_DIR\"\n    fi\n\n    # Remove all custom domain configs\n    rm -f \"$SITES_ENABLED\"/*.conf\n    echo \"[nginx-reload] Removed broken custom domain configs\"\n\n    # Test again\n    if nginx -t 2>&1; then\n        echo \"[nginx-reload] ✅ Configuration fixed\"\n    else\n        echo \"[nginx-reload] ❌ Configuration still broken after cleanup!\"\n        exit 1\n    fi\nfi\n\n# Check if nginx is actually running before trying to reload\nif [ -f /run/nginx/nginx.pid ] && [ -n \"$(cat /run/nginx/nginx.pid 2>/dev/null)\" ] && kill -0 $(cat /run/nginx/nginx.pid) 2>/dev/null; then\n    echo \"[nginx-reload] Reloading nginx...\"\n    nginx -s reload\n    echo \"[nginx-reload] ✅ Reload successful\"\nelse\n    echo \"[nginx-reload] ⚠️  nginx not running, skipping reload (will use config on next start)\"\nfi\n\nexit 0\n"
  },
  {
    "path": "scripts/utils/scan.ts",
    "content": "#!/usr/bin/env ts-node\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { chromium, Page } from 'playwright';\n\ninterface PageInfo {\n    path: string;\n    fullPath: string;\n    type: 'page' | 'layout' | 'loading' | 'error' | 'not-found' | 'route';\n    isDynamic: boolean;\n    segments: string[];\n    screenshotPath?: string;\n}\n\ninterface ScreenshotConfig {\n    baseUrl: string;\n    outputDir: string;\n    auth?: {\n        loginUrl: string;\n        credentials: {\n            email: string;\n            password: string;\n        };\n        selectors: {\n            emailInput: string;\n            passwordInput: string;\n            submitButton: string;\n        };\n    };\n    viewport?: {\n        width: number;\n        height: number;\n    };\n    waitForSelector?: string;\n    delay?: number;\n    routeParams?: {\n        projectId?: string;\n        [key: string]: string | undefined;\n    };\n}\n\ninterface RouteTreeNode {\n    name: string;\n    path: string;\n    type: PageInfo['type'];\n    isDynamic: boolean;\n    children: RouteTreeNode[];\n}\n\nclass NextJSPageScanner {\n    private appDir: string;\n    private screenshotConfig?: ScreenshotConfig;\n\n    constructor(appDir: string = './app', screenshotConfig?: ScreenshotConfig) {\n        this.appDir = path.resolve(appDir);\n        this.screenshotConfig = screenshotConfig;\n\n        if (!fs.existsSync(this.appDir)) {\n            throw new Error(`App directory not found: ${this.appDir}`);\n        }\n\n        if (this.screenshotConfig?.outputDir) {\n            this.ensureDirectoryExists(this.screenshotConfig.outputDir);\n        }\n    }\n\n    public async scanPages(): Promise<RouteTreeNode[]> {\n        const pages: PageInfo[] = [];\n        this.scanDirectory(this.appDir, '', pages);\n\n        if (this.screenshotConfig) {\n            await this.takeScreenshots(pages);\n        }\n\n        return this.buildRouteTree(pages);\n    }\n\n    private ensureDirectoryExists(dirPath: string): void {\n        if (!fs.existsSync(dirPath)) {\n            fs.mkdirSync(dirPath, { recursive: true });\n        }\n    }\n\n    private async takeScreenshots(pages: PageInfo[]): Promise<void> {\n        if (!this.screenshotConfig) return;\n\n        const screenshotablePages = this.filterScreenshotablePages(pages);\n        if (screenshotablePages.length === 0) {\n            console.log('No screenshotable pages found');\n            return;\n        }\n\n        console.log(`Taking screenshots for ${screenshotablePages.length} pages...`);\n\n        const browser = await chromium.launch({ headless: false });\n        const context = await browser.newContext({\n            viewport: this.screenshotConfig.viewport || { width: 1920, height: 1080 }\n        });\n\n        const page = await context.newPage();\n\n        try {\n            // Handle authentication if configured\n            if (this.screenshotConfig.auth) {\n                await this.performLogin(page);\n                // Wait 15 seconds after login before taking first screenshot\n                console.log('Waiting 15 seconds after login...');\n                await page.waitForTimeout(15000);\n            }\n\n            // Take screenshots of each page\n            for (const pageInfo of screenshotablePages) {\n                await this.screenshotPage(page, pageInfo);\n            }\n\n        } finally {\n            await browser.close();\n        }\n    }\n\n    private filterScreenshotablePages(pages: PageInfo[]): PageInfo[] {\n        return pages.filter(p => {\n            // Only screenshot page types\n            if (p.type !== 'page') return false;\n\n            // Handle dynamic routes\n            if (p.isDynamic) {\n                // Check if we have the required parameters\n                const requiredParams = this.extractRequiredParams(p.segments);\n                return this.hasRequiredParams(requiredParams);\n            }\n\n            // Static pages are always screenshotable\n            return true;\n        });\n    }\n\n    private extractRequiredParams(segments: string[]): string[] {\n        return segments\n            .filter(segment => segment.startsWith('[') && segment.endsWith(']'))\n            .map(segment => segment.slice(1, -1)); // Remove brackets\n    }\n\n    private hasRequiredParams(requiredParams: string[]): boolean {\n        if (!this.screenshotConfig?.routeParams) return false;\n\n        return requiredParams.every(param =>\n            this.screenshotConfig!.routeParams![param] !== undefined\n        );\n    }\n\n    private buildRouteUrl(pageInfo: PageInfo): string {\n        if (!pageInfo.isDynamic) {\n            return `${this.screenshotConfig!.baseUrl}${pageInfo.path}`;\n        }\n\n        // Replace dynamic segments with actual values\n        let url = pageInfo.path;\n\n        if (this.screenshotConfig?.routeParams) {\n            for (const [param, value] of Object.entries(this.screenshotConfig.routeParams)) {\n                if (value) {\n                    url = url.replace(`[${param}]`, value);\n                }\n            }\n        }\n\n        return `${this.screenshotConfig!.baseUrl}${url}`;\n    }\n\n    private async performLogin(page: Page): Promise<void> {\n        if (!this.screenshotConfig?.auth) return;\n\n        const { loginUrl, credentials, selectors } = this.screenshotConfig.auth;\n\n        console.log(`Logging in at ${loginUrl}...`);\n\n        await page.goto(loginUrl);\n\n        // Step 1: Enter email and click continue\n        await page.waitForSelector(selectors.emailInput);\n        await page.fill(selectors.emailInput, credentials.email);\n        await page.click(selectors.submitButton);\n\n        // Step 2: Wait for password field to appear and enter password\n        await page.waitForSelector(selectors.passwordInput, { timeout: 10000 });\n        await page.fill(selectors.passwordInput, credentials.password);\n        await page.click(selectors.submitButton);\n\n        // Wait for navigation after final login\n        await page.waitForNavigation({ waitUntil: 'networkidle' });\n        console.log('Login completed');\n    }\n\n    private async screenshotPage(page: Page, pageInfo: PageInfo): Promise<void> {\n        if (!this.screenshotConfig) return;\n\n        try {\n            const url = this.buildRouteUrl(pageInfo);\n            console.log(`Capturing: ${url}`);\n\n            await page.goto(url, { waitUntil: 'networkidle' });\n\n            // Wait for specific selector if configured\n            if (this.screenshotConfig.waitForSelector) {\n                await page.waitForSelector(this.screenshotConfig.waitForSelector, { timeout: 10000 });\n            }\n\n            // Additional delay if configured\n            if (this.screenshotConfig.delay) {\n                await page.waitForTimeout(this.screenshotConfig.delay);\n            }\n\n            const screenshotFileName = this.generateScreenshotFilename(pageInfo.path);\n            const screenshotPath = path.join(this.screenshotConfig.outputDir, screenshotFileName);\n\n            await page.screenshot({\n                path: screenshotPath,\n                fullPage: true\n            });\n\n            pageInfo.screenshotPath = screenshotPath;\n            console.log(`Screenshot saved: ${screenshotPath}`);\n\n        } catch (error) {\n            console.error(`Failed to screenshot ${pageInfo.path}:`, error);\n        }\n    }\n\n    private generateScreenshotFilename(routePath: string): string {\n        // Convert route path to safe filename\n        const safeName = routePath\n                .replace(/\\//g, '_')\n                .replace(/\\[|\\]/g, '')\n                .replace(/^_/, 'root')\n            || 'root';\n\n        return `${safeName}.png`;\n    }\n\n    private scanDirectory(dirPath: string, relativePath: string, pages: PageInfo[]): void {\n        const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n\n        for (const entry of entries) {\n            const fullPath = path.join(dirPath, entry.name);\n            const currentPath = path.join(relativePath, entry.name);\n\n            if (entry.isDirectory()) {\n                if (this.shouldSkipDirectory(entry.name)) {\n                    continue;\n                }\n                this.scanDirectory(fullPath, currentPath, pages);\n            } else if (entry.isFile()) {\n                const pageInfo = this.analyzeFile(fullPath, currentPath);\n                if (pageInfo) {\n                    pages.push(pageInfo);\n                }\n            }\n        }\n    }\n\n    private shouldSkipDirectory(dirName: string): boolean {\n        const skipDirs = [\n            'node_modules',\n            '.next',\n            '.git',\n            'components',\n            'lib',\n            'utils',\n            'styles',\n            'public'\n        ];\n        return skipDirs.includes(dirName) || dirName.startsWith('.');\n    }\n\n    private analyzeFile(fullPath: string, relativePath: string): PageInfo | null {\n        const fileName = path.basename(relativePath);\n        const dirPath = path.dirname(relativePath);\n\n        const pageFilePatterns: Record<string, PageInfo['type']> = {\n            'page.tsx': 'page',\n            'page.ts': 'page',\n            'page.jsx': 'page',\n            'page.js': 'page',\n            'layout.tsx': 'layout',\n            'layout.ts': 'layout',\n            'layout.jsx': 'layout',\n            'layout.js': 'layout',\n            'loading.tsx': 'loading',\n            'loading.ts': 'loading',\n            'loading.jsx': 'loading',\n            'loading.js': 'loading',\n            'error.tsx': 'error',\n            'error.ts': 'error',\n            'error.jsx': 'error',\n            'error.js': 'error',\n            'not-found.tsx': 'not-found',\n            'not-found.ts': 'not-found',\n            'not-found.jsx': 'not-found',\n            'not-found.js': 'not-found',\n            'route.tsx': 'route',\n            'route.ts': 'route',\n            'route.jsx': 'route',\n            'route.js': 'route'\n        };\n\n        const fileType = pageFilePatterns[fileName];\n        if (!fileType) {\n            return null;\n        }\n\n        const segments = dirPath === '.' ? [] : dirPath.split(path.sep).filter(Boolean);\n        const isDynamic = this.checkIfDynamic(segments);\n\n        let urlPath = segments.length === 0 ? '/' : '/' + segments.join('/');\n\n        urlPath = segments.reduce((acc, segment) => {\n            if (segment.startsWith('[') && segment.endsWith(']')) {\n                const paramName = segment.slice(1, -1);\n                return acc + '/[' + paramName + ']';\n            }\n            return acc + '/' + segment;\n        }, '') || '/';\n\n        return {\n            path: urlPath,\n            fullPath,\n            type: fileType,\n            isDynamic,\n            segments\n        };\n    }\n\n    private checkIfDynamic(segments: string[]): boolean {\n        return segments.some(segment =>\n            segment.startsWith('[') && segment.endsWith(']') ||\n            segment.startsWith('(') && segment.endsWith(')')\n        );\n    }\n\n    private buildRouteTree(pages: PageInfo[]): RouteTreeNode[] {\n        const root: RouteTreeNode[] = [];\n\n        const sortedPages = [...pages].sort((a, b) =>\n            a.segments.length - b.segments.length\n        );\n\n        for (const page of sortedPages) {\n            const segments = page.segments;\n            let currentLevel = root;\n            let currentPath = '';\n\n            for (let i = 0; i < segments.length; i++) {\n                const segment = segments[i];\n                currentPath += '/' + segment;\n\n                let node = currentLevel.find(n => n.name === segment);\n\n                if (!node) {\n                    node = {\n                        name: segment,\n                        path: currentPath,\n                        type: i === segments.length - 1 ? page.type : 'page',\n                        isDynamic: segment.startsWith('[') || segment.startsWith('('),\n                        children: []\n                    };\n                    currentLevel.push(node);\n                }\n\n                currentLevel = node.children;\n            }\n\n            if (segments.length === 0) {\n                const rootNode: RouteTreeNode = {\n                    name: 'root',\n                    path: '/',\n                    type: page.type,\n                    isDynamic: false,\n                    children: []\n                };\n                root.push(rootNode);\n            }\n        }\n\n        return root;\n    }\n\n    public generateTreeView(nodes: RouteTreeNode[], prefix: string = ''): string {\n        let result = '';\n\n        for (let i = 0; i < nodes.length; i++) {\n            const node = nodes[i];\n            const isLastNode = i === nodes.length - 1;\n            const connector = isLastNode ? '└── ' : '├── ';\n            const dynamicIndicator = node.isDynamic ? ' (dynamic)' : '';\n\n            result += `${prefix}${connector}${node.name} [${node.type}]${dynamicIndicator}\\n`;\n\n            if (node.children.length > 0) {\n                const childPrefix = prefix + (isLastNode ? '    ' : '│   ');\n                result += this.generateTreeView(node.children, childPrefix);\n            }\n        }\n\n        return result;\n    }\n}\n\n// Example configuration\nconst exampleConfig: ScreenshotConfig = {\n    baseUrl: 'http://localhost:3000',\n    outputDir: './screenshots',\n    auth: {\n        loginUrl: 'http://localhost:3000',\n        credentials: {\n            email: 'admin@changerawr.com', // admin seeder account email\n            password: 'password123' // admin seeder account password\n        },\n        selectors: {\n            emailInput: 'input[type=\"email\"]',\n            passwordInput: 'input[type=\"password\"]',\n            submitButton: 'button[type=\"submit\"]'\n        }\n    },\n    viewport: {\n        width: 1920,\n        height: 1080\n    },\n    // waitForSelector: '[data-testid=\"page-loaded\"]', // disabled until eventually implemented\n    delay: 1000, // Optional: additional delay in ms\n    routeParams: {\n        projectId: 'cmhy3qagr000dvt7kd5hoicrk', // Uses project ID from current testing database\n    }\n};\n\nasync function main(): Promise<void> {\n    try {\n        // Example with screenshots\n        const scanner = new NextJSPageScanner('./app', exampleConfig);\n        const routeTree = await scanner.scanPages();\n\n        if (routeTree.length === 0) {\n            console.log('No pages found in the app directory.');\n        } else {\n            console.log('\\nRoute Tree:');\n            console.log(scanner.generateTreeView(routeTree));\n        }\n\n    } catch (error) {\n        console.error('Error scanning pages:', error);\n        process.exit(1);\n    }\n}\n\nif (require.main === module) {\n    main();\n}\n\nexport {NextJSPageScanner};\nexport type { PageInfo, RouteTreeNode, ScreenshotConfig };\n"
  },
  {
    "path": "scripts/widget/build.ts",
    "content": "import * as esbuild from 'esbuild';\nimport * as dotenv from 'dotenv';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\n// Load environment variables\ndotenv.config();\n\nif (!process.env.NEXT_PUBLIC_APP_URL) {\n    throw new Error('NEXT_PUBLIC_APP_URL is required');\n}\n\nfunction copyDirectory(src: string, dest: string) {\n    // Create destination directory if it doesn't exist\n    if (!fs.existsSync(dest)) {\n        fs.mkdirSync(dest, { recursive: true });\n    }\n\n    // Read all files/folders in source directory\n    const entries = fs.readdirSync(src, { withFileTypes: true });\n\n    for (const entry of entries) {\n        const srcPath = path.join(src, entry.name);\n        const destPath = path.join(dest, entry.name);\n\n        if (entry.isDirectory()) {\n            // Recursively copy subdirectories\n            copyDirectory(srcPath, destPath);\n        } else {\n            // Copy file\n            fs.copyFileSync(srcPath, destPath);\n        }\n    }\n}\n\nasync function buildWidget() {\n    try {\n        const variants = [\n            { name: 'legacy', entry: 'widgets/changelog/index.js', output: 'public/widget-bundle.js', globalName: 'ChangerawrWidgetLoader' },\n            { name: 'classic', entry: 'widgets/variants/classic.js', output: 'public/widget-classic.js', globalName: 'ChangerawrWidget' },\n            { name: 'floating', entry: 'widgets/variants/floating.js', output: 'public/widget-floating.js', globalName: 'ChangerawrWidget' },\n            { name: 'modal', entry: 'widgets/variants/modal.js', output: 'public/widget-modal.js', globalName: 'ChangerawrWidget' },\n            { name: 'announcement', entry: 'widgets/variants/announcement.js', output: 'public/widget-announcement.js', globalName: 'ChangerawrWidget' },\n        ];\n\n        // Build JavaScript bundles\n        for (const variant of variants) {\n            await esbuild.build({\n                entryPoints: [variant.entry],\n                bundle: true,\n                minify: true,\n                sourcemap: true,\n                target: ['es2018'],\n                format: 'iife',\n                // Don't use globalName - let the widget code set window.ChangerawrWidget directly\n                outfile: variant.output,\n                define: {\n                    'process.env.NEXT_PUBLIC_APP_URL': `\"${process.env.NEXT_PUBLIC_APP_URL}\"`,\n                },\n            });\n\n            console.log(`✅ ${variant.name.charAt(0).toUpperCase() + variant.name.slice(1)} variant built successfully`);\n        }\n\n        // Copy CSS files to public directory\n        const cssSourceDir = 'widgets/core/styles';\n        const cssDestDir = 'public/widgets/core/styles';\n\n        console.log('\\n📦 Copying CSS files...');\n        copyDirectory(cssSourceDir, cssDestDir);\n        console.log(`✅ CSS files copied to ${cssDestDir}`);\n\n    } catch (error) {\n        console.error('❌ Widget build failed:', error);\n        process.exit(1);\n    }\n}\n\nbuildWidget();"
  },
  {
    "path": "tailwind.config.ts",
    "content": "import type {Config} from \"tailwindcss\";\n\nexport default {\n    darkMode: [\"class\"],\n    // Safelist from @changerawr/markdown v1.2.0 — avoids module loading issues in jiti/Docker\n    safelist: [\n        \"text-3xl\", \"text-2xl\", \"text-xl\", \"text-lg\", \"text-base\", \"text-sm\",\n        \"font-bold\", \"font-semibold\", \"font-medium\", \"font-mono\",\n        \"italic\", \"underline\", \"line-through\", \"leading-7\", \"leading-relaxed\",\n        \"mt-8\", \"mt-6\", \"mt-5\", \"mt-4\", \"mt-3\", \"mt-2\",\n        \"mb-6\", \"mb-4\", \"mb-3\", \"mb-2\", \"my-6\", \"my-4\", \"my-2\",\n        \"p-4\", \"p-6\", \"p-2\", \"px-1.5\", \"px-2\", \"px-3\", \"px-4\", \"px-6\",\n        \"py-0.5\", \"py-1\", \"py-2\", \"pl-4\", \"pl-6\",\n        \"flex\", \"inline-flex\", \"items-center\", \"justify-center\", \"gap-2\", \"space-y-1\",\n        \"relative\", \"group\", \"list-disc\", \"list-decimal\", \"list-inside\", \"ml-4\",\n        \"border-l-2\", \"border-l-4\", \"border-t\", \"border-border\", \"border-muted-foreground\",\n        \"rounded\", \"rounded-lg\", \"rounded-md\", \"bg-muted\", \"max-w-full\", \"h-auto\",\n        \"overflow-x-auto\", \"hover:underline\", \"hover:opacity-100\", \"opacity-0\",\n        \"group-hover:opacity-100\", \"transition-opacity\", \"transition-colors\", \"transition-all\", \"duration-200\",\n        \"text-muted-foreground\", \"text-primary\",\n        \"bg-blue-500/10\", \"border-blue-500/30\", \"text-blue-600\", \"border-l-blue-500\",\n        \"bg-amber-500/10\", \"border-amber-500/30\", \"text-amber-600\", \"border-l-amber-500\",\n        \"bg-red-500/10\", \"border-red-500/30\", \"text-red-600\", \"border-l-red-500\",\n        \"bg-green-500/10\", \"border-green-500/30\", \"text-green-600\", \"border-l-green-500\",\n        \"bg-blue-600\", \"text-white\", \"hover:bg-blue-700\",\n        \"bg-gray-200\", \"text-gray-900\", \"hover:bg-gray-300\",\n        \"dark:text-blue-400\", \"dark:text-amber-400\", \"dark:text-red-400\", \"dark:text-green-400\",\n        \"dark:bg-gray-800\", \"dark:text-gray-100\",\n    ],\n    content: [\n        \"./pages/**/*.{js,ts,jsx,tsx,mdx}\",\n        \"./components/**/*.{js,ts,jsx,tsx,mdx}\",\n        \"./app/**/*.{js,ts,jsx,tsx,mdx}\",\n        \"./lib/services/core/markdown/extensions/**/*.{js,ts}\",\n    ],\n    theme: {\n        extend: {\n            colors: {\n                background: 'hsl(var(--background))',\n                foreground: 'hsl(var(--foreground))',\n                card: {\n                    DEFAULT: 'hsl(var(--card))',\n                    foreground: 'hsl(var(--card-foreground))'\n                },\n                popover: {\n                    DEFAULT: 'hsl(var(--popover))',\n                    foreground: 'hsl(var(--popover-foreground))'\n                },\n                primary: {\n                    DEFAULT: 'hsl(var(--primary))',\n                    foreground: 'hsl(var(--primary-foreground))'\n                },\n                secondary: {\n                    DEFAULT: 'hsl(var(--secondary))',\n                    foreground: 'hsl(var(--secondary-foreground))'\n                },\n                muted: {\n                    DEFAULT: 'hsl(var(--muted))',\n                    foreground: 'hsl(var(--muted-foreground))'\n                },\n                accent: {\n                    DEFAULT: 'hsl(var(--accent))',\n                    foreground: 'hsl(var(--accent-foreground))'\n                },\n                destructive: {\n                    DEFAULT: 'hsl(var(--destructive))',\n                    foreground: 'hsl(var(--destructive-foreground))'\n                },\n                border: 'hsl(var(--border))',\n                input: 'hsl(var(--input))',\n                ring: 'hsl(var(--ring))',\n                chart: {\n                    '1': 'hsl(var(--chart-1))',\n                    '2': 'hsl(var(--chart-2))',\n                    '3': 'hsl(var(--chart-3))',\n                    '4': 'hsl(var(--chart-4))',\n                    '5': 'hsl(var(--chart-5))'\n                }\n            },\n            borderRadius: {\n                lg: 'var(--radius)',\n                md: 'calc(var(--radius) - 2px)',\n                sm: 'calc(var(--radius) - 4px)'\n            },\n            keyframes: {\n                'accordion-down': {\n                    from: {\n                        height: '0'\n                    },\n                    to: {\n                        height: 'var(--radix-accordion-content-height)'\n                    }\n                },\n                'accordion-up': {\n                    from: {\n                        height: 'var(--radix-accordion-content-height)'\n                    },\n                    to: {\n                        height: '0'\n                    }\n                }\n            },\n            animation: {\n                'accordion-down': 'accordion-down 0.2s ease-out',\n                'accordion-up': 'accordion-up 0.2s ease-out'\n            }\n        }\n    },\n    plugins: [require(\"tailwindcss-animate\"), require('@tailwindcss/forms')],\n} satisfies Config;\n"
  },
  {
    "path": "temp_migration.sql",
    "content": "-- This is an empty migration.\n\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\n        \"./*\"\n      ]\n    }\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\",\n    \".next/dev/types/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "useful-information-for-development/slack_scopes.md",
    "content": "| Domain     | Scope                          | Description                                                                 | Risk   |\n|------------|--------------------------------|-----------------------------------------------------------------------------|--------|\n| slack.com  | admin                          | Administer a workspace                                                      | High   |\n| slack.com  | admin.analytics:read           | Access analytics data about the organization                                | Medium |\n| slack.com  | admin.apps:read                | View apps and app requests in a workspace                                   | Medium |\n| slack.com  | admin.apps:write               | Manage apps in a workspace                                                  | High   |\n| slack.com  | admin.barriers:read            | Read information barriers in the organization                               | Medium |\n| slack.com  | admin.barriers:write           | Manage information barriers in the organization                             | High   |\n| slack.com  | admin.conversations:read       | View the channel’s member list, topic, purpose and channel name             | Medium |\n| slack.com  | admin.conversations:write      | Start a new conversation, modify a conversation and modify channel details  | High   |\n| slack.com  | admin.invites:read             | Gain information about invite requests in a Grid organization               | Medium |\n| slack.com  | admin.invites:write            | Approve or deny invite requests in a Grid organization                      | High   |\n| slack.com  | admin.teams:read               | Access information about a workspace                                        | Medium |\n| slack.com  | admin.teams:write              | Make changes to a workspace                                                 | High   |\n| slack.com  | admin.usergroups:read          | Access information about user groups                                        | Medium |\n| slack.com  | admin.usergroups:write         | Make changes to your usergroups                                             | High   |\n| slack.com  | admin.users:read               | Access a workspace’s profile information                                    | Medium |\n| slack.com  | admin.users:write              | Modify account information                                                  | High   |\n| slack.com  | app_configurations:read        | Read app configuration info via App Manifest APIs                           | Medium |\n| slack.com  | app_configurations:write       | Write app configuration info and create apps via App Manifest APIs          | High   |\n| slack.com  | app_mentions:read              | View messages that directly mention @your_slack_app                         | Low    |\n| slack.com  | auditlogs:read                 | View events from all workspaces, channels and users                         | Medium |\n| slack.com  | authorizations:read            | List authorizations associated with Events API                              | Medium |\n| slack.com  | calls:read                     | View information about ongoing and past calls                               | Medium |\n| slack.com  | calls:write                    | Start and manage calls in a workspace                                       | Medium |\n| slack.com  | channels:history               | View messages and content in public channels your app is added to           | Medium |\n| slack.com  | channels:join                  | Join public channels in a workspace                                         | Medium |\n| slack.com  | channels:manage                | Manage public channels and create new ones                                  | Medium |\n| slack.com  | channels:read                  | View basic information about public channels                                | Low    |\n| slack.com  | channels:write                 | Manage a user’s public channels and create new ones                         | Low    |\n| slack.com  | chat:write                     | Post messages in approved channels & conversations                          | Medium |\n| slack.com  | chat:write.customize           | Send messages with customized username and avatar                           | Medium |\n| slack.com  | chat:write.public              | Send messages to channels your app isn't a member of                        | Medium |\n| slack.com  | chat:write                     | Send messages as your slack app                                             | Medium |\n| slack.com  | chat:write                     | Send messages on a user’s behalf                                            | High   |\n| slack.com  | commands                       | Add shortcuts and slash commands                                            | Low    |\n| slack.com  | connections:write              | Generate websocket URIs for Socket Mode                                     | Medium |\n| slack.com  | conversations.connect:manage   | Manage Slack Connect channels                                               | Medium |\n| slack.com  | conversations.connect:read     | Receive Slack Connect invite events                                         | Medium |\n| slack.com  | conversations.connect:write    | Create and accept Slack Connect invitations                                 | Medium |\n| slack.com  | dnd:read                       | View Do Not Disturb settings                                                | Low    |\n| slack.com  | dnd:write                      | Edit Do Not Disturb settings                                                | Low    |\n| slack.com  | email                          | View a user’s email address                                                 | Low    |\n| slack.com  | emoji:read                     | View custom emoji                                                           | Low    |\n| slack.com  | files:read                     | View files shared in channels your app is in                                | Medium |\n| slack.com  | files:write                    | Upload, edit, and delete files                                              | Medium |\n| slack.com  | groups:history                 | View messages in private channels your app is added to                      | Medium |\n| slack.com  | groups:read                    | View basic info about private channels                                      | Low    |\n| slack.com  | groups:write                   | Manage private channels and create new ones                                 | Low    |\n| slack.com  | identify                       | View a user’s identity                                                      | Low    |\n| slack.com  | identity.avatar                | View a user’s Slack avatar                                                  | Low    |\n| slack.com  | identity.avatar:read           | View the user's profile picture                                             | Low    |\n| slack.com  | identity.basic                 | View identity info                                                          | Low    |\n| slack.com  | identity.email                 | View a user’s email address                                                 | Low    |\n| slack.com  | identity.team                  | View a user’s workspace name                                                | Low    |\n| slack.com  | identity.team:read             | View workspace name, domain, and icon                                       | Low    |\n| slack.com  | im:history                     | View direct messages your app is in                                         | Medium |\n| slack.com  | im:read                        | View basic information about direct messages                                | Low    |\n| slack.com  | im:write                       | Start direct messages                                                       | Medium |\n| slack.com  | incoming-webhook               | Create incoming webhooks                                                    | Medium |\n| slack.com  | links:read                     | View URLs in messages                                                       | Medium |\n| slack.com  | links:write                    | Show previews of URLs                                                       | Medium |\n| slack.com  | mpim:history                   | View content in group direct messages                                       | Low    |\n| slack.com  | mpim:read                      | View basic info about group direct messages                                 | Low    |\n| slack.com  | mpim:write                     | Start group direct messages                                                 | Medium |\n| slack.com  | pins:read                      | View pinned content                                                         | Low    |\n| slack.com  | pins:write                     | Add and remove pinned content                                               | Low    |\n| slack.com  | profile                        | View avatar and workspace info                                              | Low    |\n| slack.com  | reactions:read                 | View emoji reactions                                                        | Low    |\n| slack.com  | reactions:write                | Add and edit emoji reactions                                                | Low    |\n| slack.com  | reminders:read                 | View reminders                                                              | Low    |\n| slack.com  | reminders:write                | Add, remove, or complete reminders                                          | Low    |\n| slack.com  | remote_files:read              | View remote files                                                           | Medium |\n| slack.com  | remote_files:share             | Share remote files on a user's behalf                                       | Medium |\n| slack.com  | remote_files:write             | Add, edit, and delete remote files                                          | Medium |\n| slack.com  | search:read                    | Search workspace content                                                    | Medium |\n| slack.com  | stars:read                     | View starred messages and files                                             | Low    |\n| slack.com  | stars:write                    | Add or remove stars                                                         | Low    |\n| slack.com  | team.billing:read              | Read billing plan                                                           | Medium |\n| slack.com  | team.preferences:read          | Read workspace preferences                                                  | Medium |\n| slack.com  | team:read                      | View workspace name, email domain and icon                                  | Medium |\n| slack.com  | tokens.basic                   | Execute methods without needing a scope                                     | High   |\n| slack.com  | usergroups:read                | View user groups                                                            | Low    |\n| slack.com  | usergroups:write               | Create and manage user groups                                               | Medium |\n| slack.com  | users.profile:read             | View profile details                                                        | Low    |\n| slack.com  | users.profile:write            | Edit a user’s profile and status                                            | Medium |\n| slack.com  | users:read                     | View people in a workspace                                                  | Medium |\n| slack.com  | users:read.email               | View emails of people                                                       | Medium |\n| slack.com  | users:write                    | Set presence for your slack app                                             | Low    |\n| slack.com  | workflow.steps:execute         | Add steps for Workflow Builder                                              | Medium |\n"
  },
  {
    "path": "widgets/changelog/index.js",
    "content": "class ChangelogWidget {\n    constructor(container, options) {\n        const scriptOptions = this.getScriptOptions();\n\n        this.container = container;\n        this.options = {\n            theme: 'light',\n            maxHeight: '400px',\n            position: 'bottom-right',\n            isPopup: false,\n            maxEntries: 3,\n            hidden: false,\n            ...scriptOptions,\n            ...options\n        };\n\n        this.isOpen = false;\n        this.isLoading = false;\n        this.init();\n    }\n\n    getScriptOptions() {\n        const currentScript = document.currentScript;\n        if (!currentScript) return {};\n\n        return {\n            theme: currentScript.getAttribute('data-theme'),\n            position: currentScript.getAttribute('data-position'),\n            maxHeight: currentScript.getAttribute('data-max-height'),\n            isPopup: currentScript.getAttribute('data-popup') === 'true',\n            trigger: currentScript.getAttribute('data-trigger'),\n            maxEntries: currentScript.getAttribute('data-max-entries')\n                ? parseInt(currentScript.getAttribute('data-max-entries'), 10)\n                : undefined,\n            hidden: currentScript.getAttribute('data-popup') === 'true'\n        };\n    }\n\n    updatePosition() {\n        if (!this.options.isPopup) return;\n\n        // Reset all position styles first\n        this.container.style.removeProperty('top');\n        this.container.style.removeProperty('bottom');\n        this.container.style.removeProperty('left');\n        this.container.style.removeProperty('right');\n\n        // Apply new position based on option\n        switch (this.options.position) {\n            case 'top-right':\n                this.container.style.setProperty('top', '20px', 'important');\n                this.container.style.setProperty('right', '20px', 'important');\n                break;\n            case 'top-left':\n                this.container.style.setProperty('top', '20px', 'important');\n                this.container.style.setProperty('left', '20px', 'important');\n                break;\n            case 'bottom-left':\n                this.container.style.setProperty('bottom', '20px', 'important');\n                this.container.style.setProperty('left', '20px', 'important');\n                break;\n            case 'bottom-right':\n            default:\n                this.container.style.setProperty('bottom', '20px', 'important');\n                this.container.style.setProperty('right', '20px', 'important');\n                break;\n        }\n    }\n\n    addStyles() {\n        const styles = `\n            .changerawr-widget {\n                font-family: system-ui, -apple-system, sans-serif;\n                font-size: 14px;\n                line-height: 1.5;\n                color: #1a1a1a;\n                background: #ffffff;\n                border-radius: 8px;\n                box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n                width: 300px;\n                overflow: hidden;\n                opacity: 1;\n                transform: translateY(0);\n                transition: opacity 0.2s ease, transform 0.2s ease;\n            }\n            \n            .changerawr-widget.popup {\n                position: fixed !important;\n                z-index: 9999 !important;\n                opacity: 0;\n                transform: translateY(20px);\n                pointer-events: none;\n                transition: opacity 0.2s ease, transform 0.2s ease;\n            }\n            \n            /* Position-specific transforms */\n            .changerawr-widget.popup[data-position=\"top-right\"],\n            .changerawr-widget.popup[data-position=\"top-left\"] {\n                transform: translateY(-20px);\n            }\n\n            .changerawr-widget.popup[data-position=\"bottom-right\"],\n            .changerawr-widget.popup[data-position=\"bottom-left\"] {\n                transform: translateY(20px);\n            }\n\n            .changerawr-widget.popup.open {\n                opacity: 1 !important;\n                transform: translateY(0) !important;\n                pointer-events: all !important;\n            }\n\n            .changerawr-widget.hidden {\n                display: none !important;\n            }\n\n            .changerawr-widget.dark {\n                color: #ffffff;\n                background: #1a1a1a;\n            }\n\n            .changerawr-header {\n                padding: 12px 16px;\n                border-bottom: 1px solid #eaeaea;\n                font-weight: 600;\n                display: flex;\n                justify-content: space-between;\n                align-items: center;\n            }\n\n            .changerawr-close {\n                background: none;\n                border: none;\n                padding: 4px;\n                cursor: pointer;\n                color: inherit;\n                opacity: 0.6;\n                transition: opacity 0.2s;\n            }\n\n            .changerawr-close:hover {\n                opacity: 1;\n            }\n\n            .changerawr-close:focus {\n                outline: 2px solid #0066ff;\n                border-radius: 4px;\n            }\n\n            .dark .changerawr-header {\n                border-color: #333;\n            }\n\n            .changerawr-entries {\n                max-height: var(--max-height, 400px);\n                overflow-y: auto;\n                padding: 8px 0;\n            }\n\n            .changerawr-entry {\n                padding: 8px 16px;\n                border-bottom: 1px solid #f5f5f5;\n                opacity: 0;\n                transform: translateY(10px);\n                animation: slideIn 0.3s ease forwards;\n            }\n\n            .changerawr-entry:nth-child(2) {\n                animation-delay: 0.1s;\n            }\n\n            .changerawr-entry:nth-child(3) {\n                animation-delay: 0.2s;\n            }\n\n            @keyframes slideIn {\n                to {\n                    opacity: 1;\n                    transform: translateY(0);\n                }\n            }\n\n            .dark .changerawr-entry {\n                border-color: #333;\n            }\n\n            .changerawr-entry:last-child {\n                border: none;\n            }\n\n            .changerawr-entry:focus-within {\n                background: #f5f5f5;\n            }\n\n            .dark .changerawr-entry:focus-within {\n                background: #333;\n            }\n\n            .changerawr-tag {\n                display: inline-block;\n                padding: 2px 8px;\n                background: #e8f2ff;\n                color: #0066ff;\n                border-radius: 4px;\n                font-size: 12px;\n                margin-bottom: 4px;\n            }\n\n            .dark .changerawr-tag {\n                background: #1a365d;\n                color: #60a5fa;\n            }\n\n            .changerawr-entry-title {\n                font-weight: 500;\n                margin-bottom: 4px;\n            }\n\n            .changerawr-entry-content {\n                color: #666;\n                font-size: 13px;\n                display: -webkit-box;\n                -webkit-line-clamp: 3;\n                -webkit-box-orient: vertical;\n                overflow: hidden;\n                text-overflow: ellipsis;\n                margin-bottom: 8px;\n            }\n\n            .dark .changerawr-entry-content {\n                color: #999;\n            }\n\n            .changerawr-read-more {\n                color: #0066ff;\n                text-decoration: none;\n                font-size: 12px;\n                display: inline-block;\n                margin-top: 4px;\n                padding: 2px;\n            }\n\n            .changerawr-read-more:focus {\n                outline: 2px solid #0066ff;\n                border-radius: 4px;\n            }\n\n            .dark .changerawr-read-more {\n                color: #60a5fa;\n            }\n\n            .changerawr-read-more:hover {\n                text-decoration: underline;\n            }\n\n            .changerawr-loading {\n                display: flex;\n                justify-content: center;\n                align-items: center;\n                height: 100px;\n            }\n\n            .changerawr-spinner {\n                width: 24px;\n                height: 24px;\n                border: 2px solid #f3f3f3;\n                border-top: 2px solid #0066ff;\n                border-radius: 50%;\n                animation: spin 1s linear infinite;\n            }\n\n            @keyframes spin {\n                0% { transform: rotate(0deg); }\n                100% { transform: rotate(360deg); }\n            }\n\n            .dark .changerawr-spinner {\n                border-color: #333;\n                border-top-color: #60a5fa;\n            }\n\n            .changerawr-footer {\n                padding: 8px 16px;\n                border-top: 1px solid #eaeaea;\n                font-size: 12px;\n                color: #666;\n                display: flex;\n                justify-content: space-between;\n                align-items: center;\n            }\n\n            .dark .changerawr-footer {\n                border-color: #333;\n                color: #999;\n            }\n\n            .changerawr-footer a {\n                color: inherit;\n                text-decoration: none;\n            }\n\n            .changerawr-footer a:hover {\n                text-decoration: underline;\n            }\n        `;\n\n        const styleSheet = document.createElement('style');\n        styleSheet.textContent = styles;\n        document.head.appendChild(styleSheet);\n    }\n\n    async init() {\n        this.addStyles();\n\n        // Combine classes into a single assignment\n        let classes = `changerawr-widget ${this.options.theme}`;\n        if (this.options.isPopup) {\n            classes += ' popup';\n        }\n        if (this.options.hidden) {\n            classes += ' hidden';\n        }\n        this.container.className = classes;\n\n        this.container.style.setProperty('--max-height', this.options.maxHeight);\n        this.container.setAttribute('role', 'dialog');\n        this.container.setAttribute('aria-label', 'Changelog updates');\n\n        if (this.options.isPopup) {\n            this.updatePosition();\n        }\n\n        this.render();\n        await this.loadEntries();\n        this.setupKeyboardNavigation();\n\n        if (this.options.trigger) {\n            this.setupTriggerButton();\n        }\n    }\n\n    setupKeyboardNavigation() {\n        this.container.addEventListener('keydown', (e) => {\n            if (e.key === 'Escape' && this.isOpen) {\n                this.close();\n            }\n\n            if (e.key === 'Tab') {\n                const focusableElements = this.container.querySelectorAll(\n                    'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n                );\n                const firstElement = focusableElements[0];\n                const lastElement = focusableElements[focusableElements.length - 1];\n\n                if (e.shiftKey && document.activeElement === firstElement) {\n                    e.preventDefault();\n                    lastElement.focus();\n                } else if (!e.shiftKey && document.activeElement === lastElement) {\n                    e.preventDefault();\n                    firstElement.focus();\n                }\n            }\n        });\n    }\n\n    setupTriggerButton() {\n        const triggerButton = document.getElementById(this.options.trigger);\n        if (!triggerButton) {\n            console.warn(`Changerawr: Trigger button with ID '${this.options.trigger}' not found`);\n            return;\n        }\n\n        triggerButton.setAttribute('aria-expanded', 'false');\n        triggerButton.setAttribute('aria-haspopup', 'dialog');\n        triggerButton.setAttribute('aria-controls', this.container.id);\n\n        triggerButton.addEventListener('click', () => {\n            this.toggle();\n            triggerButton.setAttribute('aria-expanded', this.isOpen.toString());\n        });\n\n        triggerButton.addEventListener('keydown', (e) => {\n            if (e.key === 'Enter' || e.key === ' ') {\n                e.preventDefault();\n                this.toggle();\n                triggerButton.setAttribute('aria-expanded', this.isOpen.toString());\n            }\n        });\n    }\n\n    render() {\n        const header = document.createElement('div');\n        header.className = 'changerawr-header';\n        header.innerHTML = `\n            <span>Latest Updates</span>\n            ${this.options.isPopup ? `\n                <button \n                    class=\"changerawr-close\" \n                    aria-label=\"Close changelog\"\n                >\n                    <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\">\n                        <path \n                            fill=\"currentColor\" \n                            d=\"M8 6.586L4.707 3.293 3.293 4.707 6.586 8l-3.293 3.293 1.414 1.414L8 9.414l3.293 3.293 1.414-1.414L9.414 8l3.293-3.293-1.414-1.414L8 6.586z\"\n                        />\n                    </svg>\n                </button>\n            ` : ''}\n        `;\n        this.container.appendChild(header);\n\n        const entries = document.createElement('div');\n        entries.className = 'changerawr-entries';\n        entries.setAttribute('role', 'list');\n        this.container.appendChild(entries);\n\n        const footer = document.createElement('div');\n        footer.className = 'changerawr-footer';\n        footer.innerHTML = `\n            <span>Powered by Changerawr</span>\n            <a href=\"${process.env.NEXT_PUBLIC_APP_URL}/changelog/${this.options.projectId}/rss.xml\" target=\"_blank\" rel=\"noopener noreferrer\">RSS</a>\n        `;\n        this.container.appendChild(footer);\n\n        this.renderLoading();\n\n        const closeButton = this.container.querySelector('.changerawr-close');\n        if (closeButton) {\n            closeButton.addEventListener('click', () => this.close());\n        }\n    }\n\n    renderLoading() {\n        const container = this.container.querySelector('.changerawr-entries');\n        container.innerHTML = `\n            <div class=\"changerawr-loading\">\n                <div class=\"changerawr-spinner\" role=\"status\"></div>\n            </div>\n        `;\n    }\n\n    async loadEntries() {\n        this.isLoading = true;\n        try {\n            const response = await fetch(\n                `${process.env.NEXT_PUBLIC_APP_URL}/api/changelog/${this.options.projectId}/entries`\n            );\n\n            if (!response.ok) {\n                throw new Error('Failed to fetch entries');\n            }\n\n            const data = await response.json();\n            this.renderEntries(data.items);\n        } catch (error) {\n            console.error('Failed to load changelog:', error);\n            this.renderError();\n        } finally {\n            this.isLoading = false;\n        }\n    }\n\n    renderEntries(entries) {\n        const container = this.container.querySelector('.changerawr-entries');\n        container.innerHTML = '';\n\n        const entriesToShow = entries.slice(0, this.options.maxEntries);\n\n        entriesToShow.forEach((entry) => {\n            const entryEl = document.createElement('div');\n            entryEl.className = 'changerawr-entry';\n            entryEl.setAttribute('role', 'listitem');\n            entryEl.setAttribute('tabindex', '0');\n\n            if (entry.tags?.length) {\n                const tagEl = document.createElement('div');\n                tagEl.className = 'changerawr-tag';\n                tagEl.textContent = entry.tags[0].name;\n                entryEl.appendChild(tagEl);\n            }\n\n            const title = document.createElement('div');\n            title.className = 'changerawr-entry-title';\n            title.textContent = entry.title;\n            entryEl.appendChild(title);\n\n            const content = document.createElement('div');\n            content.className = 'changerawr-entry-content';\n            content.textContent = entry.content;\n            entryEl.appendChild(content);\n\n            const readMore = document.createElement('a');\n            readMore.href = `${process.env.NEXT_PUBLIC_APP_URL}/changelog/${this.options.projectId}#${entry.id}`;\n            readMore.className = 'changerawr-read-more';\n            readMore.textContent = 'Read more';\n            readMore.target = '_blank';\n            readMore.setAttribute('aria-label', `Read more about ${entry.title}`);\n            entryEl.appendChild(readMore);\n\n            container.appendChild(entryEl);\n        });\n    }\n\n    renderError() {\n        const container = this.container.querySelector('.changerawr-entries');\n        container.innerHTML = `\n            <div class=\"changerawr-error\">\n                Failed to load changelog entries\n                <br>\n                <button class=\"changerawr-retry\">\n                    Try Again\n                </button>\n            </div>\n        `;\n\n        const retryButton = container.querySelector('.changerawr-retry');\n        retryButton.addEventListener('click', () => this.loadEntries());\n    }\n\n    open() {\n        if (!this.options.isPopup) return;\n\n        this.isOpen = true;\n        this.container.classList.remove('hidden');\n        this.container.style.display = 'block';\n\n        requestAnimationFrame(() => {\n            this.container.classList.add('open');\n        });\n\n        this.previouslyFocused = document.activeElement;\n        const firstFocusable = this.container.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])');\n        if (firstFocusable) {\n            firstFocusable.focus();\n        }\n    }\n\n    close() {\n        if (!this.options.isPopup) return;\n\n        this.isOpen = false;\n        this.container.classList.remove('open');\n\n        const handleTransitionEnd = () => {\n            if (!this.isOpen) {\n                if (this.options.hidden) {\n                    this.container.classList.add('hidden');\n                }\n                this.container.style.display = 'none';\n            }\n            this.container.removeEventListener('transitionend', handleTransitionEnd);\n        };\n\n        this.container.addEventListener('transitionend', handleTransitionEnd);\n\n        if (this.previouslyFocused) {\n            this.previouslyFocused.focus();\n        }\n    }\n\n    toggle() {\n        if (this.isOpen) {\n            this.close();\n        } else {\n            this.open();\n        }\n    }\n}\n\n// Initialize scripts on page load\ndocument.addEventListener('DOMContentLoaded', () => {\n    const scripts = document.querySelectorAll('script[src*=\"/api/integrations/widget/\"]');\n    scripts.forEach(currentScript => {\n        const projectIdMatch = currentScript.getAttribute('src').match(/\\/api\\/widget\\/([^?]+)/);\n        if (!projectIdMatch) return;\n\n        const projectId = projectIdMatch[1];\n        const position = currentScript.getAttribute('data-position') || 'bottom-right';\n\n        // Validate position\n        if (!['top-right', 'top-left', 'bottom-right', 'bottom-left'].includes(position)) {\n            console.warn(`Invalid position '${position}', defaulting to bottom-right`);\n        }\n\n        const container = document.createElement('div');\n        container.id = `changerawr-widget-${Math.random().toString(36).substr(2, 9)}`;\n\n        // Insert container based on popup state\n        const isPopup = currentScript.getAttribute('data-popup') === 'true';\n        if (isPopup) {\n            document.body.appendChild(container);\n        } else {\n            currentScript.parentNode.insertBefore(container, currentScript);\n        }\n\n        // Initialize widget with correct positioning\n        // eslint-disable-next-line @typescript-eslint/no-unused-vars\n        const widget = new ChangelogWidget(container, {\n            projectId,\n            theme: currentScript.getAttribute('data-theme') || 'light',\n            position: currentScript.getAttribute('data-position') || 'bottom-right',\n            isPopup,\n            trigger: currentScript.getAttribute('data-trigger'),\n            maxEntries: currentScript.getAttribute('data-max-entries')\n                ? parseInt(currentScript.getAttribute('data-max-entries'), 10)\n                : 3,\n            hidden: isPopup // Only hide initially if it's a popup\n        });\n    });\n});\n\n// Global initialization method\nwindow.ChangerawrWidget = {\n    init: (config) => {\n        if (!config.container) {\n            throw new Error('Container element is required');\n        }\n\n        if (!config.projectId) {\n            throw new Error('Project ID is required');\n        }\n\n        // Validate position\n        const position = config.position || 'bottom-right';\n        if (!['top-right', 'top-left', 'bottom-right', 'bottom-left'].includes(position)) {\n            console.warn(`Invalid position '${position}', defaulting to bottom-right`);\n        }\n\n        config.container.id = config.container.id ||\n            `changerawr-widget-${Math.random().toString(36).substr(2, 9)}`;\n\n        // Always ensure the container is properly positioned if it's a popup\n        if (config.isPopup) {\n            document.body.appendChild(config.container);\n        }\n\n        return new ChangelogWidget(config.container, {\n            projectId: config.projectId,\n            theme: config.theme || 'light',\n            maxHeight: config.maxHeight || '400px',\n            position: config.position || 'bottom-right',\n            isPopup: config.isPopup || false,\n            maxEntries: config.maxEntries || 3,\n            hidden: config.isPopup || false, // Only hide initially if it's a popup\n            trigger: config.trigger\n        });\n    }\n};"
  },
  {
    "path": "widgets/core/styles/announcement.css",
    "content": "/**\n * Announcement Bar Widget Styles\n */\n\n.changerawr-announcement {\n    position: fixed;\n    left: 0;\n    width: 100%;\n    z-index: var(--changerawr-z-index, 9999);\n    transition: transform var(--changerawr-animation-duration, 0.3s) var(--changerawr-animation-easing, ease);\n}\n\n.changerawr-announcement-top {\n    top: 0;\n}\n\n.changerawr-announcement-bottom {\n    bottom: 0;\n}\n\n.changerawr-announcement-hidden {\n    transform: translateY(-100%);\n}\n\n.changerawr-announcement-bottom.changerawr-announcement-hidden {\n    transform: translateY(100%);\n}\n\n/* Announcement bar */\n.changerawr-announcement-bar {\n    display: flex;\n    align-items: center;\n    gap: var(--changerawr-spacing-md, 0.75rem);\n    padding: var(--changerawr-spacing-md, 0.75rem) var(--changerawr-spacing-lg, 1.5rem);\n    background: var(--changerawr-primary-color, #0066ff);\n    color: #ffffff;\n    box-shadow: var(--changerawr-shadow-md, 0 4px 12px rgba(0, 0, 0, 0.1));\n}\n\n/* Icon */\n.changerawr-announcement-icon {\n    font-size: 1.2em;\n    flex-shrink: 0;\n}\n\n/* Content */\n.changerawr-announcement-content {\n    flex: 1;\n    display: flex;\n    align-items: center;\n    gap: var(--changerawr-spacing-sm, 0.5rem);\n    flex-wrap: wrap;\n    min-width: 0;\n}\n\n.changerawr-announcement-label {\n    font-weight: 700;\n    font-size: var(--changerawr-font-size-sm, 12px);\n    text-transform: uppercase;\n    letter-spacing: 0.05em;\n    flex-shrink: 0;\n}\n\n.changerawr-announcement-title {\n    font-size: var(--changerawr-font-size-base, 14px);\n    font-weight: 500;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n\n.changerawr-announcement-tag {\n    padding: 2px 8px;\n    background: rgba(255, 255, 255, 0.2);\n    border-radius: var(--changerawr-border-radius-sm, 4px);\n    font-size: var(--changerawr-font-size-xs, 11px);\n    font-weight: 600;\n    text-transform: uppercase;\n    letter-spacing: 0.05em;\n    flex-shrink: 0;\n}\n\n/* Actions */\n.changerawr-announcement-actions {\n    display: flex;\n    align-items: center;\n    gap: var(--changerawr-spacing-md, 0.75rem);\n    flex-shrink: 0;\n}\n\n.changerawr-announcement-link {\n    color: #ffffff;\n    font-size: var(--changerawr-font-size-sm, 12px);\n    font-weight: 600;\n    text-decoration: none;\n    white-space: nowrap;\n    padding: 4px 12px;\n    background: rgba(255, 255, 255, 0.15);\n    border-radius: var(--changerawr-border-radius-sm, 4px);\n    transition: background var(--changerawr-animation-duration, 0.3s) var(--changerawr-animation-easing, ease);\n}\n\n.changerawr-announcement-link:hover {\n    background: rgba(255, 255, 255, 0.25);\n}\n\n.changerawr-announcement-dismiss {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 28px;\n    height: 28px;\n    padding: 0;\n    background: rgba(255, 255, 255, 0.15);\n    border: none;\n    border-radius: var(--changerawr-border-radius-sm, 4px);\n    color: #ffffff;\n    font-size: 16px;\n    cursor: pointer;\n    transition: all var(--changerawr-animation-duration, 0.3s) var(--changerawr-animation-easing, ease);\n}\n\n.changerawr-announcement-dismiss:hover {\n    background: rgba(255, 255, 255, 0.25);\n    transform: scale(1.1);\n}\n\n/* Dark mode */\n.changerawr-announcement.dark .changerawr-announcement-bar {\n    background: var(--changerawr-primary-color-dark, #3b82f6);\n}\n\n/* Theme variations */\n.changerawr-announcement-bar[data-variant=\"success\"] {\n    background: var(--changerawr-success-color, #10b981);\n}\n\n.changerawr-announcement-bar[data-variant=\"warning\"] {\n    background: var(--changerawr-warning-color, #f59e0b);\n}\n\n.changerawr-announcement-bar[data-variant=\"error\"] {\n    background: var(--changerawr-error-color, #ef4444);\n}\n\n.changerawr-announcement-bar[data-variant=\"info\"] {\n    background: var(--changerawr-info-color, #0ea5e9);\n}\n\n/* Mobile responsiveness */\n@media (max-width: 640px) {\n    .changerawr-announcement-bar {\n        padding: var(--changerawr-spacing-sm, 0.5rem) var(--changerawr-spacing-md, 0.75rem);\n        gap: var(--changerawr-spacing-sm, 0.5rem);\n    }\n\n    .changerawr-announcement-icon {\n        font-size: 1em;\n    }\n\n    .changerawr-announcement-label {\n        display: none;\n    }\n\n    .changerawr-announcement-title {\n        font-size: var(--changerawr-font-size-sm, 12px);\n    }\n\n    .changerawr-announcement-tag {\n        display: none;\n    }\n\n    .changerawr-announcement-link {\n        padding: 4px 8px;\n        font-size: var(--changerawr-font-size-xs, 11px);\n    }\n\n    .changerawr-announcement-dismiss {\n        width: 24px;\n        height: 24px;\n        font-size: 14px;\n    }\n}\n\n/* Reduce motion */\n@media (prefers-reduced-motion: reduce) {\n    .changerawr-announcement {\n        transition: none;\n    }\n}\n"
  },
  {
    "path": "widgets/core/styles/common.css",
    "content": "/**\n * Changerawr Widget Common Styles\n *\n * Shared component styles used across all widget variants.\n * Import variables.css and reset.css before this file.\n */\n\n/* ============================================\n   BASE WIDGET CONTAINER\n   ============================================ */\n.changerawr-widget {\n    font-family: var(--changerawr-font-family);\n    font-size: var(--changerawr-font-size-base);\n    line-height: var(--changerawr-line-height);\n    color: var(--changerawr-text-primary);\n    background: var(--changerawr-bg-primary);\n    border-radius: var(--changerawr-border-radius);\n    box-shadow: var(--changerawr-shadow-md);\n    width: var(--changerawr-widget-width);\n    overflow: hidden;\n    opacity: 1;\n    transform: translateY(0);\n    transition: opacity var(--changerawr-transition-speed) var(--changerawr-transition-easing),\n                transform var(--changerawr-transition-speed) var(--changerawr-transition-easing);\n}\n\n/* Floating widget specific overrides */\n.changerawr-widget.changerawr-floating {\n    width: auto;\n    overflow: visible;\n    background: transparent;\n    box-shadow: none;\n    border-radius: 0;\n    padding: 0;\n}\n\n/* ============================================\n   POPUP MODE\n   ============================================ */\n.changerawr-widget.popup {\n    position: fixed !important;\n    z-index: var(--changerawr-z-index) !important;\n    opacity: 0;\n    transform: translateY(var(--changerawr-popup-transform-distance));\n    pointer-events: none;\n    transition: opacity var(--changerawr-transition-speed) var(--changerawr-transition-easing),\n                transform var(--changerawr-transition-speed) var(--changerawr-transition-easing);\n}\n\n/* Position-specific transforms for popup */\n.changerawr-widget.popup.changerawr-position-top-right,\n.changerawr-widget.popup.changerawr-position-top-left {\n    transform: translateY(calc(-1 * var(--changerawr-popup-transform-distance)));\n}\n\n.changerawr-widget.popup.changerawr-position-bottom-right,\n.changerawr-widget.popup.changerawr-position-bottom-left {\n    transform: translateY(var(--changerawr-popup-transform-distance));\n}\n\n.changerawr-widget.popup.open {\n    opacity: 1 !important;\n    transform: translateY(0) !important;\n    pointer-events: all !important;\n}\n\n.changerawr-widget.hidden {\n    display: none !important;\n}\n\n/* ============================================\n   HEADER\n   ============================================ */\n.changerawr-header {\n    padding: var(--changerawr-header-padding);\n    border-bottom: 1px solid var(--changerawr-border-color);\n    font-weight: var(--changerawr-font-weight-semibold);\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n}\n\n.changerawr-header-title {\n    font-size: var(--changerawr-font-size-base);\n    font-weight: var(--changerawr-font-weight-semibold);\n    color: var(--changerawr-text-primary);\n}\n\n/* ============================================\n   CLOSE BUTTON\n   ============================================ */\n.changerawr-close {\n    background: none;\n    border: none;\n    padding: var(--changerawr-spacing-xs);\n    cursor: pointer;\n    color: var(--changerawr-text-secondary);\n    opacity: 0.6;\n    transition: opacity var(--changerawr-transition-speed);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    border-radius: var(--changerawr-border-radius-sm);\n}\n\n.changerawr-close:hover {\n    opacity: 1;\n}\n\n.changerawr-close:focus {\n    outline: 2px solid var(--changerawr-border-focus);\n    outline-offset: 2px;\n}\n\n/* ============================================\n   ENTRIES CONTAINER\n   ============================================ */\n.changerawr-entries {\n    max-height: var(--changerawr-widget-max-height);\n    overflow-y: auto;\n    padding: var(--changerawr-spacing-sm) 0;\n}\n\n/* ============================================\n   ENTRY ITEM\n   ============================================ */\n.changerawr-entry {\n    padding: var(--changerawr-entry-padding);\n    border-bottom: 1px solid var(--changerawr-border-color-light);\n    opacity: 0;\n    transform: translateY(10px);\n    animation: changerawr-slide-in var(--changerawr-transition-speed-slow) var(--changerawr-transition-easing) forwards;\n}\n\n.changerawr-entry:nth-child(2) {\n    animation-delay: calc(var(--changerawr-transition-speed) * 0.5);\n}\n\n.changerawr-entry:nth-child(3) {\n    animation-delay: var(--changerawr-transition-speed);\n}\n\n.changerawr-entry:nth-child(4) {\n    animation-delay: calc(var(--changerawr-transition-speed) * 1.5);\n}\n\n.changerawr-entry:last-child {\n    border-bottom: none;\n}\n\n.changerawr-entry:focus-within {\n    background: var(--changerawr-bg-hover);\n}\n\n@keyframes changerawr-slide-in {\n    to {\n        opacity: 1;\n        transform: translateY(0);\n    }\n}\n\n/* ============================================\n   ENTRY CONTENT\n   ============================================ */\n.changerawr-entry-title {\n    font-weight: var(--changerawr-font-weight-medium);\n    margin-bottom: var(--changerawr-spacing-xs);\n    color: var(--changerawr-text-primary);\n    font-size: var(--changerawr-font-size-base);\n}\n\n.changerawr-entry-content {\n    color: var(--changerawr-text-secondary);\n    font-size: var(--changerawr-font-size-sm);\n    display: -webkit-box;\n    -webkit-line-clamp: 3;\n    -webkit-box-orient: vertical;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    margin-bottom: var(--changerawr-spacing-sm);\n    line-height: var(--changerawr-line-height);\n}\n\n.changerawr-entry-meta {\n    display: flex;\n    align-items: center;\n    gap: var(--changerawr-spacing-sm);\n    margin-bottom: var(--changerawr-spacing-sm);\n}\n\n/* ============================================\n   TAGS\n   ============================================ */\n.changerawr-tag {\n    display: inline-block;\n    padding: var(--changerawr-tag-padding);\n    background: var(--changerawr-primary-light);\n    color: var(--changerawr-primary-color);\n    border-radius: var(--changerawr-border-radius-sm);\n    font-size: var(--changerawr-font-size-sm);\n    font-weight: var(--changerawr-font-weight-medium);\n    margin-right: var(--changerawr-spacing-xs);\n    margin-bottom: var(--changerawr-spacing-xs);\n}\n\n/* ============================================\n   READ MORE LINK\n   ============================================ */\n.changerawr-read-more {\n    color: var(--changerawr-primary-color);\n    text-decoration: none;\n    font-size: var(--changerawr-font-size-sm);\n    display: inline-block;\n    margin-top: var(--changerawr-spacing-xs);\n    padding: var(--changerawr-spacing-xs);\n    font-weight: var(--changerawr-font-weight-medium);\n    transition: color var(--changerawr-transition-speed-fast);\n}\n\n.changerawr-read-more:hover {\n    text-decoration: underline;\n    color: var(--changerawr-primary-hover);\n}\n\n.changerawr-read-more:focus {\n    outline: 2px solid var(--changerawr-border-focus);\n    outline-offset: 2px;\n    border-radius: var(--changerawr-border-radius-sm);\n}\n\n/* ============================================\n   LOADING STATE\n   ============================================ */\n.changerawr-loading {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    height: 100px;\n    padding: var(--changerawr-spacing-xl);\n}\n\n.changerawr-spinner {\n    width: 24px;\n    height: 24px;\n    border: 2px solid var(--changerawr-bg-tertiary);\n    border-top: 2px solid var(--changerawr-primary-color);\n    border-radius: 50%;\n    animation: changerawr-spin 1s linear infinite;\n}\n\n@keyframes changerawr-spin {\n    0% {\n        transform: rotate(0deg);\n    }\n    100% {\n        transform: rotate(360deg);\n    }\n}\n\n/* ============================================\n   ERROR STATE\n   ============================================ */\n.changerawr-error {\n    padding: var(--changerawr-spacing-xl);\n    text-align: center;\n    color: var(--changerawr-text-secondary);\n}\n\n.changerawr-error-icon {\n    font-size: var(--changerawr-font-size-2xl);\n    margin-bottom: var(--changerawr-spacing-sm);\n}\n\n.changerawr-error-message {\n    font-size: var(--changerawr-font-size-sm);\n    color: var(--changerawr-danger-color);\n}\n\n.changerawr-error-retry {\n    margin-top: var(--changerawr-spacing-md);\n    padding: var(--changerawr-spacing-sm) var(--changerawr-spacing-lg);\n    background: var(--changerawr-primary-color);\n    color: var(--changerawr-text-inverse);\n    border: none;\n    border-radius: var(--changerawr-border-radius);\n    cursor: pointer;\n    font-size: var(--changerawr-font-size-sm);\n    font-weight: var(--changerawr-font-weight-medium);\n    transition: background var(--changerawr-transition-speed-fast);\n}\n\n.changerawr-error-retry:hover {\n    background: var(--changerawr-primary-hover);\n}\n\n/* ============================================\n   FOOTER\n   ============================================ */\n.changerawr-footer {\n    padding: var(--changerawr-footer-padding);\n    border-top: 1px solid var(--changerawr-border-color);\n    font-size: var(--changerawr-font-size-sm);\n    color: var(--changerawr-text-secondary);\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n}\n\n.changerawr-footer a {\n    color: inherit;\n    text-decoration: none;\n    transition: color var(--changerawr-transition-speed-fast);\n}\n\n.changerawr-footer a:hover {\n    text-decoration: underline;\n    color: var(--changerawr-text-primary);\n}\n\n.changerawr-footer a:focus {\n    outline: 2px solid var(--changerawr-border-focus);\n    outline-offset: 2px;\n    border-radius: var(--changerawr-border-radius-sm);\n}\n\n/* ============================================\n   BADGE (for unread count)\n   ============================================ */\n.changerawr-badge {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    min-width: 18px;\n    height: 18px;\n    padding: 0 var(--changerawr-spacing-xs);\n    background: var(--changerawr-danger-color);\n    color: var(--changerawr-text-inverse);\n    border-radius: var(--changerawr-border-radius-full);\n    font-size: var(--changerawr-font-size-xs);\n    font-weight: var(--changerawr-font-weight-bold);\n    line-height: 1;\n}\n\n.changerawr-badge.hidden {\n    display: none;\n}\n\n/* ============================================\n   EMPTY STATE\n   ============================================ */\n.changerawr-empty {\n    padding: var(--changerawr-spacing-2xl);\n    text-align: center;\n    color: var(--changerawr-text-secondary);\n}\n\n.changerawr-empty-icon {\n    font-size: var(--changerawr-font-size-3xl);\n    margin-bottom: var(--changerawr-spacing-md);\n    opacity: 0.5;\n}\n\n.changerawr-empty-message {\n    font-size: var(--changerawr-font-size-sm);\n}\n\n/* ============================================\n   RESPONSIVE - MOBILE\n   ============================================ */\n@media (max-width: 640px) {\n    .changerawr-widget.popup {\n        width: calc(100vw - 32px) !important;\n        max-width: var(--changerawr-widget-max-width);\n    }\n}\n\n@media (max-width: 480px) {\n    .changerawr-widget {\n        width: 100%;\n        max-width: 100%;\n    }\n\n    .changerawr-widget.popup {\n        width: 100vw !important;\n        height: 100vh !important;\n        max-height: none !important;\n        border-radius: 0 !important;\n        top: 0 !important;\n        left: 0 !important;\n        right: 0 !important;\n        bottom: 0 !important;\n    }\n\n    .changerawr-entries {\n        max-height: none;\n        height: calc(100vh - 120px); /* Account for header and footer */\n    }\n}\n\n/* ============================================\n   ACCESSIBILITY - HIGH CONTRAST\n   ============================================ */\n@media (prefers-contrast: high) {\n    .changerawr-widget {\n        border: 2px solid var(--changerawr-border-color);\n    }\n\n    .changerawr-entry {\n        border-bottom-width: 2px;\n    }\n\n    .changerawr-tag {\n        border: 1px solid currentColor;\n    }\n}\n\n/* ============================================\n   ACCESSIBILITY - REDUCED MOTION\n   ============================================ */\n@media (prefers-reduced-motion: reduce) {\n    .changerawr-widget,\n    .changerawr-entry,\n    .changerawr-spinner,\n    .changerawr-close,\n    .changerawr-read-more,\n    .changerawr-footer a {\n        animation: none !important;\n        transition: none !important;\n    }\n}\n\n/* ============================================\n   DARK THEME SUPPORT (legacy class compatibility)\n   ============================================ */\n.changerawr-widget.dark,\n.changerawr-theme-dark .changerawr-widget {\n    color: var(--changerawr-text-primary);\n    background: var(--changerawr-bg-primary);\n}\n\n.dark .changerawr-header,\n.changerawr-theme-dark .changerawr-header {\n    border-color: var(--changerawr-border-color);\n}\n\n.dark .changerawr-entry,\n.changerawr-theme-dark .changerawr-entry {\n    border-color: var(--changerawr-border-color);\n}\n\n.dark .changerawr-entry:focus-within,\n.changerawr-theme-dark .changerawr-entry:focus-within {\n    background: var(--changerawr-bg-hover);\n}\n\n.dark .changerawr-tag,\n.changerawr-theme-dark .changerawr-tag {\n    background: var(--changerawr-primary-light);\n    color: var(--changerawr-primary-dark);\n}\n\n.dark .changerawr-entry-content,\n.changerawr-theme-dark .changerawr-entry-content {\n    color: var(--changerawr-text-secondary);\n}\n\n.dark .changerawr-read-more,\n.changerawr-theme-dark .changerawr-read-more {\n    color: var(--changerawr-primary-dark);\n}\n\n.dark .changerawr-footer,\n.changerawr-theme-dark .changerawr-footer {\n    border-color: var(--changerawr-border-color);\n    color: var(--changerawr-text-secondary);\n}\n"
  },
  {
    "path": "widgets/core/styles/floating.css",
    "content": "/**\n * Floating Widget Styles\n * Styles for the floating changelog widget variant\n */\n\n/* ============================================\n   FLOATING WIDGET CONTAINER\n   ============================================ */\n.changerawr-floating {\n    position: fixed;\n    z-index: var(--changerawr-z-index, 999999);\n}\n\n/* ============================================\n   POSITION CLASSES\n   ============================================ */\n.changerawr-floating.changerawr-position-bottom-right {\n    bottom: var(--changerawr-spacing-lg, 1.5rem);\n    right: var(--changerawr-spacing-lg, 1.5rem);\n}\n\n.changerawr-floating.changerawr-position-bottom-left {\n    bottom: var(--changerawr-spacing-lg, 1.5rem);\n    left: var(--changerawr-spacing-lg, 1.5rem);\n}\n\n.changerawr-floating.changerawr-position-top-right {\n    top: var(--changerawr-spacing-lg, 1.5rem);\n    right: var(--changerawr-spacing-lg, 1.5rem);\n}\n\n.changerawr-floating.changerawr-position-top-left {\n    top: var(--changerawr-spacing-lg, 1.5rem);\n    left: var(--changerawr-spacing-lg, 1.5rem);\n}\n\n/* ============================================\n   WRAPPER (for positioning context)\n   ============================================ */\n.changerawr-floating-wrapper {\n    position: relative;\n    display: inline-block;\n}\n\n/* ============================================\n   FLOATING BUTTON\n   ============================================ */\n.changerawr-floating-button {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    padding: 10px 14px;\n    background: var(--changerawr-primary-color, #0066ff);\n    color: #ffffff;\n    border: none;\n    border-radius: 6px;\n    font-family: var(--changerawr-font-family);\n    font-size: 14px;\n    font-weight: 600;\n    cursor: pointer;\n    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n    transition: all 0.2s ease;\n    position: relative;\n    gap: 10px;\n}\n\n.changerawr-floating-button:hover {\n    transform: translateY(-2px);\n    box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);\n}\n\n.changerawr-floating-button:active {\n    transform: translateY(0);\n}\n\n.changerawr-floating-button:focus {\n    outline: none;\n    box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.3);\n}\n\n/* ============================================\n   BUTTON CONTENT\n   ============================================ */\n.changerawr-floating-button-content {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    z-index: 1;\n}\n\n.changerawr-floating-icon {\n    font-size: 1.2em;\n    flex-shrink: 0;\n}\n\n.changerawr-floating-text {\n    white-space: nowrap;\n}\n\n/* ============================================\n   BADGE\n   ============================================ */\n.changerawr-floating-badge {\n    position: absolute;\n    top: -6px;\n    right: -6px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    min-width: 20px;\n    height: 20px;\n    padding: 0 5px;\n    background: var(--changerawr-danger-color, #ef4444);\n    color: #ffffff;\n    border-radius: 999px;\n    font-size: 11px;\n    font-weight: 700;\n    line-height: 1;\n    border: 2px solid #ffffff;\n    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);\n}\n\n/* ============================================\n   FLOATING PANEL\n   ============================================ */\n.changerawr-floating-panel {\n    position: absolute;\n    bottom: calc(100% + 8px);\n    right: 0;\n    width: 380px;\n    max-width: calc(100vw - 2rem);\n    max-height: 550px;\n    background: var(--changerawr-bg-primary, #ffffff);\n    border-radius: 12px;\n    box-shadow: 0 20px 50px rgba(0, 0, 0, 0.15);\n    display: none;\n    flex-direction: column;\n    overflow: hidden;\n    opacity: 0;\n    transform: translateY(10px) scale(0.95);\n    transition: opacity 0.3s ease, transform 0.3s ease;\n    z-index: 100;\n}\n\n.changerawr-floating-panel-open {\n    opacity: 1;\n    transform: translateY(0) scale(1);\n}\n\n/* Panel positioning based on widget position */\n.changerawr-position-bottom-left .changerawr-floating-panel {\n    right: auto;\n    left: 0;\n}\n\n.changerawr-position-top-right .changerawr-floating-panel {\n    bottom: auto;\n    top: calc(100% + 8px);\n    right: 0;\n}\n\n.changerawr-position-top-left .changerawr-floating-panel {\n    bottom: auto;\n    top: calc(100% + 8px);\n    right: auto;\n    left: 0;\n}\n\n/* ============================================\n   PANEL HEADER\n   ============================================ */\n.changerawr-floating-panel-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 16px;\n    border-bottom: 1px solid var(--changerawr-border-color, #eaeaea);\n    flex-shrink: 0;\n}\n\n.changerawr-floating-panel-title {\n    margin: 0;\n    font-size: 16px;\n    font-weight: 600;\n    color: var(--changerawr-text-primary, #1a1a1a);\n}\n\n.changerawr-floating-close-btn {\n    background: none;\n    border: none;\n    padding: 4px;\n    cursor: pointer;\n    font-size: 20px;\n    color: var(--changerawr-text-secondary, #666666);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    opacity: 0.7;\n    transition: opacity 0.2s ease;\n    flex-shrink: 0;\n}\n\n.changerawr-floating-close-btn:hover {\n    opacity: 1;\n}\n\n.changerawr-floating-close-btn:focus {\n    outline: none;\n    opacity: 1;\n}\n\n/* ============================================\n   PANEL CONTENT\n   ============================================ */\n.changerawr-floating-panel-content {\n    flex: 1;\n    overflow-y: auto;\n    overflow-x: hidden;\n    padding: 0;\n}\n\n/* Scrollbar styling */\n.changerawr-floating-panel-content::-webkit-scrollbar {\n    width: 6px;\n}\n\n.changerawr-floating-panel-content::-webkit-scrollbar-track {\n    background: transparent;\n}\n\n.changerawr-floating-panel-content::-webkit-scrollbar-thumb {\n    background: var(--changerawr-border-color, #eaeaea);\n    border-radius: 3px;\n}\n\n.changerawr-floating-panel-content::-webkit-scrollbar-thumb:hover {\n    background: var(--changerawr-text-secondary, #999999);\n}\n\n/* ============================================\n   ENTRY ITEM\n   ============================================ */\n.changerawr-floating-entry {\n    padding: 12px 16px;\n    border-bottom: 1px solid var(--changerawr-border-color-light, #f5f5f5);\n    cursor: pointer;\n    transition: background-color 0.2s ease;\n}\n\n.changerawr-floating-entry:last-child {\n    border-bottom: none;\n}\n\n.changerawr-floating-entry:hover {\n    background-color: var(--changerawr-bg-hover, #f5f5f5);\n}\n\n/* ============================================\n   ENTRY META (Tags)\n   ============================================ */\n.changerawr-floating-entry-meta {\n    display: flex;\n    gap: 6px;\n    margin-bottom: 8px;\n    flex-wrap: wrap;\n}\n\n.changerawr-floating-tag {\n    display: inline-block;\n    padding: 3px 8px;\n    background: var(--changerawr-primary-light, #e8f2ff);\n    color: var(--changerawr-primary-color, #0066ff);\n    border-radius: 4px;\n    font-size: 11px;\n    font-weight: 500;\n    white-space: nowrap;\n}\n\n/* ============================================\n   ENTRY TITLE\n   ============================================ */\n.changerawr-floating-entry-title {\n    margin: 0 0 6px 0;\n    font-size: 14px;\n    font-weight: 600;\n    color: var(--changerawr-text-primary, #1a1a1a);\n    line-height: 1.4;\n}\n\n/* ============================================\n   ENTRY CONTENT\n   ============================================ */\n.changerawr-floating-entry-content {\n    margin: 0 0 8px 0;\n    font-size: 13px;\n    color: var(--changerawr-text-secondary, #666666);\n    line-height: 1.5;\n    display: -webkit-box;\n    -webkit-line-clamp: 3;\n    -webkit-box-orient: vertical;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n\n/* ============================================\n   READ MORE LINK\n   ============================================ */\n.changerawr-floating-read-more {\n    display: inline-block;\n    margin-top: 6px;\n    font-size: 12px;\n    font-weight: 500;\n    color: var(--changerawr-primary-color, #0066ff);\n    text-decoration: none;\n    cursor: pointer;\n    transition: color 0.2s ease;\n    padding: 4px 0;\n}\n\n.changerawr-floating-read-more:hover {\n    text-decoration: underline;\n    color: var(--changerawr-primary-hover, #0052cc);\n}\n\n.changerawr-floating-read-more:focus {\n    outline: none;\n    text-decoration: underline;\n}\n\n/* ============================================\n   EMPTY STATE\n   ============================================ */\n.changerawr-floating-empty {\n    padding: 48px 24px;\n    text-align: center;\n    color: var(--changerawr-text-secondary, #666666);\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n}\n\n.changerawr-floating-empty-icon {\n    font-size: 40px;\n    margin-bottom: 16px;\n    opacity: 0.5;\n}\n\n.changerawr-floating-empty-message {\n    font-size: 14px;\n}\n\n/* ============================================\n   PANEL FOOTER\n   ============================================ */\n.changerawr-floating-panel-footer {\n    padding: 12px 16px;\n    border-top: 1px solid var(--changerawr-border-color, #eaeaea);\n    font-size: 12px;\n    color: var(--changerawr-text-secondary, #666666);\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    gap: 12px;\n    flex-shrink: 0;\n    flex-wrap: wrap;\n}\n\n.changerawr-floating-panel-footer a {\n    color: inherit;\n    text-decoration: none;\n    transition: color 0.2s ease;\n}\n\n.changerawr-floating-panel-footer a:hover {\n    text-decoration: underline;\n    color: var(--changerawr-text-primary, #1a1a1a);\n}\n\n/* ============================================\n   DARK THEME\n   ============================================ */\n.changerawr-floating.changerawr-theme-dark .changerawr-floating-button {\n    background: var(--changerawr-primary-color, #60a5fa);\n    color: #ffffff;\n}\n\n.changerawr-floating.changerawr-theme-dark .changerawr-floating-button:hover {\n    background: var(--changerawr-primary-hover, #93c5fd);\n}\n\n.changerawr-floating.changerawr-theme-dark .changerawr-floating-panel {\n    background: var(--changerawr-bg-primary, #1f2937);\n    color: var(--changerawr-text-primary, #f3f4f6);\n}\n\n.changerawr-floating.changerawr-theme-dark .changerawr-floating-panel-header {\n    border-color: var(--changerawr-border-color, #4b5563);\n    background: var(--changerawr-bg-secondary, #111827);\n}\n\n.changerawr-floating.changerawr-theme-dark .changerawr-floating-panel-title {\n    color: var(--changerawr-text-primary, #f3f4f6);\n}\n\n.changerawr-floating.changerawr-theme-dark .changerawr-floating-close-btn {\n    color: var(--changerawr-text-secondary, #d1d5db);\n}\n\n.changerawr-floating.changerawr-theme-dark .changerawr-floating-panel-content {\n    background: var(--changerawr-bg-primary, #1f2937);\n}\n\n.changerawr-floating.changerawr-theme-dark .changerawr-floating-entry {\n    border-color: var(--changerawr-border-color-light, #374151);\n    color: var(--changerawr-text-primary, #f3f4f6);\n}\n\n.changerawr-floating.changerawr-theme-dark .changerawr-floating-entry:hover {\n    background-color: var(--changerawr-bg-hover, #374151);\n}\n\n.changerawr-floating.changerawr-theme-dark .changerawr-floating-entry-title {\n    color: var(--changerawr-text-primary, #f3f4f6);\n}\n\n.changerawr-floating.changerawr-theme-dark .changerawr-floating-entry-content {\n    color: var(--changerawr-text-secondary, #d1d5db);\n}\n\n.changerawr-floating.changerawr-theme-dark .changerawr-floating-tag {\n    background: var(--changerawr-primary-light, #1e3a8a);\n    color: var(--changerawr-primary-color, #60a5fa);\n}\n\n.changerawr-floating.changerawr-theme-dark .changerawr-floating-read-more {\n    color: var(--changerawr-primary-color, #60a5fa);\n}\n\n.changerawr-floating.changerawr-theme-dark .changerawr-floating-read-more:hover {\n    color: var(--changerawr-primary-hover, #93c5fd);\n}\n\n.changerawr-floating.changerawr-theme-dark .changerawr-floating-panel-footer {\n    border-color: var(--changerawr-border-color, #4b5563);\n    background: var(--changerawr-bg-secondary, #111827);\n    color: var(--changerawr-text-secondary, #d1d5db);\n}\n\n.changerawr-floating.changerawr-theme-dark .changerawr-floating-panel-footer a {\n    color: var(--changerawr-text-secondary, #d1d5db);\n}\n\n.changerawr-floating.changerawr-theme-dark .changerawr-floating-panel-footer a:hover {\n    color: var(--changerawr-text-primary, #f3f4f6);\n}\n\n.changerawr-floating.changerawr-theme-dark .changerawr-floating-empty {\n    color: var(--changerawr-text-secondary, #d1d5db);\n}\n\n/* ============================================\n   MOBILE RESPONSIVENESS\n   ============================================ */\n@media (max-width: 640px) {\n    .changerawr-floating-panel {\n        width: calc(100vw - 2rem);\n        max-width: none;\n        max-height: 500px;\n    }\n\n    .changerawr-floating-text {\n        display: none;\n    }\n\n    .changerawr-floating-button {\n        padding: 12px;\n        width: 56px;\n        height: 56px;\n    }\n\n    .changerawr-floating-button-content {\n        gap: 0;\n    }\n\n    .changerawr-floating-entry {\n        padding: 10px 12px;\n    }\n\n    .changerawr-floating-panel-header {\n        padding: 12px;\n    }\n\n    .changerawr-floating-panel-footer {\n        padding: 10px 12px;\n        font-size: 11px;\n    }\n}\n\n@media (max-width: 480px) {\n    .changerawr-floating {\n        bottom: 0 !important;\n        right: 0 !important;\n        left: 0 !important;\n        top: auto !important;\n    }\n\n    .changerawr-floating-panel {\n        position: fixed;\n        bottom: 0;\n        right: 0;\n        left: 0;\n        top: auto;\n        width: 100vw;\n        max-width: 100vw;\n        max-height: 80vh;\n        border-radius: 12px 12px 0 0;\n        bottom: 0;\n    }\n\n    .changerawr-floating-button {\n        width: 56px;\n        height: 56px;\n        margin-bottom: 0;\n    }\n}\n\n/* ============================================\n   ACCESSIBILITY - REDUCED MOTION\n   ============================================ */\n@media (prefers-reduced-motion: reduce) {\n    .changerawr-floating-button,\n    .changerawr-floating-panel,\n    .changerawr-floating-entry,\n    .changerawr-floating-close-btn,\n    .changerawr-floating-read-more {\n        animation: none !important;\n        transition: none !important;\n    }\n}\n\n/* ============================================\n   ACCESSIBILITY - HIGH CONTRAST\n   ============================================ */\n@media (prefers-contrast: high) {\n    .changerawr-floating-button {\n        border: 2px solid var(--changerawr-border-color, #eaeaea);\n    }\n\n    .changerawr-floating-entry {\n        border-bottom-width: 2px;\n    }\n\n    .changerawr-floating-tag {\n        border: 1px solid currentColor;\n    }\n}\n"
  },
  {
    "path": "widgets/core/styles/modal.css",
    "content": "/**\n * Modal Widget Styles\n */\n\n.changerawr-modal {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    z-index: var(--changerawr-z-index, 9999);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    padding: var(--changerawr-spacing-lg, 1.5rem);\n}\n\n/* Overlay */\n.changerawr-modal-overlay {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background: rgba(0, 0, 0, 0.5);\n    backdrop-filter: blur(4px);\n    opacity: 0;\n    transition: opacity var(--changerawr-animation-duration, 0.3s) var(--changerawr-animation-easing, ease);\n}\n\n.changerawr-modal.open .changerawr-modal-overlay {\n    opacity: 1;\n}\n\n/* Modal content */\n.changerawr-modal-content {\n    position: relative;\n    width: 100%;\n    max-width: 800px;\n    max-height: 90vh;\n    background: var(--changerawr-bg-primary, #ffffff);\n    border-radius: var(--changerawr-border-radius-lg, 12px);\n    box-shadow: var(--changerawr-shadow-xl, 0 20px 60px rgba(0, 0, 0, 0.2));\n    display: flex;\n    flex-direction: column;\n    overflow: hidden;\n    opacity: 0;\n    transform: scale(0.9) translateY(20px);\n    transition: all var(--changerawr-animation-duration, 0.3s) var(--changerawr-animation-easing, ease);\n}\n\n.changerawr-modal.open .changerawr-modal-content {\n    opacity: 1;\n    transform: scale(1) translateY(0);\n}\n\n/* Header */\n.changerawr-modal .changerawr-header {\n    padding: var(--changerawr-spacing-lg, 1.5rem);\n    border-bottom: 1px solid var(--changerawr-border-color, #e5e7eb);\n}\n\n.changerawr-modal .changerawr-header-title {\n    font-size: var(--changerawr-font-size-xl, 20px);\n    font-weight: 700;\n}\n\n/* Entries container */\n.changerawr-modal .changerawr-entries {\n    flex: 1;\n    overflow-y: auto;\n    padding: var(--changerawr-spacing-lg, 1.5rem);\n}\n\n.changerawr-modal .changerawr-entry {\n    padding: var(--changerawr-spacing-lg, 1.5rem);\n    margin-bottom: var(--changerawr-spacing-md, 0.75rem);\n    background: var(--changerawr-bg-secondary, #f9fafb);\n    border-radius: var(--changerawr-border-radius-md, 8px);\n}\n\n.changerawr-modal .changerawr-entry-date {\n    font-size: var(--changerawr-font-size-sm, 12px);\n    color: var(--changerawr-text-secondary, #6b7280);\n    margin-bottom: var(--changerawr-spacing-sm, 0.5rem);\n}\n\n/* Footer */\n.changerawr-modal .changerawr-footer {\n    padding: var(--changerawr-spacing-md, 0.75rem) var(--changerawr-spacing-lg, 1.5rem);\n    border-top: 1px solid var(--changerawr-border-color, #e5e7eb);\n}\n\n/* Dark mode */\n.changerawr-modal.dark .changerawr-modal-content {\n    background: var(--changerawr-bg-primary-dark, #1f2937);\n}\n\n.changerawr-modal.dark .changerawr-header,\n.changerawr-modal.dark .changerawr-footer {\n    border-color: var(--changerawr-border-color-dark, #374151);\n}\n\n.changerawr-modal.dark .changerawr-entry {\n    background: var(--changerawr-bg-secondary-dark, #111827);\n}\n\n/* Mobile responsiveness */\n@media (max-width: 640px) {\n    .changerawr-modal {\n        padding: var(--changerawr-spacing-sm, 0.5rem);\n    }\n\n    .changerawr-modal-content {\n        max-height: 95vh;\n        border-radius: var(--changerawr-border-radius-md, 8px);\n    }\n\n    .changerawr-modal .changerawr-header,\n    .changerawr-modal .changerawr-entries,\n    .changerawr-modal .changerawr-footer {\n        padding: var(--changerawr-spacing-md, 0.75rem);\n    }\n\n    .changerawr-modal .changerawr-entry {\n        padding: var(--changerawr-spacing-md, 0.75rem);\n    }\n}\n\n/* Reduce motion */\n@media (prefers-reduced-motion: reduce) {\n    .changerawr-modal-overlay,\n    .changerawr-modal-content {\n        transition: none;\n    }\n}\n"
  },
  {
    "path": "widgets/core/styles/reset.css",
    "content": "/**\n * Changerawr Widget CSS Reset\n *\n * Scoped reset to prevent conflicts with customer site styles.\n * All rules are scoped to .changerawr-* classes.\n */\n\n/* ============================================\n   BOX SIZING\n   ============================================ */\n.changerawr-widget,\n.changerawr-widget *,\n.changerawr-widget *::before,\n.changerawr-widget *::after {\n    box-sizing: border-box;\n}\n\n/* ============================================\n   RESET MARGINS & PADDING\n   ============================================ */\n.changerawr-widget h1,\n.changerawr-widget h2,\n.changerawr-widget h3,\n.changerawr-widget h4,\n.changerawr-widget h5,\n.changerawr-widget h6,\n.changerawr-widget p,\n.changerawr-widget ul,\n.changerawr-widget ol,\n.changerawr-widget li,\n.changerawr-widget blockquote,\n.changerawr-widget figure,\n.changerawr-widget pre {\n    margin: 0;\n    padding: 0;\n}\n\n/* ============================================\n   LIST STYLES\n   ============================================ */\n.changerawr-widget ul,\n.changerawr-widget ol {\n    list-style: none;\n}\n\n/* ============================================\n   LINKS\n   ============================================ */\n.changerawr-widget a {\n    text-decoration: inherit;\n    color: inherit;\n}\n\n.changerawr-widget a:focus {\n    outline: 2px solid var(--changerawr-border-focus);\n    outline-offset: 2px;\n}\n\n/* ============================================\n   BUTTONS\n   ============================================ */\n.changerawr-widget button {\n    font-family: inherit;\n    font-size: inherit;\n    line-height: inherit;\n    color: inherit;\n    background: none;\n    border: none;\n    padding: 0;\n    margin: 0;\n    cursor: pointer;\n}\n\n.changerawr-widget button:focus {\n    outline: 2px solid var(--changerawr-border-focus);\n    outline-offset: 2px;\n}\n\n.changerawr-widget button:disabled {\n    cursor: not-allowed;\n    opacity: 0.5;\n}\n\n/* ============================================\n   IMAGES\n   ============================================ */\n.changerawr-widget img {\n    max-width: 100%;\n    height: auto;\n    display: block;\n}\n\n/* ============================================\n   INPUTS\n   ============================================ */\n.changerawr-widget input,\n.changerawr-widget textarea,\n.changerawr-widget select {\n    font-family: inherit;\n    font-size: inherit;\n    line-height: inherit;\n}\n\n/* ============================================\n   SCROLLBAR STYLING (WEBKIT)\n   ============================================ */\n.changerawr-widget ::-webkit-scrollbar {\n    width: 8px;\n}\n\n.changerawr-widget ::-webkit-scrollbar-track {\n    background: var(--changerawr-bg-secondary);\n}\n\n.changerawr-widget ::-webkit-scrollbar-thumb {\n    background: var(--changerawr-border-color);\n    border-radius: var(--changerawr-border-radius);\n}\n\n.changerawr-widget ::-webkit-scrollbar-thumb:hover {\n    background: var(--changerawr-text-tertiary);\n}\n\n/* ============================================\n   FIREFOX SCROLLBAR\n   ============================================ */\n.changerawr-widget * {\n    scrollbar-width: thin;\n    scrollbar-color: var(--changerawr-border-color) var(--changerawr-bg-secondary);\n}\n\n/* ============================================\n   PREVENT CUSTOMER SITE STYLE BLEED\n   ============================================ */\n.changerawr-widget {\n    all: initial;\n    /* Restore necessary properties */\n    display: block;\n    position: relative;\n}\n\n/* ============================================\n   ACCESSIBILITY - FOCUS VISIBLE\n   ============================================ */\n.changerawr-widget :focus:not(:focus-visible) {\n    outline: none;\n}\n\n.changerawr-widget :focus-visible {\n    outline: 2px solid var(--changerawr-border-focus);\n    outline-offset: 2px;\n}\n\n/* ============================================\n   PRINT STYLES\n   ============================================ */\n@media print {\n    .changerawr-widget {\n        display: none !important;\n    }\n}\n"
  },
  {
    "path": "widgets/core/styles/variables.css",
    "content": "/**\n * Changerawr Widget CSS Custom Properties\n *\n * These variables can be overridden via:\n * 1. data-* attributes on the script tag\n * 2. External stylesheet via data-custom-css\n * 3. Database-stored theme CSS\n */\n\n:root {\n    /* ============================================\n       COLORS - Primary & Brand\n       ============================================ */\n    --changerawr-primary-color: #0066ff;\n    --changerawr-primary-hover: #0052cc;\n    --changerawr-primary-active: #003d99;\n    --changerawr-primary-light: #e8f2ff;\n    --changerawr-primary-dark: #1a365d;\n\n    /* ============================================\n       COLORS - Background\n       ============================================ */\n    --changerawr-bg-primary: #ffffff;\n    --changerawr-bg-secondary: #f8f9fa;\n    --changerawr-bg-tertiary: #f5f5f5;\n    --changerawr-bg-hover: #f5f5f5;\n    --changerawr-bg-overlay: rgba(0, 0, 0, 0.5);\n\n    /* ============================================\n       COLORS - Text\n       ============================================ */\n    --changerawr-text-primary: #1a1a1a;\n    --changerawr-text-secondary: #666666;\n    --changerawr-text-tertiary: #999999;\n    --changerawr-text-inverse: #ffffff;\n\n    /* ============================================\n       COLORS - Borders\n       ============================================ */\n    --changerawr-border-color: #eaeaea;\n    --changerawr-border-color-light: #f5f5f5;\n    --changerawr-border-color-dark: #333333;\n    --changerawr-border-focus: var(--changerawr-primary-color);\n\n    /* ============================================\n       COLORS - Semantic\n       ============================================ */\n    --changerawr-success-color: #10b981;\n    --changerawr-warning-color: #f59e0b;\n    --changerawr-danger-color: #ef4444;\n    --changerawr-info-color: #3b82f6;\n\n    /* ============================================\n       SHADOWS\n       ============================================ */\n    --changerawr-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);\n    --changerawr-shadow-md: 0 2px 8px rgba(0, 0, 0, 0.1);\n    --changerawr-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);\n    --changerawr-shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15);\n\n    /* ============================================\n       TYPOGRAPHY - Font Families\n       ============================================ */\n    --changerawr-font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n    --changerawr-font-family-mono: 'SF Mono', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;\n\n    /* ============================================\n       TYPOGRAPHY - Font Sizes\n       ============================================ */\n    --changerawr-font-size-xs: 11px;\n    --changerawr-font-size-sm: 12px;\n    --changerawr-font-size-base: 14px;\n    --changerawr-font-size-md: 14px;\n    --changerawr-font-size-lg: 16px;\n    --changerawr-font-size-xl: 18px;\n    --changerawr-font-size-2xl: 24px;\n    --changerawr-font-size-3xl: 30px;\n\n    /* ============================================\n       TYPOGRAPHY - Font Weights\n       ============================================ */\n    --changerawr-font-weight-normal: 400;\n    --changerawr-font-weight-medium: 500;\n    --changerawr-font-weight-semibold: 600;\n    --changerawr-font-weight-bold: 700;\n\n    /* ============================================\n       TYPOGRAPHY - Line Heights\n       ============================================ */\n    --changerawr-line-height: 1.5;\n    --changerawr-line-height-tight: 1.25;\n    --changerawr-line-height-relaxed: 1.75;\n\n    /* ============================================\n       SPACING SCALE\n       ============================================ */\n    --changerawr-spacing-xs: 4px;\n    --changerawr-spacing-sm: 8px;\n    --changerawr-spacing-md: 12px;\n    --changerawr-spacing-lg: 16px;\n    --changerawr-spacing-xl: 24px;\n    --changerawr-spacing-2xl: 32px;\n    --changerawr-spacing-3xl: 48px;\n\n    /* ============================================\n       LAYOUT - Dimensions\n       ============================================ */\n    --changerawr-widget-width: 300px;\n    --changerawr-widget-max-width: 400px;\n    --changerawr-widget-min-width: 280px;\n    --changerawr-widget-max-height: 400px;\n    --changerawr-widget-min-height: 200px;\n\n    /* ============================================\n       LAYOUT - Border Radius\n       ============================================ */\n    --changerawr-border-radius-sm: 4px;\n    --changerawr-border-radius: 8px;\n    --changerawr-border-radius-md: 8px;\n    --changerawr-border-radius-lg: 12px;\n    --changerawr-border-radius-xl: 16px;\n    --changerawr-border-radius-full: 9999px;\n\n    /* ============================================\n       LAYOUT - Z-Index\n       ============================================ */\n    --changerawr-z-index: 999999;\n    --changerawr-z-index-backdrop: 999998;\n    --changerawr-z-index-modal: 1000000;\n\n    /* ============================================\n       ANIMATIONS - Timing\n       ============================================ */\n    --changerawr-transition-speed-fast: 150ms;\n    --changerawr-transition-speed: 200ms;\n    --changerawr-transition-speed-slow: 300ms;\n    --changerawr-transition-speed-slower: 400ms;\n\n    /* ============================================\n       ANIMATIONS - Easing Functions\n       ============================================ */\n    --changerawr-transition-easing: cubic-bezier(0.4, 0, 0.2, 1);\n    --changerawr-transition-easing-in: cubic-bezier(0.4, 0, 1, 1);\n    --changerawr-transition-easing-out: cubic-bezier(0, 0, 0.2, 1);\n    --changerawr-transition-easing-in-out: cubic-bezier(0.4, 0, 0.2, 1);\n    --changerawr-transition-easing-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);\n\n    /* ============================================\n       COMPONENT SPECIFIC\n       ============================================ */\n    --changerawr-header-padding: 12px 16px;\n    --changerawr-entry-padding: 8px 16px;\n    --changerawr-footer-padding: 8px 16px;\n    --changerawr-tag-padding: 2px 8px;\n\n    /* ============================================\n       POSITIONING\n       ============================================ */\n    --changerawr-popup-offset: 20px;\n    --changerawr-popup-transform-distance: 20px;\n}\n\n/* ============================================\n   DARK THEME\n   ============================================ */\n.changerawr-theme-dark {\n    /* Colors - Background */\n    --changerawr-bg-primary: #1f2937;\n    --changerawr-bg-secondary: #111827;\n    --changerawr-bg-tertiary: #374151;\n    --changerawr-bg-hover: #374151;\n\n    /* Colors - Text */\n    --changerawr-text-primary: #f3f4f6;\n    --changerawr-text-secondary: #d1d5db;\n    --changerawr-text-tertiary: #9ca3af;\n\n    /* Colors - Primary (lighter for dark backgrounds) */\n    --changerawr-primary-color: #60a5fa;\n    --changerawr-primary-hover: #93c5fd;\n    --changerawr-primary-active: #3b82f6;\n    --changerawr-primary-light: #1e3a8a;\n    --changerawr-primary-dark: #60a5fa;\n\n    /* Colors - Semantic */\n    --changerawr-success-color: #34d399;\n    --changerawr-warning-color: #fbbf24;\n    --changerawr-danger-color: #f87171;\n    --changerawr-info-color: #60a5fa;\n\n    /* Colors - Borders */\n    --changerawr-border-color: #4b5563;\n    --changerawr-border-color-light: #374151;\n    --changerawr-border-color-dark: #1f2937;\n\n    /* Shadows (stronger for dark theme) */\n    --changerawr-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5);\n    --changerawr-shadow-md: 0 2px 8px rgba(0, 0, 0, 0.6);\n    --changerawr-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.7);\n    --changerawr-shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.8);\n\n    /* Spinner colors */\n    --changerawr-spinner-track: #4b5563;\n    --changerawr-spinner-color: var(--changerawr-primary-color);\n}\n\n/* ============================================\n   LIGHT THEME (explicit class for override)\n   ============================================ */\n.changerawr-theme-light {\n    /* Use root defaults - included for explicitness */\n    --changerawr-bg-primary: #ffffff;\n    --changerawr-text-primary: #1a1a1a;\n    --changerawr-primary-color: #0066ff;\n}\n\n/* ============================================\n   ACCESSIBILITY\n   ============================================ */\n\n/* High contrast mode support */\n@media (prefers-contrast: high) {\n    :root {\n        --changerawr-border-color: #000000;\n        --changerawr-text-secondary: #000000;\n    }\n\n    .changerawr-theme-dark {\n        --changerawr-border-color: #ffffff;\n        --changerawr-text-secondary: #ffffff;\n    }\n}\n\n/* Reduced motion support */\n@media (prefers-reduced-motion: reduce) {\n    :root {\n        --changerawr-transition-speed-fast: 0ms;\n        --changerawr-transition-speed: 0ms;\n        --changerawr-transition-speed-slow: 0ms;\n        --changerawr-transition-speed-slower: 0ms;\n    }\n}\n\n/* ============================================\n   UTILITY CLASSES FOR POSITIONING\n   ============================================ */\n\n.changerawr-position-top-left {\n    top: var(--changerawr-popup-offset);\n    left: var(--changerawr-popup-offset);\n}\n\n.changerawr-position-top-right {\n    top: var(--changerawr-popup-offset);\n    right: var(--changerawr-popup-offset);\n}\n\n.changerawr-position-bottom-left {\n    bottom: var(--changerawr-popup-offset);\n    left: var(--changerawr-popup-offset);\n}\n\n.changerawr-position-bottom-right {\n    bottom: var(--changerawr-popup-offset);\n    right: var(--changerawr-popup-offset);\n}\n"
  },
  {
    "path": "widgets/variants/announcement.js",
    "content": "/**\n * Announcement Bar Changelog Widget\n * Top or bottom bar displaying latest changelog entry\n */\n\nclass ChangelogAnnouncementWidget {\n    constructor(container, options) {\n        this.container = container;\n        this.options = {\n            theme: 'light',\n            position: 'top',\n            dismissible: true,\n            autoHide: false,\n            hideDelay: 10000,\n            showIcon: true,\n            customCSS: null,\n            ...options\n        };\n\n        this.isVisible = true;\n        this.isDismissed = false;\n        this.baseUrl = options.baseUrl || process.env.NEXT_PUBLIC_APP_URL || '';\n        this.storageKey = `changerawr-announcement-dismissed-${this.options.projectId}`;\n        this.init();\n    }\n\n    async loadStyles() {\n        // Load core CSS files\n        const cssFiles = [\n            '/widgets/core/styles/variables.css',\n            '/widgets/core/styles/reset.css',\n            '/widgets/core/styles/common.css',\n            '/widgets/core/styles/announcement.css'\n        ];\n\n        for (const file of cssFiles) {\n            if (!document.querySelector(`link[href=\"${this.baseUrl}${file}\"]`)) {\n                const link = document.createElement('link');\n                link.rel = 'stylesheet';\n                link.href = this.baseUrl + file;\n                document.head.appendChild(link);\n            }\n        }\n\n        // Inject custom CSS if provided\n        if (this.options.customCSS) {\n            const styleId = `changerawr-custom-css-${this.options.projectId || 'default'}`;\n            let customStyle = document.getElementById(styleId);\n\n            if (!customStyle) {\n                customStyle = document.createElement('style');\n                customStyle.id = styleId;\n                document.head.appendChild(customStyle);\n            }\n\n            customStyle.textContent = this.options.customCSS;\n        }\n    }\n\n    checkDismissed() {\n        if (!this.options.dismissible) return false;\n\n        try {\n            const dismissed = localStorage.getItem(this.storageKey);\n            return dismissed === 'true';\n        } catch (e) {\n            return false;\n        }\n    }\n\n    markDismissed() {\n        try {\n            localStorage.setItem(this.storageKey, 'true');\n        } catch (e) {\n            console.warn('Changerawr: Could not save dismissal state');\n        }\n    }\n\n    async init() {\n        await this.loadStyles();\n\n        // Check if already dismissed\n        if (this.checkDismissed()) {\n            this.container.style.display = 'none';\n            return;\n        }\n\n        // Apply base classes\n        this.container.classList.add('changerawr-widget', 'changerawr-announcement');\n\n        // Apply theme\n        if (this.options.theme === 'dark') {\n            this.container.classList.add('dark');\n        }\n\n        // Apply position\n        if (this.options.position === 'bottom') {\n            this.container.classList.add('changerawr-announcement-bottom');\n        } else {\n            this.container.classList.add('changerawr-announcement-top');\n        }\n\n        // ARIA attributes\n        this.container.setAttribute('role', 'banner');\n        this.container.setAttribute('aria-label', 'Changelog announcement');\n\n        await this.loadLatestEntry();\n\n        if (this.options.autoHide && this.options.hideDelay > 0) {\n            setTimeout(() => this.hide(), this.options.hideDelay);\n        }\n    }\n\n    async loadLatestEntry() {\n        try {\n            const response = await fetch(\n                `${this.baseUrl}/api/changelog/${this.options.projectId}/entries`\n            );\n\n            if (!response.ok) {\n                throw new Error(`HTTP ${response.status}`);\n            }\n\n            const data = await response.json();\n            const entries = data.items || [];\n            this.project = data.project;\n\n            if (entries.length > 0) {\n                this.latestEntry = entries[0];\n                this.render();\n            } else {\n                this.container.style.display = 'none';\n            }\n        } catch (error) {\n            console.error('Failed to load changelog entry:', error);\n            this.container.style.display = 'none';\n        }\n    }\n\n    render() {\n        if (!this.latestEntry) return;\n\n        const bar = document.createElement('div');\n        bar.className = 'changerawr-announcement-bar';\n\n        // Icon\n        if (this.options.showIcon) {\n            const icon = document.createElement('span');\n            icon.className = 'changerawr-announcement-icon';\n            icon.innerHTML = '🎉';\n            bar.appendChild(icon);\n        }\n\n        // Content container\n        const content = document.createElement('div');\n        content.className = 'changerawr-announcement-content';\n\n        // Label (optional)\n        const label = document.createElement('span');\n        label.className = 'changerawr-announcement-label';\n        label.textContent = 'New:';\n        content.appendChild(label);\n\n        // Title\n        const title = document.createElement('span');\n        title.className = 'changerawr-announcement-title';\n        title.textContent = this.latestEntry.title;\n        content.appendChild(title);\n\n        // Tags (if any)\n        if (this.latestEntry.tags && this.latestEntry.tags.length > 0) {\n            const tag = this.latestEntry.tags[0]; // Show only first tag\n            const tagEl = document.createElement('span');\n            tagEl.className = 'changerawr-announcement-tag';\n            tagEl.textContent = tag.name;\n            if (tag.color) {\n                tagEl.style.backgroundColor = tag.color + '20';\n                tagEl.style.color = tag.color;\n            }\n            content.appendChild(tagEl);\n        }\n\n        bar.appendChild(content);\n\n        // Actions\n        const actions = document.createElement('div');\n        actions.className = 'changerawr-announcement-actions';\n\n        // Read more link\n        const readMore = document.createElement('a');\n        readMore.className = 'changerawr-announcement-link';\n        readMore.href = `${this.baseUrl}/changelog/${this.options.projectId}/${this.latestEntry.id}`;\n        readMore.textContent = 'Read more';\n        readMore.target = '_blank';\n        readMore.rel = 'noopener';\n        actions.appendChild(readMore);\n\n        // Dismiss button\n        if (this.options.dismissible) {\n            const dismissBtn = document.createElement('button');\n            dismissBtn.className = 'changerawr-announcement-dismiss';\n            dismissBtn.innerHTML = '✕';\n            dismissBtn.setAttribute('aria-label', 'Dismiss announcement');\n            dismissBtn.addEventListener('click', () => this.dismiss());\n            actions.appendChild(dismissBtn);\n        }\n\n        bar.appendChild(actions);\n\n        this.container.innerHTML = '';\n        this.container.appendChild(bar);\n    }\n\n    dismiss() {\n        if (this.isDismissed) return;\n\n        this.isDismissed = true;\n        this.markDismissed();\n        this.hide();\n    }\n\n    hide() {\n        if (!this.isVisible) return;\n\n        this.isVisible = false;\n        this.container.classList.add('changerawr-announcement-hidden');\n\n        setTimeout(() => {\n            this.container.style.display = 'none';\n        }, 300);\n    }\n\n    show() {\n        if (this.isVisible) return;\n\n        this.isVisible = true;\n        this.container.style.display = 'block';\n        this.container.classList.remove('changerawr-announcement-hidden');\n    }\n}\n\n// Export globally for browser\nwindow.ChangerawrWidget = {\n    init: (options) => {\n        const container = options.container || document.getElementById('changerawr-widget');\n        return new ChangelogAnnouncementWidget(container, options);\n    }\n};\n"
  },
  {
    "path": "widgets/variants/classic.js",
    "content": "/**\n * Classic Changelog Widget\n * Refactored to use external CSS files\n */\n\nclass ChangelogWidget {\n    constructor(container, options) {\n        this.container = container;\n        this.options = {\n            theme: 'light',\n            maxHeight: '400px',\n            position: 'bottom-right',\n            isPopup: false,\n            maxEntries: 3,\n            hidden: false,\n            customCSS: null,\n            ...options\n        };\n\n        this.isOpen = false;\n        this.isLoading = false;\n        this.baseUrl = options.baseUrl || process.env.NEXT_PUBLIC_APP_URL || '';\n        this.init();\n    }\n\n    async loadStyles() {\n        // Load core CSS files\n        const cssFiles = [\n            '/widgets/core/styles/variables.css',\n            '/widgets/core/styles/reset.css',\n            '/widgets/core/styles/common.css'\n        ];\n\n        for (const file of cssFiles) {\n            if (!document.querySelector(`link[href=\"${this.baseUrl}${file}\"]`)) {\n                const link = document.createElement('link');\n                link.rel = 'stylesheet';\n                link.href = this.baseUrl + file;\n                document.head.appendChild(link);\n            }\n        }\n\n        // Inject custom CSS if provided\n        if (this.options.customCSS) {\n            const styleId = `changerawr-custom-css-${this.options.projectId || 'default'}`;\n            let customStyle = document.getElementById(styleId);\n\n            if (!customStyle) {\n                customStyle = document.createElement('style');\n                customStyle.id = styleId;\n                document.head.appendChild(customStyle);\n            }\n\n            customStyle.textContent = this.options.customCSS;\n        }\n    }\n\n    updatePosition() {\n        if (!this.options.isPopup) return;\n\n        // Use CSS classes instead of inline styles\n        this.container.classList.remove(\n            'changerawr-position-top-right',\n            'changerawr-position-top-left',\n            'changerawr-position-bottom-right',\n            'changerawr-position-bottom-left'\n        );\n        this.container.classList.add(`changerawr-position-${this.options.position}`);\n    }\n\n    async init() {\n        await this.loadStyles();\n\n        // Apply base classes\n        this.container.classList.add('changerawr-widget');\n\n        // Apply theme\n        if (this.options.theme === 'dark') {\n            this.container.classList.add('dark');\n        }\n\n        // Apply popup class and position\n        if (this.options.isPopup) {\n            this.container.classList.add('popup');\n            this.updatePosition();\n        }\n\n        if (this.options.hidden) {\n            this.container.classList.add('hidden');\n        }\n\n        // Set max height\n        this.container.style.setProperty('--changerawr-widget-max-height', this.options.maxHeight);\n\n        // ARIA attributes\n        this.container.setAttribute('role', 'dialog');\n        this.container.setAttribute('aria-label', 'Changelog updates');\n\n        this.render();\n        await this.loadEntries();\n        this.setupKeyboardNavigation();\n\n        if (this.options.trigger) {\n            this.setupTriggerButton();\n        }\n    }\n\n    setupKeyboardNavigation() {\n        this.container.addEventListener('keydown', (e) => {\n            if (e.key === 'Escape' && this.isOpen) {\n                this.close();\n            }\n\n            if (e.key === 'Tab') {\n                const focusableElements = this.container.querySelectorAll(\n                    'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n                );\n                const firstElement = focusableElements[0];\n                const lastElement = focusableElements[focusableElements.length - 1];\n\n                if (e.shiftKey) {\n                    if (document.activeElement === firstElement) {\n                        e.preventDefault();\n                        lastElement.focus();\n                    }\n                } else {\n                    if (document.activeElement === lastElement) {\n                        e.preventDefault();\n                        firstElement.focus();\n                    }\n                }\n            }\n        });\n    }\n\n    setupTriggerButton() {\n        const trigger = document.getElementById(this.options.trigger);\n        if (!trigger) {\n            console.error(`Changerawr: Trigger button '${this.options.trigger}' not found`);\n            return;\n        }\n\n        trigger.addEventListener('click', () => this.toggle());\n        trigger.setAttribute('aria-haspopup', 'dialog');\n        trigger.setAttribute('aria-expanded', this.isOpen.toString());\n    }\n\n    async loadEntries() {\n        this.isLoading = true;\n        this.renderLoading();\n\n        try {\n            const response = await fetch(\n                `${this.baseUrl}/api/changelog/${this.options.projectId}/entries`\n            );\n\n            if (!response.ok) {\n                throw new Error(`HTTP ${response.status}`);\n            }\n\n            const data = await response.json();\n            this.entries = data.items || [];\n            this.project = data.project;\n            this.renderEntries(this.entries);\n        } catch (error) {\n            console.error('Failed to load changelog entries:', error);\n            this.renderError();\n        } finally {\n            this.isLoading = false;\n        }\n    }\n\n    render() {\n        const header = this.options.isPopup ? this.createHeader() : null;\n        const content = document.createElement('div');\n        content.className = 'changerawr-entries';\n\n        const footer = this.createFooter();\n\n        this.container.innerHTML = '';\n        if (header) this.container.appendChild(header);\n        this.container.appendChild(content);\n        this.container.appendChild(footer);\n    }\n\n    createHeader() {\n        const header = document.createElement('div');\n        header.className = 'changerawr-header';\n\n        const title = document.createElement('div');\n        title.className = 'changerawr-header-title';\n        title.textContent = this.project?.name || 'Changelog';\n\n        const closeBtn = document.createElement('button');\n        closeBtn.className = 'changerawr-close';\n        closeBtn.innerHTML = '✕';\n        closeBtn.setAttribute('aria-label', 'Close changelog');\n        closeBtn.addEventListener('click', () => this.close());\n\n        header.appendChild(title);\n        header.appendChild(closeBtn);\n\n        return header;\n    }\n\n    createFooter() {\n        const footer = document.createElement('div');\n        footer.className = 'changerawr-footer';\n\n        const poweredBy = document.createElement('span');\n        poweredBy.innerHTML = 'Powered by <a href=\"https://github.com/supernova3339/changerawr\" target=\"_blank\" rel=\"noopener\">Changerawr</a>';\n\n        const rssLink = document.createElement('a');\n        rssLink.href = `${this.baseUrl}/changelog/${this.options.projectId}/rss.xml`;\n        rssLink.textContent = 'RSS';\n        rssLink.target = '_blank';\n        rssLink.rel = 'noopener';\n\n        footer.appendChild(poweredBy);\n        footer.appendChild(rssLink);\n\n        return footer;\n    }\n\n    renderLoading() {\n        const content = this.container.querySelector('.changerawr-entries');\n        if (!content) return;\n\n        content.innerHTML = '<div class=\"changerawr-loading\"><div class=\"changerawr-spinner\"></div></div>';\n    }\n\n    renderError() {\n        const content = this.container.querySelector('.changerawr-entries');\n        if (!content) return;\n\n        content.innerHTML = `\n            <div class=\"changerawr-error\">\n                <div class=\"changerawr-error-icon\">⚠️</div>\n                <div class=\"changerawr-error-message\">Failed to load changelog</div>\n            </div>\n        `;\n    }\n\n    renderEntries(entries) {\n        const content = this.container.querySelector('.changerawr-entries');\n        if (!content) return;\n\n        content.innerHTML = '';\n\n        if (!entries || entries.length === 0) {\n            content.innerHTML = `\n                <div class=\"changerawr-empty\">\n                    <div class=\"changerawr-empty-icon\">📰</div>\n                    <div class=\"changerawr-empty-message\">No changelog entries yet</div>\n                </div>\n            `;\n            return;\n        }\n\n        const entriesToShow = entries.slice(0, this.options.maxEntries);\n\n        entriesToShow.forEach((entry, index) => {\n            const entryEl = document.createElement('div');\n            entryEl.className = 'changerawr-entry';\n            entryEl.style.animationDelay = `${index * 0.1}s`;\n\n            // Tags\n            if (entry.tags && entry.tags.length > 0) {\n                const tagsContainer = document.createElement('div');\n                tagsContainer.className = 'changerawr-entry-meta';\n\n                entry.tags.forEach(tag => {\n                    const tagEl = document.createElement('span');\n                    tagEl.className = 'changerawr-tag';\n                    tagEl.textContent = tag.name;\n                    if (tag.color) {\n                        tagEl.style.backgroundColor = tag.color + '20';\n                        tagEl.style.color = tag.color;\n                    }\n                    tagsContainer.appendChild(tagEl);\n                });\n\n                entryEl.appendChild(tagsContainer);\n            }\n\n            // Title\n            const title = document.createElement('div');\n            title.className = 'changerawr-entry-title';\n            title.textContent = entry.title;\n            entryEl.appendChild(title);\n\n            // Excerpt/Content\n            if (entry.excerpt) {\n                const excerpt = document.createElement('div');\n                excerpt.className = 'changerawr-entry-content';\n                excerpt.textContent = entry.excerpt;\n                entryEl.appendChild(excerpt);\n            }\n\n            // Read more link\n            const readMore = document.createElement('a');\n            readMore.className = 'changerawr-read-more';\n            readMore.href = `${this.baseUrl}/changelog/${this.options.projectId}/${entry.id}`;\n            readMore.textContent = 'Read more →';\n            readMore.target = '_blank';\n            readMore.rel = 'noopener';\n            entryEl.appendChild(readMore);\n\n            content.appendChild(entryEl);\n        });\n    }\n\n    open() {\n        if (this.isOpen) return;\n\n        this.isOpen = true;\n        this.container.classList.add('open');\n        this.container.classList.remove('hidden');\n\n        const trigger = this.options.trigger ? document.getElementById(this.options.trigger) : null;\n        if (trigger) {\n            trigger.setAttribute('aria-expanded', 'true');\n        }\n\n        // Focus first focusable element\n        const focusable = this.container.querySelectorAll(\n            'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n        );\n        if (focusable.length > 0) {\n            focusable[0].focus();\n        }\n    }\n\n    close() {\n        if (!this.isOpen) return;\n\n        this.isOpen = false;\n        this.container.classList.remove('open');\n\n        if (this.options.isPopup) {\n            this.container.classList.add('hidden');\n        }\n\n        const trigger = this.options.trigger ? document.getElementById(this.options.trigger) : null;\n        if (trigger) {\n            trigger.setAttribute('aria-expanded', 'false');\n            trigger.focus();\n        }\n    }\n\n    toggle() {\n        if (this.isOpen) {\n            this.close();\n        } else {\n            this.open();\n        }\n    }\n}\n\n// Export globally for browser\nwindow.ChangerawrWidget = {\n    init: (options) => {\n        const container = options.container || document.getElementById('changerawr-widget');\n        return new ChangelogWidget(container, options);\n    }\n};\n"
  },
  {
    "path": "widgets/variants/floating.js",
    "content": "/**\n * Floating Changelog Widget\n * A floating button with badge count that expands to show changelog entries\n */\n\nclass ChangelogFloatingWidget {\n    constructor(container, options) {\n        this.container = container;\n        this.options = {\n            theme: 'light',\n            position: 'bottom-right',\n            maxEntries: 5,\n            customCSS: null,\n            buttonText: \"What's New\",\n            showBadge: true,\n            ...options\n        };\n\n        this.isOpen = false;\n        this.isLoading = false;\n        this.unreadCount = 0;\n        this.entries = [];\n        this.project = null;\n        this.baseUrl = options.baseUrl || process.env.NEXT_PUBLIC_APP_URL || '';\n\n        this.init();\n    }\n\n    async loadStyles() {\n        // Load core CSS files\n        const cssFiles = [\n            '/widgets/core/styles/variables.css',\n            '/widgets/core/styles/reset.css',\n            '/widgets/core/styles/common.css',\n            '/widgets/core/styles/floating.css'\n        ];\n\n        for (const file of cssFiles) {\n            const href = this.baseUrl + file;\n            if (!document.querySelector(`link[href=\"${href}\"]`)) {\n                const link = document.createElement('link');\n                link.rel = 'stylesheet';\n                link.href = href;\n                document.head.appendChild(link);\n            }\n        }\n\n        // Inject custom CSS if provided\n        if (this.options.customCSS) {\n            const styleId = `changerawr-custom-css-${this.options.projectId || 'default'}`;\n            let customStyle = document.getElementById(styleId);\n\n            if (!customStyle) {\n                customStyle = document.createElement('style');\n                customStyle.id = styleId;\n                document.head.appendChild(customStyle);\n            }\n\n            customStyle.textContent = this.options.customCSS;\n        }\n    }\n\n    async init() {\n        await this.loadStyles();\n\n        // Setup container\n        this.container.classList.add('changerawr-widget', 'changerawr-floating');\n\n        if (this.options.theme === 'dark') {\n            this.container.classList.add('changerawr-theme-dark');\n        }\n\n        this.container.classList.add(`changerawr-position-${this.options.position}`);\n        this.container.setAttribute('role', 'region');\n        this.container.setAttribute('aria-label', 'Changelog updates');\n\n        this.render();\n        await this.loadEntries();\n        this.attachEventListeners();\n    }\n\n    render() {\n        // Clear container\n        this.container.innerHTML = '';\n\n        // Create button wrapper with relative positioning\n        const buttonWrapper = document.createElement('div');\n        buttonWrapper.className = 'changerawr-floating-wrapper';\n        buttonWrapper.style.position = 'relative';\n        buttonWrapper.style.display = 'inline-block';\n\n        // Create floating button\n        const button = document.createElement('button');\n        button.className = 'changerawr-floating-button';\n        button.type = 'button';\n        button.setAttribute('aria-label', 'Open changelog');\n        button.setAttribute('aria-expanded', 'false');\n        button.setAttribute('aria-haspopup', 'dialog');\n\n        // Button content\n        const buttonContent = document.createElement('div');\n        buttonContent.className = 'changerawr-floating-button-content';\n        buttonContent.style.display = 'flex';\n        buttonContent.style.alignItems = 'center';\n        buttonContent.style.gap = '8px';\n\n        const icon = document.createElement('span');\n        icon.className = 'changerawr-floating-icon';\n        icon.innerHTML = '📰';\n        icon.style.fontSize = '1.2em';\n        icon.style.flexShrink = '0';\n\n        const text = document.createElement('span');\n        text.className = 'changerawr-floating-text';\n        text.textContent = this.options.buttonText;\n        text.style.whiteSpace = 'nowrap';\n\n        buttonContent.appendChild(icon);\n        buttonContent.appendChild(text);\n        button.appendChild(buttonContent);\n\n        // Badge\n        if (this.options.showBadge) {\n            const badge = document.createElement('span');\n            badge.className = 'changerawr-floating-badge';\n            badge.setAttribute('aria-hidden', 'true');\n            badge.textContent = '0';\n            badge.style.display = 'none';\n            button.appendChild(badge);\n        }\n\n        // Create panel\n        const panel = document.createElement('div');\n        panel.className = 'changerawr-floating-panel';\n        panel.setAttribute('role', 'dialog');\n        panel.setAttribute('aria-label', 'Changelog entries');\n        panel.setAttribute('aria-hidden', 'true');\n        panel.style.display = 'none';\n\n        // Panel header\n        const header = document.createElement('div');\n        header.className = 'changerawr-floating-panel-header';\n        header.style.display = 'flex';\n        header.style.justifyContent = 'space-between';\n        header.style.alignItems = 'center';\n        header.style.padding = '16px';\n        header.style.borderBottom = '1px solid var(--changerawr-border-color, #eaeaea)';\n\n        const title = document.createElement('h2');\n        title.className = 'changerawr-floating-panel-title';\n        title.textContent = this.project?.name || 'Changelog';\n        title.style.margin = '0';\n        title.style.fontSize = '16px';\n        title.style.fontWeight = '600';\n\n        const closeBtn = document.createElement('button');\n        closeBtn.className = 'changerawr-floating-close-btn';\n        closeBtn.type = 'button';\n        closeBtn.innerHTML = '✕';\n        closeBtn.setAttribute('aria-label', 'Close changelog');\n        closeBtn.style.background = 'none';\n        closeBtn.style.border = 'none';\n        closeBtn.style.padding = '4px';\n        closeBtn.style.cursor = 'pointer';\n        closeBtn.style.fontSize = '20px';\n        closeBtn.style.color = 'var(--changerawr-text-secondary, #666666)';\n        closeBtn.style.display = 'flex';\n        closeBtn.style.alignItems = 'center';\n        closeBtn.style.justifyContent = 'center';\n\n        header.appendChild(title);\n        header.appendChild(closeBtn);\n\n        // Panel content area\n        const content = document.createElement('div');\n        content.className = 'changerawr-floating-panel-content';\n        content.style.flex = '1';\n        content.style.overflowY = 'auto';\n        content.style.padding = '0';\n\n        // Panel footer\n        const footer = document.createElement('div');\n        footer.className = 'changerawr-floating-panel-footer';\n        footer.style.padding = '12px 16px';\n        footer.style.borderTop = '1px solid var(--changerawr-border-color, #eaeaea)';\n        footer.style.fontSize = '12px';\n        footer.style.color = 'var(--changerawr-text-secondary, #666666)';\n        footer.style.display = 'flex';\n        footer.style.justifyContent = 'space-between';\n        footer.style.alignItems = 'center';\n        footer.style.gap = '12px';\n\n        const poweredBy = document.createElement('span');\n        poweredBy.innerHTML = 'Powered by <a href=\"https://github.com/supernova3339/changerawr\" target=\"_blank\" rel=\"noopener\" style=\"color: inherit; text-decoration: none;\">Changerawr</a>';\n\n        const rssLink = document.createElement('a');\n        rssLink.href = `${this.baseUrl}/changelog/${this.options.projectId}/rss.xml`;\n        rssLink.textContent = 'RSS';\n        rssLink.target = '_blank';\n        rssLink.rel = 'noopener';\n        rssLink.style.color = 'inherit';\n        rssLink.style.textDecoration = 'none';\n\n        footer.appendChild(poweredBy);\n        footer.appendChild(rssLink);\n\n        // Assemble panel\n        panel.appendChild(header);\n        panel.appendChild(content);\n        panel.appendChild(footer);\n\n        // Assemble button wrapper\n        buttonWrapper.appendChild(button);\n        buttonWrapper.appendChild(panel);\n\n        // Add to container\n        this.container.appendChild(buttonWrapper);\n\n        // Store references\n        this.button = button;\n        this.panel = panel;\n        this.panelContent = content;\n        this.badge = button.querySelector('.changerawr-floating-badge');\n        this.closeBtn = closeBtn;\n    }\n\n    attachEventListeners() {\n        this.button.addEventListener('click', (e) => {\n            e.preventDefault();\n            this.toggle();\n        });\n\n        this.closeBtn.addEventListener('click', (e) => {\n            e.preventDefault();\n            e.stopPropagation();\n            this.close();\n        });\n\n        // Close on escape\n        document.addEventListener('keydown', (e) => {\n            if (e.key === 'Escape' && this.isOpen) {\n                this.close();\n            }\n        });\n\n        // Close when clicking outside\n        document.addEventListener('click', (e) => {\n            if (this.isOpen && !this.button.contains(e.target) && !this.panel.contains(e.target)) {\n                this.close();\n            }\n        });\n    }\n\n    async loadEntries() {\n        this.isLoading = true;\n\n        try {\n            const response = await fetch(\n                `${this.baseUrl}/api/changelog/${this.options.projectId}/entries`\n            );\n\n            if (!response.ok) {\n                throw new Error(`HTTP ${response.status}`);\n            }\n\n            const data = await response.json();\n            this.entries = data.items || [];\n            this.project = data.project;\n\n            // Calculate unread count (entries from last 7 days)\n            const weekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);\n            this.unreadCount = this.entries.filter(entry => {\n                const entryDate = new Date(entry.createdAt).getTime();\n                return entryDate > weekAgo;\n            }).length;\n\n            this.updateBadge();\n        } catch (error) {\n            console.error('Failed to load changelog entries:', error);\n            this.unreadCount = 0;\n        } finally {\n            this.isLoading = false;\n        }\n    }\n\n    updateBadge() {\n        if (!this.badge) return;\n\n        if (this.unreadCount > 0) {\n            this.badge.textContent = this.unreadCount > 99 ? '99+' : this.unreadCount.toString();\n            this.badge.style.display = 'flex';\n        } else {\n            this.badge.style.display = 'none';\n        }\n    }\n\n    renderEntries() {\n        this.panelContent.innerHTML = '';\n\n        if (!this.entries || this.entries.length === 0) {\n            const empty = document.createElement('div');\n            empty.className = 'changerawr-floating-empty';\n            empty.style.padding = '48px 24px';\n            empty.style.textAlign = 'center';\n            empty.style.color = 'var(--changerawr-text-secondary, #666666)';\n\n            const emptyIcon = document.createElement('div');\n            emptyIcon.style.fontSize = '40px';\n            emptyIcon.style.marginBottom = '16px';\n            emptyIcon.style.opacity = '0.5';\n            emptyIcon.textContent = '📰';\n\n            const emptyMsg = document.createElement('div');\n            emptyMsg.style.fontSize = '14px';\n            emptyMsg.textContent = 'No changelog entries yet';\n\n            empty.appendChild(emptyIcon);\n            empty.appendChild(emptyMsg);\n            this.panelContent.appendChild(empty);\n            return;\n        }\n\n        const entriesToShow = this.entries.slice(0, this.options.maxEntries);\n\n        entriesToShow.forEach((entry, index) => {\n            const entryEl = document.createElement('div');\n            entryEl.className = 'changerawr-floating-entry';\n            entryEl.style.padding = '12px 16px';\n            entryEl.style.borderBottom = '1px solid var(--changerawr-border-color-light, #f5f5f5)';\n            entryEl.style.transition = 'background-color 0.2s ease';\n\n            // Tags\n            if (entry.tags && entry.tags.length > 0) {\n                const tagsContainer = document.createElement('div');\n                tagsContainer.style.display = 'flex';\n                tagsContainer.style.gap = '6px';\n                tagsContainer.style.marginBottom = '8px';\n                tagsContainer.style.flexWrap = 'wrap';\n\n                entry.tags.forEach(tag => {\n                    const tagEl = document.createElement('span');\n                    tagEl.className = 'changerawr-floating-tag';\n                    tagEl.textContent = tag.name;\n                    tagEl.style.display = 'inline-block';\n                    tagEl.style.padding = '3px 8px';\n                    tagEl.style.fontSize = '11px';\n                    tagEl.style.fontWeight = '500';\n                    tagEl.style.borderRadius = '4px';\n                    tagEl.style.whiteSpace = 'nowrap';\n\n                    if (tag.color) {\n                        tagEl.style.backgroundColor = tag.color + '20';\n                        tagEl.style.color = tag.color;\n                    } else {\n                        tagEl.style.backgroundColor = 'var(--changerawr-primary-light, #e8f2ff)';\n                        tagEl.style.color = 'var(--changerawr-primary-color, #0066ff)';\n                    }\n\n                    tagsContainer.appendChild(tagEl);\n                });\n\n                entryEl.appendChild(tagsContainer);\n            }\n\n            // Title\n            const titleEl = document.createElement('h3');\n            titleEl.className = 'changerawr-floating-entry-title';\n            titleEl.textContent = entry.title;\n            titleEl.style.margin = '0 0 6px 0';\n            titleEl.style.fontSize = '14px';\n            titleEl.style.fontWeight = '600';\n            titleEl.style.color = 'var(--changerawr-text-primary, #1a1a1a)';\n            titleEl.style.lineHeight = '1.4';\n            entryEl.appendChild(titleEl);\n\n            // Content\n            if (entry.excerpt) {\n                const contentEl = document.createElement('p');\n                contentEl.className = 'changerawr-floating-entry-content';\n                contentEl.textContent = entry.excerpt;\n                contentEl.style.margin = '0 0 8px 0';\n                contentEl.style.fontSize = '13px';\n                contentEl.style.color = 'var(--changerawr-text-secondary, #666666)';\n                contentEl.style.lineHeight = '1.5';\n                contentEl.style.display = '-webkit-box';\n                contentEl.style.webkitLineClamp = '3';\n                contentEl.style.webkitBoxOrient = 'vertical';\n                contentEl.style.overflow = 'hidden';\n                contentEl.style.textOverflow = 'ellipsis';\n                entryEl.appendChild(contentEl);\n            }\n\n            // Read more link\n            const linkEl = document.createElement('a');\n            linkEl.className = 'changerawr-floating-read-more';\n            linkEl.href = `${this.baseUrl}/changelog/${this.options.projectId}/${entry.id}`;\n            linkEl.textContent = 'Read more →';\n            linkEl.target = '_blank';\n            linkEl.rel = 'noopener';\n            linkEl.style.display = 'inline-block';\n            linkEl.style.marginTop = '6px';\n            linkEl.style.fontSize = '12px';\n            linkEl.style.fontWeight = '500';\n            linkEl.style.color = 'var(--changerawr-primary-color, #0066ff)';\n            linkEl.style.textDecoration = 'none';\n            linkEl.style.cursor = 'pointer';\n            linkEl.style.padding = '4px 0';\n\n            linkEl.addEventListener('mouseenter', () => {\n                linkEl.style.textDecoration = 'underline';\n            });\n\n            linkEl.addEventListener('mouseleave', () => {\n                linkEl.style.textDecoration = 'none';\n            });\n\n            entryEl.appendChild(linkEl);\n\n            // Hover effect\n            entryEl.addEventListener('mouseenter', () => {\n                entryEl.style.backgroundColor = 'var(--changerawr-bg-hover, #f5f5f5)';\n            });\n\n            entryEl.addEventListener('mouseleave', () => {\n                entryEl.style.backgroundColor = 'transparent';\n            });\n\n            this.panelContent.appendChild(entryEl);\n        });\n    }\n\n    open() {\n        if (this.isOpen) return;\n\n        this.isOpen = true;\n        this.panel.style.display = 'flex';\n        this.panel.setAttribute('aria-hidden', 'false');\n        this.button.setAttribute('aria-expanded', 'true');\n\n        // Render entries when opening\n        this.renderEntries();\n\n        // Trigger animation with a small delay\n        setTimeout(() => {\n            this.panel.classList.add('changerawr-floating-panel-open');\n        }, 10);\n\n        // Focus first element\n        setTimeout(() => {\n            const firstLink = this.panel.querySelector('a');\n            if (firstLink) {\n                firstLink.focus();\n            }\n        }, 100);\n    }\n\n    close() {\n        if (!this.isOpen) return;\n\n        this.isOpen = false;\n        this.panel.classList.remove('changerawr-floating-panel-open');\n\n        // Hide panel after animation\n        setTimeout(() => {\n            if (!this.isOpen) {\n                this.panel.style.display = 'none';\n                this.panel.setAttribute('aria-hidden', 'true');\n            }\n        }, 300);\n\n        this.button.setAttribute('aria-expanded', 'false');\n        this.button.focus();\n    }\n\n    toggle() {\n        if (this.isOpen) {\n            this.close();\n        } else {\n            this.open();\n        }\n    }\n}\n\n// Export globally for browser\nwindow.ChangerawrWidget = {\n    init: (options) => {\n        const container = options.container || document.getElementById('changerawr-widget');\n        return new ChangelogFloatingWidget(container, options);\n    }\n};\n"
  },
  {
    "path": "widgets/variants/modal.js",
    "content": "/**\n * Modal Changelog Widget\n * Full-screen overlay modal for changelog entries\n */\n\nclass ChangelogModalWidget {\n    constructor(container, options) {\n        this.container = container;\n        this.options = {\n            theme: 'light',\n            maxEntries: 10,\n            autoOpen: false,\n            trigger: null,\n            customCSS: null,\n            showOverlay: true,\n            ...options\n        };\n\n        this.isOpen = false;\n        this.isLoading = false;\n        this.baseUrl = options.baseUrl || process.env.NEXT_PUBLIC_APP_URL || '';\n        this.init();\n    }\n\n    async loadStyles() {\n        // Load core CSS files\n        const cssFiles = [\n            '/widgets/core/styles/variables.css',\n            '/widgets/core/styles/reset.css',\n            '/widgets/core/styles/common.css',\n            '/widgets/core/styles/modal.css'\n        ];\n\n        for (const file of cssFiles) {\n            if (!document.querySelector(`link[href=\"${this.baseUrl}${file}\"]`)) {\n                const link = document.createElement('link');\n                link.rel = 'stylesheet';\n                link.href = this.baseUrl + file;\n                document.head.appendChild(link);\n            }\n        }\n\n        // Inject custom CSS if provided\n        if (this.options.customCSS) {\n            const styleId = `changerawr-custom-css-${this.options.projectId || 'default'}`;\n            let customStyle = document.getElementById(styleId);\n\n            if (!customStyle) {\n                customStyle = document.createElement('style');\n                customStyle.id = styleId;\n                document.head.appendChild(customStyle);\n            }\n\n            customStyle.textContent = this.options.customCSS;\n        }\n    }\n\n    async init() {\n        await this.loadStyles();\n\n        // Apply base classes\n        this.container.classList.add('changerawr-widget', 'changerawr-modal');\n\n        // Apply theme\n        if (this.options.theme === 'dark') {\n            this.container.classList.add('dark');\n        }\n\n        // Initially hidden\n        this.container.style.display = 'none';\n\n        // ARIA attributes\n        this.container.setAttribute('role', 'dialog');\n        this.container.setAttribute('aria-modal', 'true');\n        this.container.setAttribute('aria-label', 'Changelog updates');\n\n        this.render();\n        await this.loadEntries();\n        this.setupKeyboardNavigation();\n\n        if (this.options.trigger) {\n            this.setupTriggerButton();\n        }\n\n        if (this.options.autoOpen) {\n            setTimeout(() => this.open(), 1000);\n        }\n    }\n\n    setupKeyboardNavigation() {\n        this.container.addEventListener('keydown', (e) => {\n            if (e.key === 'Escape' && this.isOpen) {\n                this.close();\n            }\n\n            if (e.key === 'Tab' && this.isOpen) {\n                const focusableElements = this.container.querySelectorAll(\n                    'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n                );\n                const firstElement = focusableElements[0];\n                const lastElement = focusableElements[focusableElements.length - 1];\n\n                if (e.shiftKey) {\n                    if (document.activeElement === firstElement) {\n                        e.preventDefault();\n                        lastElement.focus();\n                    }\n                } else {\n                    if (document.activeElement === lastElement) {\n                        e.preventDefault();\n                        firstElement.focus();\n                    }\n                }\n            }\n        });\n    }\n\n    setupTriggerButton() {\n        const trigger = document.getElementById(this.options.trigger);\n        if (!trigger) {\n            console.error(`Changerawr: Trigger button '${this.options.trigger}' not found`);\n            return;\n        }\n\n        trigger.addEventListener('click', () => this.toggle());\n        trigger.setAttribute('aria-haspopup', 'dialog');\n        trigger.setAttribute('aria-expanded', this.isOpen.toString());\n    }\n\n    async loadEntries() {\n        this.isLoading = true;\n        this.renderLoading();\n\n        try {\n            const response = await fetch(\n                `${this.baseUrl}/api/changelog/${this.options.projectId}/entries`\n            );\n\n            if (!response.ok) {\n                throw new Error(`HTTP ${response.status}`);\n            }\n\n            const data = await response.json();\n            this.entries = data.items || [];\n            this.project = data.project;\n            this.renderEntries(this.entries);\n        } catch (error) {\n            console.error('Failed to load changelog entries:', error);\n            this.renderError();\n        } finally {\n            this.isLoading = false;\n        }\n    }\n\n    render() {\n        // Overlay\n        if (this.options.showOverlay) {\n            const overlay = document.createElement('div');\n            overlay.className = 'changerawr-modal-overlay';\n            overlay.addEventListener('click', () => this.close());\n            this.container.appendChild(overlay);\n        }\n\n        // Modal content\n        const modal = document.createElement('div');\n        modal.className = 'changerawr-modal-content';\n\n        const header = this.createHeader();\n        const content = document.createElement('div');\n        content.className = 'changerawr-entries';\n        const footer = this.createFooter();\n\n        modal.appendChild(header);\n        modal.appendChild(content);\n        modal.appendChild(footer);\n\n        this.container.appendChild(modal);\n    }\n\n    createHeader() {\n        const header = document.createElement('div');\n        header.className = 'changerawr-header';\n\n        const title = document.createElement('div');\n        title.className = 'changerawr-header-title';\n        title.textContent = this.project?.name || 'Changelog';\n\n        const closeBtn = document.createElement('button');\n        closeBtn.className = 'changerawr-close';\n        closeBtn.innerHTML = '✕';\n        closeBtn.setAttribute('aria-label', 'Close changelog');\n        closeBtn.addEventListener('click', () => this.close());\n\n        header.appendChild(title);\n        header.appendChild(closeBtn);\n\n        return header;\n    }\n\n    createFooter() {\n        const footer = document.createElement('div');\n        footer.className = 'changerawr-footer';\n\n        const poweredBy = document.createElement('span');\n        poweredBy.innerHTML = 'Powered by <a href=\"https://github.com/supernova3339/changerawr\" target=\"_blank\" rel=\"noopener\">Changerawr</a>';\n\n        const rssLink = document.createElement('a');\n        rssLink.href = `${this.baseUrl}/changelog/${this.options.projectId}/rss.xml`;\n        rssLink.textContent = 'RSS';\n        rssLink.target = '_blank';\n        rssLink.rel = 'noopener';\n\n        footer.appendChild(poweredBy);\n        footer.appendChild(rssLink);\n\n        return footer;\n    }\n\n    renderLoading() {\n        const content = this.container.querySelector('.changerawr-entries');\n        if (!content) return;\n\n        content.innerHTML = '<div class=\"changerawr-loading\"><div class=\"changerawr-spinner\"></div></div>';\n    }\n\n    renderError() {\n        const content = this.container.querySelector('.changerawr-entries');\n        if (!content) return;\n\n        content.innerHTML = `\n            <div class=\"changerawr-error\">\n                <div class=\"changerawr-error-icon\">⚠️</div>\n                <div class=\"changerawr-error-message\">Failed to load changelog</div>\n            </div>\n        `;\n    }\n\n    renderEntries(entries) {\n        const content = this.container.querySelector('.changerawr-entries');\n        if (!content) return;\n\n        content.innerHTML = '';\n\n        if (!entries || entries.length === 0) {\n            content.innerHTML = `\n                <div class=\"changerawr-empty\">\n                    <div class=\"changerawr-empty-icon\">📰</div>\n                    <div class=\"changerawr-empty-message\">No changelog entries yet</div>\n                </div>\n            `;\n            return;\n        }\n\n        const entriesToShow = entries.slice(0, this.options.maxEntries);\n\n        entriesToShow.forEach((entry, index) => {\n            const entryEl = document.createElement('div');\n            entryEl.className = 'changerawr-entry';\n            entryEl.style.animationDelay = `${index * 0.05}s`;\n\n            // Date\n            const date = document.createElement('div');\n            date.className = 'changerawr-entry-date';\n            const entryDate = new Date(entry.createdAt);\n            date.textContent = entryDate.toLocaleDateString('en-US', {\n                year: 'numeric',\n                month: 'long',\n                day: 'numeric'\n            });\n            entryEl.appendChild(date);\n\n            // Tags\n            if (entry.tags && entry.tags.length > 0) {\n                const tagsContainer = document.createElement('div');\n                tagsContainer.className = 'changerawr-entry-meta';\n\n                entry.tags.forEach(tag => {\n                    const tagEl = document.createElement('span');\n                    tagEl.className = 'changerawr-tag';\n                    tagEl.textContent = tag.name;\n                    if (tag.color) {\n                        tagEl.style.backgroundColor = tag.color + '20';\n                        tagEl.style.color = tag.color;\n                    }\n                    tagsContainer.appendChild(tagEl);\n                });\n\n                entryEl.appendChild(tagsContainer);\n            }\n\n            // Title\n            const title = document.createElement('div');\n            title.className = 'changerawr-entry-title';\n            title.textContent = entry.title;\n            entryEl.appendChild(title);\n\n            // Excerpt/Content\n            if (entry.excerpt) {\n                const excerpt = document.createElement('div');\n                excerpt.className = 'changerawr-entry-content';\n                excerpt.textContent = entry.excerpt;\n                entryEl.appendChild(excerpt);\n            }\n\n            // Read more link\n            const readMore = document.createElement('a');\n            readMore.className = 'changerawr-read-more';\n            readMore.href = `${this.baseUrl}/changelog/${this.options.projectId}/${entry.id}`;\n            readMore.textContent = 'Read more →';\n            readMore.target = '_blank';\n            readMore.rel = 'noopener';\n            entryEl.appendChild(readMore);\n\n            content.appendChild(entryEl);\n        });\n    }\n\n    open() {\n        if (this.isOpen) return;\n\n        this.isOpen = true;\n        this.container.style.display = 'flex';\n        this.container.classList.add('open');\n\n        // Prevent body scroll\n        document.body.style.overflow = 'hidden';\n\n        const trigger = this.options.trigger ? document.getElementById(this.options.trigger) : null;\n        if (trigger) {\n            trigger.setAttribute('aria-expanded', 'true');\n        }\n\n        // Focus first focusable element\n        setTimeout(() => {\n            const focusable = this.container.querySelectorAll(\n                'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n            );\n            if (focusable.length > 0) {\n                focusable[0].focus();\n            }\n        }, 100);\n    }\n\n    close() {\n        if (!this.isOpen) return;\n\n        this.isOpen = false;\n        this.container.classList.remove('open');\n\n        // Restore body scroll\n        document.body.style.overflow = '';\n\n        setTimeout(() => {\n            this.container.style.display = 'none';\n        }, 300);\n\n        const trigger = this.options.trigger ? document.getElementById(this.options.trigger) : null;\n        if (trigger) {\n            trigger.setAttribute('aria-expanded', 'false');\n            trigger.focus();\n        }\n    }\n\n    toggle() {\n        if (this.isOpen) {\n            this.close();\n        } else {\n            this.open();\n        }\n    }\n}\n\n// Export globally for browser\nwindow.ChangerawrWidget = {\n    init: (options) => {\n        const container = options.container || document.getElementById('changerawr-widget');\n        return new ChangelogModalWidget(container, options);\n    }\n};\n"
  }
]