[
  {
    "path": ".dockerignore",
    "content": "assets/\nREADME.md\nSECURITY.md\nCODE_OF_CONDUCT.md\nCODEOWNERS\nLICENSE\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[{*.go,Makefile,.gitmodules,go.mod,go.sum}]\nindent_style = tab\n\n[*.md]\nindent_style = tab\ntrim_trailing_whitespace = false\n\n[*.{yml,yaml,json}]\nindent_style = space\nindent_size = 2\n\n[*.{js,jsx,ts,tsx,css,less,sass,scss,vue,py}]\nindent_style = space\nindent_size = 4\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. iOS]\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n - Device: [e.g. iPhone6]\n - OS: [e.g. iOS8.1]\n - Browser [e.g. stock browser, safari]\n - Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file\n\nversion: 2\nupdates:\n  - package-ecosystem: \"npm\" # See documentation for possible values\n    directory: \"kubelab-ui/\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n    commit-message:\n      prefix: \":robot:\"\n  - package-ecosystem: \"gomod\" # See documentation for possible values\n    directory: \"kubelab-backend/\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n    commit-message:\n      prefix: \":robot:\"\n  # GitHub Actions\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    commit-message:\n      prefix: \":seedling:\"\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ '*' ]\n\njobs:\n\n  go-build:\n    name: Backend Build\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: kubelab-backend\n    strategy:\n      matrix:\n        goVer: [1.22]\n\n    steps:\n    - name: Set up Go ${{ matrix.goVer }}\n      uses: actions/setup-go@v5\n      with:\n        go-version: ${{ matrix.goVer }}\n      id: go\n\n    - name: Check out code into the Go module directory\n      uses: actions/checkout@v4\n\n    - name: Get dependencies\n      run: |\n        go get -v -t -d ./...\n        go mod vendor\n\n    - name: Test\n      run: |\n        go test -v ./...\n\n    - name: Build\n      run: |\n        go build -v ./...\n\n  ui-build:\n    name: Frontend Build\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: kubelab-ui\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n      - name: Install dependencies\n        run: npm install --legacy-peer-deps\n      - name: Build\n        run: npm run build\n"
  },
  {
    "path": ".github/workflows/docker-release.yml",
    "content": "name: Docker Image Build & Push\n\non:\n  release:\n    types: [created]\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n\njobs:\n  build-and-push-image:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Log in to the Container registry\n        uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata (tags, labels) for Docker\n        id: meta\n        uses: docker/metadata-action@60a0d343a0d8a18aedee9d34e62251f752153bdb\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n\n      - name: Build and push Docker image\n        uses: docker/build-push-action@5176d81f87c23d6fc96624dfdbcd9f3830bbe445\n        with:\n          context: .\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n"
  },
  {
    "path": ".github/workflows/pr-labels.yml",
    "content": "name: Size Label\non: pull_request\njobs:\n  size-label:\n    runs-on: ubuntu-latest\n    if: github.actor != 'dependabot[bot]'\n    steps:\n      - name: size-label\n        uses: \"pascalgn/size-label-action@v0.5.4\"\n        env:\n          GITHUB_TOKEN: \"${{ secrets.GITHUB_TOKEN }}\"\n"
  },
  {
    "path": ".gitignore",
    "content": "# Mac OS X files\n.DS_Store\n\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, build with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736\n.glide/\n\n# Dependency directories (remove the comment below to include it)\nvendor/\npb_data/\ntemp/\ntmp/\n\ndist/\n.builds/\n\n.cache\n.local\n.npm\n.env\n.ash_history\n/docker-compose.override.yml\n_temp/\nlocal_userlist_november.csv\nvenv/\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v4.3.0\n    hooks:\n      - id: check-yaml\n      - id: end-of-file-fixer\n      - id: trailing-whitespace\n        exclude: README.md\n  - repo: https://github.com/psf/black\n    rev: 22.8.0\n    hooks:\n      - id: black\n  - repo: https://github.com/dnephin/pre-commit-golang\n    rev: v0.5.0\n    hooks:\n      - id: go-fmt\n      - id: no-go-testing\n      # - id: golangci-lint\n      #   exclude: '^tmp/'\n      # - id: go-unit-tests\n  - repo: https://github.com/pre-commit/mirrors-eslint\n    rev: \"v8.25.0\"\n    hooks:\n      - id: eslint\n        additional_dependencies:\n        - eslint-config-next@12.1.6\n        files: ^ui/\n        types_or: [ts, tsx]\n  - repo: https://github.com/pre-commit/mirrors-prettier\n    rev: \"v2.7.1\"\n    hooks:\n      - id: prettier\n        files: ^ui/\n        types_or: [javascript, jsx, ts, tsx, json, css, scss, markdown]\n        additional_dependencies:\n          - prettier\n          - prettier-plugin-svelte\n          - svelte\n"
  },
  {
    "path": "CODEOWNERS",
    "content": "*   @janlauber\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\ninfo@natron.io.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\n<https://www.contributor-covenant.org/version/2/0/code_of_conduct.html>.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\n<https://www.contributor-covenant.org/faq>. Translations are available at\n<https://www.contributor-covenant.org/translations>.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nWhen contributing to this repository, please first discuss the change you wish to make via issue,\nemail, or any other method with the owners of this repository before making a change. \n\nPlease note we have a code of conduct, please follow it in all your interactions with the project.\n\n## Pull Request Process\n\n1. Ensure any install or build dependencies are removed before the end of the layer when doing a \n   build.\n2. Update the README.md with details of changes to the interface, this includes new environment \n   variables, exposed ports, useful file locations and container parameters.\n3. Increase the version numbers in any examples files and the README.md to the new version that this\n   Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).\n4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you \n   do not have permission to do that, you may request the second reviewer to merge it for you.\n\n## Code of Conduct\n\n### Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, gender identity and expression, level of experience,\nnationality, personal appearance, race, religion, or sexual identity and\norientation.\n\n### Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\nadvances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n### Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n### Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n### Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at [INSERT EMAIL ADDRESS]. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n### Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM golang:1.22-alpine AS backend-builder\nWORKDIR /build\nCOPY kubelab-backend/go.mod kubelab-backend/go.sum kubelab-backend/main.go ./\nCOPY kubelab-backend/hooks ./hooks\nCOPY kubelab-backend/pkg ./pkg\nCOPY kubelab-backend/vcluster-values.yaml ./vcluster-values.yaml\nRUN apk --no-cache add upx make git gcc libtool musl-dev ca-certificates dumb-init \\\n  && go mod tidy \\\n  && CGO_ENABLED=0 go build \\\n  && upx kubelab\n\nFROM node:lts-slim as ui-builder\nWORKDIR /build\nCOPY ./kubelab-ui/package*.json ./\nRUN rm -rf ./node_modules\nRUN rm -rf ./build\nCOPY ./kubelab-ui .\nRUN npm install --legacy-peer-deps\nRUN npm run build\n\nFROM alpine as runtime\nWORKDIR /app/kubelab\nCOPY --from=backend-builder /build/kubelab /app/kubelab/kubelab\nCOPY --from=backend-builder /build/vcluster-values.yaml /app/kubelab/vcluster-values.yaml\nCOPY ./kubelab-backend/pb_migrations ./pb_migrations\nCOPY --from=ui-builder /build/build /app/kubelab/pb_public\nEXPOSE 8090\nCMD [\"/app/kubelab/kubelab\",\"serve\", \"--http\", \"0.0.0.0:8090\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "Apache License\n==============\n\n_Version 2.0, January 2004_\n_&lt;<http://www.apache.org/licenses/>&gt;_\n\n### Terms and Conditions for use, reproduction, and distribution\n\n#### 1. Definitions\n\n“License” shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n“Licensor” shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n“Legal Entity” shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, “control” means **(i)** the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the\noutstanding shares, or **(iii)** beneficial ownership of such entity.\n\n“You” (or “Your”) shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n“Source” form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n“Object” form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n“Work” shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n“Derivative Works” shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n“Contribution” shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n“submitted” means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as “Not a Contribution.”\n\n“Contributor” shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n#### 2. Grant of Copyright License\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n#### 3. Grant of Patent License\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n#### 4. Redistribution\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\n* **(a)** You must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\n* **(b)** You must cause any modified files to carry prominent notices stating that You\nchanged the files; and\n* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\n* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\n\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n#### 5. Submission of Contributions\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n#### 6. Trademarks\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n#### 7. Disclaimer of Warranty\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an “AS IS” BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n#### 8. Limitation of Liability\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n#### 9. Accepting Warranty or Additional Liability\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\n_END OF TERMS AND CONDITIONS_\n\n### APPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets `[]` replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same “printed page” as the copyright notice for easier identification within\nthird-party archives.\n\n    Copyright 2023 Natron Tech GmbH / Natron Tech AG\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# KubeLab: The Ultimate Kubernetes Learning Platform \n\n<p align=\"center\">\n    <a href=\"https://kubelab.natron.io\">\n        <img height=\"130px\" src=\"assets/kubelab-logo.png\" />\n    </a>\n</p>\n\n<p align=\"center\">\n  <strong>\n    <a href=\"https://kubelab.natron.io/\">KubeLab</a>\n    <br />\n    Embark on your Kubernetes Journey through Hands-on Practice\n  </strong>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://github.com/natrontech/kubelab/issues\"><img\n    src=\"https://img.shields.io/github/issues/natrontech/kubelab\"\n    alt=\"Build\"\n  /></a>\n  <a href=\"https://github.com/natrontech/kubelab\"><img\n    src=\"https://img.shields.io/github/license/natrontech/kubelab\"\n    alt=\"License\"\n  /></a>\n  <img alt=\"GitHub go.mod Go version\" src=\"https://img.shields.io/github/go-mod/go-version/natrontech/kubelab/main/kubelab-backend?label=Go%20Version\" />\n  <img alt=\"GitHub Workflow Status\" src=\"https://img.shields.io/github/actions/workflow/status/natrontech/kubelab/ci.yml?label=CI\" />\n  <img alt=\"GitHub Workflow Status\" src=\"https://img.shields.io/github/actions/workflow/status/natrontech/kubelab/codeql.yml?label=CodeQL\" />\n  <img alt=\"GitHub Workflow Status\" src=\"https://img.shields.io/github/actions/workflow/status/natrontech/kubelab/docker-release.yml?label=Docker%20Release\" />\n</p>\n\n<h2></h2>\n\nWelcome to KubeLab! Our advanced web-based platform offers a rich set of interactive labs, specifically crafted for Kubernetes workshops. We aim to revolutionize your learning experience by making it more interactive, engaging, and practical. Our labs will help you grasp and apply complex Kubernetes concepts in a real-world context.\n\nKubeLab is a proud offering by [Natron Tech](https://natron.io), and if you're interested in a tailor-made Kubernetes services or workshops for your company, do not hesitate to reach out to us!\n\nKubeLab is built using:\n\n- [kubelab-agent](https://github.com/natrontech/kubelab-agent)\n- [xterm.js](https://xtermjs.org/)\n- [code-server](https://docs.linuxserver.io/images/docker-code-server/)\n- [pocketbase](https://pocketbase.io)\n- [vcluster](https://vcluster.com)\n\n<p align=\"center\">\n\t<img height=\"500px\" src=\"assets/screenrecording.gif\" />\n</p>\n\n**Please note:** This project is still in its early stages, and we're diligently working to enhance your experience. However, bugs might appear, and your patience and feedback will be greatly appreciated.\n\n---\n\n## Cloud Version\n\nYou can access the cloud version of KubeLab at [kubelab.ch](https://kubelab.ch). This version is hosted by us. As of security reasons, we do not let you to sign up for the cloud version. If you want to use KubeLab for your company, please contact us at [info@natron.io](mailto:info@natron.io). Also, when you want to host a KubeLab instance for your company, we can provide you with a hosted version of KubeLab.\n\n## Features\n\n### Web Terminal\n\nKubeLab features a smooth in-browser terminal, letting you execute commands and interact with your Kubernetes cluster in real-time, without needing any additional setup or software.\n\n### Code Editor\n\nKubeLab comes with a vscode-based code editor, allowing you to edit and run code directly from your browser. The editor supports syntax highlighting, code completion, and more. Watch out for the code editor button in the bottom left corner of your screen.\n\n### Dedicated Cluster Per Session\n\nEvery learning session on KubeLab has its own isolated Kubernetes cluster. This design ensures a secure and dedicated learning environment, enabling you to experiment with Kubernetes without impacting others.\n\n### Custom Kubernetes Labs\n\nWith KubeLab, you can define your own labs and exercises. Check out our workshops [here](https://github.com/natrontech/kubelab-workshops).\nAll the labs and exercises follow the same structure and can be easily created and shared.\n\nThe structure of a lab with an exercise is as follows:\n```bash\nkubelab-workshops/kubernetes-basics # Example workshop\n└── 01_introduction # Name of the lab\n    ├── 01_get_kubectl_version # Name of the exercise\n    │   ├── bootstrap.sh # The script that will be executed when the exercise starts\n    │   ├── check.sh # The script that will be executed to check if the exercise is solved\n    │   ├── docs.md # The text that will be displayed to the user\n    │   ├── hint.md # The hint that will be displayed to the user\n    │   └── solution.md # The solution that will be displayed to the user\n    └── docs.md # The text that will be displayed to the user for the lab\n```\n\n### Workshop Mode\n\nKubeLab offers a workshop mode, allowing you to hold your own Kubernetes workshops. Each user can have the `workshop` set to `true` and a `company` assigned. This will allow you to filter the users by company and see their progress. Then you can create a user with the role `admin` and when you log in with this user you can see the dashboards of all the companies. The users then also have an addional `request for help` button in the UI which will send a real-time notification to the dashboard.\n\n---\n\n## Development\n\nInterested in contributing to KubeLab? Please make sure you have the following prerequisites:\n\n- [Docker](https://docs.docker.com/get-docker/)\n- [Docker Compose](https://docs.docker.com/compose/install/)\n- [Node.js](https://nodejs.org/en/download/) (v18+)\n- [Go](https://golang.org/doc/install) (v1.22+)\n- [modd](https://github.com/cortesi/modd/releases)\n\nPlease refer to our detailed development guides for the [backend](./kubelab-backend/README.md) and [frontend](./kubelab-ui/README.md) to get started. For contributing, please read our [CONTRIBUTING.md](CONTRIBUTING.md) for information on code conduct and the process for submitting pull requests.\n\n## Deployment\n\nTo get started, ensure that you have a Kubernetes cluster (v1.27+).\n\n1. Take a look at the [deployment](./deployment) folder for an example deployment.\n2. Create a wildcard certificate and store it in a secret. (You can use [cert-manager](https://cert-manager.io/docs/) for this)\n```yaml\napiVersion: cert-manager.io/v1\nkind: Certificate\nmetadata:\n  name: kubelab-ch-wildcard-cert\n  namespace: cert-manager\nspec:\n  secretName: kubelab-ch-wildcard-cert\n  issuerRef:\n    name: letsencrypt\n    kind: ClusterIssuer\n  dnsNames:\n    - \"*.kubelab.ch\"\n  secretTemplate:\n    annotations:\n      reflector.v1.k8s.emberstack.com/reflection-allowed: \"true\"\n      reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: \"\"\n      reflector.v1.k8s.emberstack.com/reflection-auto-enabled: \"true\"\n      reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: \"\"\n```\n3. Deploy the [reflector](https://github.com/emberstack/kubernetes-reflector/tree/main/src/helm/reflector) to sync the TLS secret with each namespace.\n4. Deploy the KubeLab as described in the [deployment](./deployment) folder.\n5. Access the Pocketbase UI and create a admin user. (https://<ingress-url>/_)\n6. Disable the `create` authentication for the `labs` and `exercises` collections in Pocketbase. (This is because you wan't to use the `kubelab-fill` script to fill the labs and exercises collections.)\n   ![disable-creation-rule](./assets/disable-creation-rule.png)\n7. Run the [kubelab-fill](./kubelab-fill) script to fill the labs and exercises collections in Pocketbase. (Make sure to set the environment variables in the script `.env.example`)\n8. Now disable the `create` authentication for the `labs` and `exercises` collections in Pocketbase again.\n9. Create a user in the Pocketbase UI in the `users` collection and set the `role` to `user`.\n10. Access the KubeLab UI and login with the user you created in step 9. (https://<ingress-url>/)\n\n### Environment Variables\n\nThe following environment variables are required for KubeLab to function properly:\n\n| Variable Name               | Default                                         | Description                                                                                            |\n| --------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------ |\n| `LOCAL`                     | `false`                                         | Set to `true` if you're running KubeLab locally. It will take your local kubeconfig under .kube/config |\n| `KUBELAB_AGENT_IMAGE`       | `ghcr.io/natrontech/kubelab-agent:latest`       | The image for the agent                                                                                |\n| `CODE_SERVER_IMAGE`         | `ghcr.io/natrontech/kubelab-code-server:latest` | The image for the code-server                                                                          |\n| `ALLOWED_HOSTS`             | `*`                                             | The allowed hosts for the backend                                                                      |\n| `RESOURCE_NAME`             | `kubelab`                                       | The name of the resource                                                                               |\n| `AGENT_INGRESS_CLASS`       | `nginx`                                         | The ingress class for the agent                                                                        |\n| `PODS_LIMIT`                | `70`                                            | The maximum number of pods allowed per session                                                         |\n| `STORAGE_LIMIT`             | `50Gi`                                          | The maximum storage allowed per session                                                                |\n| `VCLUSTER_CHART_VERSION`    | `0.16.4`                                        | The version of the vcluster chart                                                                      |\n| `VCLUSTER_VALUES_FILE_PATH` | `./vcluster-values.yaml`                        | The path to the vcluster values file                                                                   |\n| `CronTick`                  | `* * * * *`                                     | The cron tick which creates user sessions for each lab and exercise                                    |\n| `TlsSecretName`             | `kubelab-tls`                                   | The name of the TLS secret which will be for each agent ingress instance (use a wildcard certificate)  |\n\n## Known Issues\n\n- Either you need to create a wildcard Certificate and use it as the default TLS secret or you need to use something like [reflector](https://github.com/emberstack/kubernetes-reflector/tree/main/src/helm/reflector) to sync the TLS secret with each namespace.\n- The labs need to be manually created via an upload script in [./kubelab-fill](./kubelab-fill). This will be automated in the future.\n- Signup is not yet implemented. We're working on it and want to make a free signup with a limited number of sessions available soon.\n- The frontend is not yet optimized for mobile devices. This is **not a priority** for us at the moment, but we'll get to it eventually.\n\n---\n\nBegin your Kubernetes journey with KubeLab today!\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\n| Version | Supported          |\n| ------- | ------------------ |\n| latest  | :white_check_mark: |\n\n## Reporting a Vulnerability\n\nOpen up an issue :)\n"
  },
  {
    "path": "assets/drawio/architecture.drawio",
    "content": "<mxfile host=\"app.diagrams.net\" modified=\"2023-06-12T06:58:34.998Z\" agent=\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36\" etag=\"NxmnkzNyEjIMfRW3cUqh\" version=\"21.3.8\" type=\"device\">\n  <diagram name=\"Page-1\" id=\"WUQg41ddvhskJoTHI96X\">\n    <mxGraphModel dx=\"1010\" dy=\"1143\" grid=\"1\" gridSize=\"10\" guides=\"1\" tooltips=\"1\" connect=\"1\" arrows=\"1\" fold=\"1\" page=\"1\" pageScale=\"1\" pageWidth=\"850\" pageHeight=\"1100\" math=\"0\" shadow=\"0\">\n      <root>\n        <mxCell id=\"0\" />\n        <mxCell id=\"1\" parent=\"0\" />\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-37\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.25;entryDx=0;entryDy=0;\" parent=\"1\" source=\"UOWZPONmYOvx8WyAluzy-5\" target=\"UOWZPONmYOvx8WyAluzy-9\" edge=\"1\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-38\" value=\"1\" style=\"edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\" parent=\"UOWZPONmYOvx8WyAluzy-37\" vertex=\"1\" connectable=\"0\">\n          <mxGeometry x=\"-0.8627\" y=\"-1\" relative=\"1\" as=\"geometry\">\n            <mxPoint as=\"offset\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-39\" value=\"n\" style=\"edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\" parent=\"UOWZPONmYOvx8WyAluzy-37\" vertex=\"1\" connectable=\"0\">\n          <mxGeometry x=\"0.8064\" y=\"2\" relative=\"1\" as=\"geometry\">\n            <mxPoint as=\"offset\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-5\" value=\"Users\" style=\"swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=none;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"100\" y=\"100\" width=\"180\" height=\"182\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-6\" value=\"+ username: type\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-5\" vertex=\"1\">\n          <mxGeometry y=\"26\" width=\"180\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"7reBWwmH2FoituciZp9q-1\" value=\"+ email: type\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-5\" vertex=\"1\">\n          <mxGeometry y=\"52\" width=\"180\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"7reBWwmH2FoituciZp9q-2\" value=\"+ name: type\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-5\" vertex=\"1\">\n          <mxGeometry y=\"78\" width=\"180\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"7reBWwmH2FoituciZp9q-3\" value=\"+ avatar: type\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-5\" vertex=\"1\">\n          <mxGeometry y=\"104\" width=\"180\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"7reBWwmH2FoituciZp9q-4\" value=\"+ totalScore: type\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-5\" vertex=\"1\">\n          <mxGeometry y=\"130\" width=\"180\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"7reBWwmH2FoituciZp9q-5\" value=\"+ avgMinutesToSolution: type\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-5\" vertex=\"1\">\n          <mxGeometry y=\"156\" width=\"180\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-25\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;\" parent=\"1\" source=\"UOWZPONmYOvx8WyAluzy-9\" target=\"UOWZPONmYOvx8WyAluzy-17\" edge=\"1\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-29\" value=\"1\" style=\"edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\" parent=\"UOWZPONmYOvx8WyAluzy-25\" vertex=\"1\" connectable=\"0\">\n          <mxGeometry x=\"0.6766\" y=\"1\" relative=\"1\" as=\"geometry\">\n            <mxPoint as=\"offset\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-30\" value=\"1\" style=\"edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\" parent=\"UOWZPONmYOvx8WyAluzy-25\" vertex=\"1\" connectable=\"0\">\n          <mxGeometry x=\"-0.7247\" y=\"-1\" relative=\"1\" as=\"geometry\">\n            <mxPoint as=\"offset\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-31\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;\" parent=\"1\" source=\"UOWZPONmYOvx8WyAluzy-21\" target=\"UOWZPONmYOvx8WyAluzy-13\" edge=\"1\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-32\" value=\"1\" style=\"edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\" parent=\"UOWZPONmYOvx8WyAluzy-31\" vertex=\"1\" connectable=\"0\">\n          <mxGeometry x=\"0.7067\" y=\"-1\" relative=\"1\" as=\"geometry\">\n            <mxPoint as=\"offset\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-33\" value=\"1\" style=\"edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\" parent=\"UOWZPONmYOvx8WyAluzy-31\" vertex=\"1\" connectable=\"0\">\n          <mxGeometry x=\"-0.7495\" y=\"1\" relative=\"1\" as=\"geometry\">\n            <mxPoint as=\"offset\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-34\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;\" parent=\"1\" source=\"UOWZPONmYOvx8WyAluzy-9\" target=\"UOWZPONmYOvx8WyAluzy-15\" edge=\"1\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-35\" value=\"1\" style=\"edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\" parent=\"UOWZPONmYOvx8WyAluzy-34\" vertex=\"1\" connectable=\"0\">\n          <mxGeometry x=\"-0.8376\" y=\"-1\" relative=\"1\" as=\"geometry\">\n            <mxPoint as=\"offset\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-36\" value=\"1\" style=\"edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\" parent=\"UOWZPONmYOvx8WyAluzy-34\" vertex=\"1\" connectable=\"0\">\n          <mxGeometry x=\"0.7732\" y=\"-1\" relative=\"1\" as=\"geometry\">\n            <mxPoint as=\"offset\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-9\" value=\"Sessions\" style=\"swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=none;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"460\" y=\"210\" width=\"140\" height=\"156\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-10\" value=\"+ user: type\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-9\" vertex=\"1\">\n          <mxGeometry y=\"26\" width=\"140\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"7reBWwmH2FoituciZp9q-7\" value=\"+ startTime: dateTime\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-9\" vertex=\"1\">\n          <mxGeometry y=\"52\" width=\"140\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"7reBWwmH2FoituciZp9q-8\" value=\"+ endTime: dateTime\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-9\" vertex=\"1\">\n          <mxGeometry y=\"78\" width=\"140\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"7reBWwmH2FoituciZp9q-9\" value=\"+ lab: relation\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-9\" vertex=\"1\">\n          <mxGeometry y=\"104\" width=\"140\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"7reBWwmH2FoituciZp9q-11\" value=\"+ clusterRunning: bool\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-9\" vertex=\"1\">\n          <mxGeometry y=\"130\" width=\"140\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-13\" value=\"Kubelab Agent\" style=\"html=1;dropTarget=0;whiteSpace=wrap;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"470\" y=\"530\" width=\"130\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-14\" value=\"\" style=\"shape=module;jettyWidth=8;jettyHeight=4;\" parent=\"UOWZPONmYOvx8WyAluzy-13\" vertex=\"1\">\n          <mxGeometry x=\"1\" width=\"20\" height=\"20\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"-27\" y=\"7\" as=\"offset\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-15\" value=\"vCluster\" style=\"html=1;dropTarget=0;whiteSpace=wrap;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"310\" y=\"400\" width=\"120\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-16\" value=\"\" style=\"shape=module;jettyWidth=8;jettyHeight=4;\" parent=\"UOWZPONmYOvx8WyAluzy-15\" vertex=\"1\">\n          <mxGeometry x=\"1\" width=\"20\" height=\"20\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"-27\" y=\"7\" as=\"offset\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-26\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;\" parent=\"1\" source=\"UOWZPONmYOvx8WyAluzy-17\" target=\"UOWZPONmYOvx8WyAluzy-21\" edge=\"1\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-27\" value=\"n\" style=\"edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\" parent=\"UOWZPONmYOvx8WyAluzy-26\" vertex=\"1\" connectable=\"0\">\n          <mxGeometry x=\"0.4171\" relative=\"1\" as=\"geometry\">\n            <mxPoint as=\"offset\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-28\" value=\"1\" style=\"edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\" parent=\"UOWZPONmYOvx8WyAluzy-26\" vertex=\"1\" connectable=\"0\">\n          <mxGeometry x=\"-0.6667\" y=\"-1\" relative=\"1\" as=\"geometry\">\n            <mxPoint as=\"offset\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-17\" value=\"Labs\" style=\"swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=none;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"630\" y=\"140\" width=\"140\" height=\"130\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-18\" value=\"+ title: text\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-17\" vertex=\"1\">\n          <mxGeometry y=\"26\" width=\"140\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"7reBWwmH2FoituciZp9q-14\" value=\"+ description: text\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-17\" vertex=\"1\">\n          <mxGeometry y=\"52\" width=\"140\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"7reBWwmH2FoituciZp9q-15\" value=\"+ docs: url\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-17\" vertex=\"1\">\n          <mxGeometry y=\"78\" width=\"140\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"7reBWwmH2FoituciZp9q-16\" value=\"+ exercises: relation\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-17\" vertex=\"1\">\n          <mxGeometry y=\"104\" width=\"140\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-21\" value=\"Exercises\" style=\"swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=none;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"630\" y=\"352\" width=\"140\" height=\"208\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-24\" value=\"+ title: text\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-21\" vertex=\"1\">\n          <mxGeometry y=\"26\" width=\"140\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"7reBWwmH2FoituciZp9q-13\" value=\"+ description: text\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-21\" vertex=\"1\">\n          <mxGeometry y=\"52\" width=\"140\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"7reBWwmH2FoituciZp9q-12\" value=\"+ docs: url\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-21\" vertex=\"1\">\n          <mxGeometry y=\"78\" width=\"140\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-41\" value=\"+ hint: url\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-21\" vertex=\"1\">\n          <mxGeometry y=\"104\" width=\"140\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-42\" value=\"+ solution: url\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-21\" vertex=\"1\">\n          <mxGeometry y=\"130\" width=\"140\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-43\" value=\"+ check: url\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-21\" vertex=\"1\">\n          <mxGeometry y=\"156\" width=\"140\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-44\" value=\"+ bootstrap: url\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-21\" vertex=\"1\">\n          <mxGeometry y=\"182\" width=\"140\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-45\" value=\"\" style=\"strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.containers.browserWindow;rSize=0;strokeColor=#666666;strokeColor2=#008cff;strokeColor3=#c4c4c4;mainText=,;recursiveResize=0;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"220\" y=\"710\" width=\"550\" height=\"380\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-46\" value=\"Page 1\" style=\"strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.containers.anchor;fontSize=17;fontColor=#666666;align=left;whiteSpace=wrap;\" parent=\"UOWZPONmYOvx8WyAluzy-45\" vertex=\"1\">\n          <mxGeometry x=\"60\" y=\"12\" width=\"110\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-47\" value=\"https://www.draw.io\" style=\"strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.containers.anchor;rSize=0;fontSize=17;fontColor=#666666;align=left;\" parent=\"UOWZPONmYOvx8WyAluzy-45\" vertex=\"1\">\n          <mxGeometry x=\"130\" y=\"60\" width=\"250\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-48\" value=\"Lab1\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-45\" vertex=\"1\">\n          <mxGeometry x=\"50\" y=\"120\" width=\"80\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-49\" value=\"Lab2\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-45\" vertex=\"1\">\n          <mxGeometry x=\"50\" y=\"150\" width=\"80\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-50\" value=\"Lab3\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-45\" vertex=\"1\">\n          <mxGeometry x=\"50\" y=\"180\" width=\"80\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-51\" value=\"start\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-45\" vertex=\"1\">\n          <mxGeometry x=\"350\" y=\"120\" width=\"40\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-52\" value=\"stop\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-45\" vertex=\"1\">\n          <mxGeometry x=\"350\" y=\"150\" width=\"40\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-53\" value=\"console\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-45\" vertex=\"1\">\n          <mxGeometry x=\"300\" y=\"150\" width=\"50\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-54\" value=\"1/3 Session\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-45\" vertex=\"1\">\n          <mxGeometry x=\"460\" y=\"110\" width=\"80\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-55\" value=\"start\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-45\" vertex=\"1\">\n          <mxGeometry x=\"350\" y=\"175\" width=\"40\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"UOWZPONmYOvx8WyAluzy-56\" value=\"8/10 done\" style=\"text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;\" parent=\"UOWZPONmYOvx8WyAluzy-45\" vertex=\"1\">\n          <mxGeometry x=\"100\" y=\"150\" width=\"70\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"cBvfc4a4BUQHZ_fMePWL-1\" value=\"\" style=\"strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.containers.browserWindow;rSize=0;strokeColor=#666666;strokeColor2=#008cff;strokeColor3=#c4c4c4;mainText=,;recursiveResize=0;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"150\" y=\"1200\" width=\"770\" height=\"620\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"cBvfc4a4BUQHZ_fMePWL-2\" value=\"Page 1\" style=\"strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.containers.anchor;fontSize=17;fontColor=#666666;align=left;whiteSpace=wrap;\" vertex=\"1\" parent=\"cBvfc4a4BUQHZ_fMePWL-1\">\n          <mxGeometry x=\"60\" y=\"12\" width=\"110\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"cBvfc4a4BUQHZ_fMePWL-3\" value=\"https://www.draw.io\" style=\"strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.containers.anchor;rSize=0;fontSize=17;fontColor=#666666;align=left;\" vertex=\"1\" parent=\"cBvfc4a4BUQHZ_fMePWL-1\">\n          <mxGeometry x=\"130\" y=\"60\" width=\"250\" height=\"26\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"cBvfc4a4BUQHZ_fMePWL-5\" value=\"CLI\" style=\"rounded=0;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"cBvfc4a4BUQHZ_fMePWL-1\">\n          <mxGeometry y=\"140\" width=\"390\" height=\"450\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"cBvfc4a4BUQHZ_fMePWL-10\" value=\"Docs\" style=\"rounded=0;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"cBvfc4a4BUQHZ_fMePWL-1\">\n          <mxGeometry x=\"390\" y=\"140\" width=\"380\" height=\"320\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"cBvfc4a4BUQHZ_fMePWL-11\" value=\"Hints / Solution\" style=\"rounded=0;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"cBvfc4a4BUQHZ_fMePWL-1\">\n          <mxGeometry x=\"390\" y=\"460\" width=\"380\" height=\"130\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"cBvfc4a4BUQHZ_fMePWL-12\" value=\"Check\" style=\"strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;\" vertex=\"1\" parent=\"cBvfc4a4BUQHZ_fMePWL-1\">\n          <mxGeometry x=\"660\" y=\"600\" width=\"100\" height=\"20\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"cBvfc4a4BUQHZ_fMePWL-13\" value=\"Reveal Hint / Solution\" style=\"strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;\" vertex=\"1\" parent=\"cBvfc4a4BUQHZ_fMePWL-1\">\n          <mxGeometry x=\"490\" y=\"480\" width=\"180\" height=\"20\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"cBvfc4a4BUQHZ_fMePWL-14\" value=\"\" style=\"ellipse;whiteSpace=wrap;html=1;aspect=fixed;\" vertex=\"1\" parent=\"cBvfc4a4BUQHZ_fMePWL-1\">\n          <mxGeometry x=\"10\" y=\"600\" width=\"20\" height=\"20\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"cBvfc4a4BUQHZ_fMePWL-15\" value=\"\" style=\"ellipse;whiteSpace=wrap;html=1;aspect=fixed;\" vertex=\"1\" parent=\"cBvfc4a4BUQHZ_fMePWL-1\">\n          <mxGeometry x=\"60\" y=\"600\" width=\"20\" height=\"20\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"cBvfc4a4BUQHZ_fMePWL-16\" value=\"\" style=\"ellipse;whiteSpace=wrap;html=1;aspect=fixed;\" vertex=\"1\" parent=\"cBvfc4a4BUQHZ_fMePWL-1\">\n          <mxGeometry x=\"110\" y=\"600\" width=\"20\" height=\"20\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"cBvfc4a4BUQHZ_fMePWL-17\" value=\"\" style=\"ellipse;whiteSpace=wrap;html=1;aspect=fixed;\" vertex=\"1\" parent=\"cBvfc4a4BUQHZ_fMePWL-1\">\n          <mxGeometry x=\"160\" y=\"600\" width=\"20\" height=\"20\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"cBvfc4a4BUQHZ_fMePWL-18\" value=\"Start Exercise\" style=\"strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.buttons.button;strokeColor=#666666;fontColor=#ffffff;mainText=;buttonStyle=round;fontSize=17;fontStyle=1;fillColor=#008cff;whiteSpace=wrap;\" vertex=\"1\" parent=\"cBvfc4a4BUQHZ_fMePWL-1\">\n          <mxGeometry x=\"120\" y=\"250\" width=\"150\" height=\"40\" as=\"geometry\" />\n        </mxCell>\n      </root>\n    </mxGraphModel>\n  </diagram>\n</mxfile>\n"
  },
  {
    "path": "deploy/clusterrole.yaml",
    "content": "apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n  name: cluster-admin\nrules:\n- apiGroups: [\"*\"]\n  resources: [\"*\"]\n  verbs: [\"*\"]\n"
  },
  {
    "path": "deploy/clusterrolebinding.yaml",
    "content": "apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n  name: kubelab-admin\nsubjects:\n- kind: ServiceAccount\n  name: kubelab-admin\n  namespace: kubelab\nroleRef:\n  kind: ClusterRole\n  name: cluster-admin\n  apiGroup: rbac.authorization.k8s.io\n"
  },
  {
    "path": "deploy/configmap.yaml",
    "content": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: kubelab-config\ndata:\n  KUBELAB_AGENT_IMAGE: ghcr.io/natrontech/kubelab-agent:latest\n  ALLOWED_HOSTS: kubelab.natr-demo.k8s.natron.cloud\n  PODS_LIMIT: \"70\"\n  STORAGE_LIMIT: \"50Gi\"\n  AGENT_INGRESS_CLASS: nginx-external\n"
  },
  {
    "path": "deploy/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: kubelab-ingress\n  namespace: kubelab\n  annotations:\n    # cert-manager.io/cluster-issuer: letsencrypt-prod-natron-cloud\n    cert-manager.io/private-key-rotation-policy: Always\n    ingress.kubernetes.io/force-ssl-redirect: \"true\"\nspec:\n  ingressClassName: nginx-external\n  rules:\n  - host: kubelab.natr-demo.k8s.natron.cloud\n    http:\n      paths:\n      - pathType: Prefix\n        path: \"/\"\n        backend:\n          service:\n            name: kubelab-service\n            port:\n              number: 8090\n  # tls:\n  # - hosts:\n  #   - kubelab.natr-demo.k8s.natron.cloud\n  #   secretName: kubelab-tls\n"
  },
  {
    "path": "deploy/kustomization.yaml",
    "content": "apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nnamespace: kubelab\nresources:\n- ns.yaml\n- service.yaml\n- statefulset.yaml\n- ingress.yaml\n- serviceaccount.yaml\n- clusterrole.yaml\n- clusterrolebinding.yaml\n- configmap.yaml\n"
  },
  {
    "path": "deploy/ns.yaml",
    "content": "apiVersion: v1\nkind: Namespace\nmetadata:\n  name: kubelab\n"
  },
  {
    "path": "deploy/persistentvolume.yaml",
    "content": "apiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: hostpath-pv\n  namespace: kubelab\n  labels:\n    type: local\nspec:\n  storageClassName: manual\n  capacity:\n    storage: 10Gi # Adjust size as needed\n  accessModes:\n    - ReadWriteOnce\n  hostPath:\n    path: \"/data/kubelab\" # Directory on the host\n"
  },
  {
    "path": "deploy/service.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: kubelab-service\n  namespace: kubelab\nspec:\n  selector:\n    app: kubelab\n  ports:\n    - protocol: TCP\n      port: 8090\n"
  },
  {
    "path": "deploy/serviceaccount.yaml",
    "content": "apiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: kubelab-admin\n  namespace: kubelab\n"
  },
  {
    "path": "deploy/statefulset.yaml",
    "content": "apiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n  name: kubelab-statefulset\n  namespace: kubelab\nspec:\n  serviceName: \"kubelab-service\"\n  replicas: 1\n  selector:\n    matchLabels:\n      app: kubelab\n  template:\n    metadata:\n      labels:\n        app: kubelab\n    spec:\n      serviceAccountName: kubelab-admin\n      containers:\n      - name: kubelab\n        image: ghcr.io/natrontech/kubelab:latest\n        # image: kubelab:local\n        ports:\n        - containerPort: 8090\n        volumeMounts:\n        - name: pb-data-hostpath\n          mountPath: /app/kubelab/pb_data\n        envFrom:\n        - configMapRef:\n            name: kubelab-config\n        resources:\n          requests:\n            memory: \"16Gi\"\n            cpu: \"4\"\n          limits:\n            memory: \"16Gi\"\n            cpu: \"8\"\n      nodeSelector:\n        hostpath-node: \"true\"\n      volumes:\n      - name: pb-data-hostpath\n        persistentVolumeClaim:\n          claimName: hostpath-pvc\n  volumeClaimTemplates:\n  - metadata:\n      name: pb-data-hostpath\n    spec:\n      accessModes: [ \"ReadWriteOnce\" ]\n      resources:\n        requests:\n          storage: 10Gi\n      storageClassName: manual\n      volumeMode: Filesystem\n      volumeName: hostpath-pv\n"
  },
  {
    "path": "docker-compose.yaml",
    "content": "version: '3.5'\n\nservices:\n  kubelab:\n    build: .\n    # image: ghcr.io/natrontech/kubelab:latest\n    ports:\n      - \"8090:8090\"\n    volumes:\n      # * This is the path to the data folder on your local machine\n      - $PWD/kubelab-backend/pb_data:/app/kubelab/pb_data\n"
  },
  {
    "path": "kubelab-backend/.gitignore",
    "content": "/.cache\n/pocketbase\n/pocketbase*.zip\n/pb_data\n/pb_data_old\n/tmp\n/bin\n./pocketbase\nkubelab\n"
  },
  {
    "path": "kubelab-backend/README.md",
    "content": "# KubeLab Backend\n\nThere are two flavors of the backend:\n\n1. standard release downloaded from https://github.com/pocketbase/pocketbase/releases. This one is a good start, but most real-world applications would require more (see next).\n2. custom compiled (`go build`), possibly with my customizations and perhaps yours too.\n\nOut of the box, the project assumes #2 (custom compiled with my customizations).\n\n## standard (official) release of pocketbase\n\nDownload from release archive from https://github.com/pocketbase/pocketbase/releases/latest, unzip it and place the `pocketbase` binary in this folder, and you're done.\n\n## custom build\n\nIf you would like to extend PocketBase and use it as a framework then there is a `main.go` in this folder that you can customize and build using `go build` or do live development using `modd`.\n\nSee https://pocketbase.io/docs/use-as-framework/ for details.\n\n# Setup\n\n## Architecture\n\n> **Note:** For optimal set up, ensure you are using a standard distribution of Linux. For other operating systems, you may run into issues, or need additional configuration.\n> A docker-compose setup is included with the project, which can be used on any OS.\n\n## Build\n\nAssuming you have Go language tools installed ...\n\n`go build`\n\nIf you don't have Go and don't want to install it, you can use docker-compose setup. Otherwise, your only choice is to download the binary from https://github.com/pocketbase/pocketbase/releases/latest, and placing it in this folder. But then you will not be able to use any of the custom code (such as \"config-driven hooks\")\n\n## Run migrations\n\nBefore you can run the actual backend, you must run the migrations using `./pocketbase migrate up` in the current directory. It will create appropriate schema tables/collections.\n\n## Run the backend\n\nYou can run the PocketBase backend direct with `./pocketbase serve` or using `npm run backend` in the `sk` directory. Note that if you want the backend to also serve the frontend assets, then you must add the `--publicDir ../frontend/build` option.\n\n## Docker\n\nA highly recommended option is to run it inside a Docker container. A `Dockerfile` is included that builds a production Docker image. Also, a `docker-compose.yml` along with an _override_ file example are included, which should be used during development.\n\n## Active development with `modd`\n\nFinally, if you are going to actively develop using Go using PocketBase as a framework, then you probably want to use [modd](https://github.com/cortesi/modd), a development tool that rebuilds and restarts your Go binary everytime a source file changes (live reload on change). An basic `modd.conf` config file is included in this setup. You can run it by installing `modd` (`go install github.com/cortesi/modd/cmd/modd@latest`) and then running `modd`. All this is done automatically for you if you are using Docker.\n\n# Schema (Collections)\n\nWith the 0.9 version of PocketBase, JavaScript auto-migrations as implemented. The JS files in `pb_migrations` can create/drop/modify collections and data. These are executed automatically by PocketBase on startup.\n\nNot only that, they are also generated automatically whenever you change the schema! So go ahead and make changes to the schema and watch new JS files generated in the `pb_migrations` folder. Just remember to commit them to version control.\n\n## Generated Types\n\nThe file `generated-types.ts` contains TypeScript definitions of `Record` types mirroring the fields in your database collections. But it needs to be regenerated every time you modify the schema. This can be done by simply running the `typegen` script in the frontend's `package.json`. So remember to do that.\n\n# Hooks\n\nPocketBase provides API's like .OnModelBefore* and .OnModelAfter* to run\ncallbacks when records change. This app builds on top of that by providing\na \"hooks\" table that drives those hooks using configuration. It has the\nfollowing fields:\n\n- collection: name of the collection that triggers an action\n- event: insert/update/delete event that triggers the action\n- action_type: \"command\" if you want to run a program/script or \"post\" if\n  you want to POST to a webhook endpoint. The record will be marshaled to\n  JSON and passed to the command as STDIN or to the webhook POST as\n  request body (with header 'content-type: application/json')\n- action: path to the command/script or URL of the webhook to POST to\n- action_params: a string that will be passed as argument to the action\n\nSo now by configuring the above table, you can execute external commands/scripts\nand POST data to external webhooks in reaction to insert/update/delete of\nrecords.\n\nMost web services these days provide webhook endpoints (e.g. sendgrid, mailchimp, stripe, etc) which you can POST directly to. But if you need special\nprocessing then you can write a script that receives changed data as JSON, parses and manipulates it using [`jq`](https://github.com/stedolan/jq) before\nsending it on its way.\n\nSee `example-hook-script.sh` for a demonstration.\n\nPossible use cases:\n\n- Clone git repo when a record is inserted to \"repositories\" table\n- Execute a terraform script when a new cluster is inserted to \"clusters\" table\n- Send an acknowledgement email when a \"contact\" form table is inserted to.\n- Charge a credit card when payment_token table is inserted to and then\n  send email upon success/failure\n- Recalculate inventory levels as \"orders\" table is inserted to, and then\n  send notifications when inventory becomes low.\n"
  },
  {
    "path": "kubelab-backend/entrypoint.sh",
    "content": "#!/bin/sh\nset -e # exit on any non-zero status (error)\n\n# this entrypoint script checks that all required setup is done\n# if not done, does it\n# and then proceeds to execute the main \"command\" for this container\n\n# build if needed\ngo mod tidy\ngo build\n\nif [ ! -x \"$(which modd)\" ]; then\n  echo \"go install modd\"\n  go install github.com/cortesi/modd/cmd/modd@latest\nfi\n\nexec \"$@\"\n"
  },
  {
    "path": "kubelab-backend/example-hook-script.sh",
    "content": "#!/usr/bin/env bash\n\n# This example script, along with hooks.go, shows how to trigger a command\n# when a record changes in PocketBase and how to feed the changed record to this\n# script.\n\nparams=$1 # `action_params` field passed from the \"hooks\" table\necho \"PARAMS=$params\"\n\n# The body of the record (as JSON) is fed to this script as stdin.\n# The following just reformats it and pretty-prints it.\ncat | jq -C\n"
  },
  {
    "path": "kubelab-backend/go.mod",
    "content": "module github.com/natrontech/kubelab\n\ngo 1.22\ntoolchain go1.22.5\n\nrequire (\n\tgithub.com/caarlos0/env/v8 v8.0.0\n\tgithub.com/pocketbase/dbx v1.10.1\n\tgithub.com/pocketbase/pocketbase v0.22.18\n\thelm.sh/helm/v3 v3.15.4\n\tk8s.io/api v0.30.3\n\tk8s.io/apimachinery v0.30.3\n\tk8s.io/utils v0.0.0-20240102154912-e7106e64919e\n)\n\nrequire (\n\tgithub.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect\n\tgithub.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect\n\tgithub.com/BurntSushi/toml v1.3.2 // indirect\n\tgithub.com/MakeNowJust/heredoc v1.0.0 // indirect\n\tgithub.com/Masterminds/goutils v1.1.1 // indirect\n\tgithub.com/Masterminds/semver/v3 v3.2.1 // indirect\n\tgithub.com/Masterminds/sprig/v3 v3.2.3 // indirect\n\tgithub.com/Masterminds/squirrel v1.5.4 // indirect\n\tgithub.com/Microsoft/hcsshim v0.11.4 // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/chai2010/gettext-go v1.0.2 // indirect\n\tgithub.com/containerd/containerd v1.7.12 // indirect\n\tgithub.com/containerd/log v0.1.0 // indirect\n\tgithub.com/cyphar/filepath-securejoin v0.2.4 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/distribution/reference v0.5.0 // indirect\n\tgithub.com/docker/cli v25.0.1+incompatible // indirect\n\tgithub.com/docker/distribution v2.8.3+incompatible // indirect\n\tgithub.com/docker/docker v25.0.6+incompatible // indirect\n\tgithub.com/docker/docker-credential-helpers v0.8.0 // indirect\n\tgithub.com/docker/go-connections v0.5.0 // indirect\n\tgithub.com/docker/go-metrics v0.0.1 // indirect\n\tgithub.com/emicklei/go-restful/v3 v3.11.1 // indirect\n\tgithub.com/evanphx/json-patch v5.7.0+incompatible // indirect\n\tgithub.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fsnotify/fsnotify v1.7.0 // indirect\n\tgithub.com/go-errors/errors v1.5.1 // indirect\n\tgithub.com/go-gorp/gorp/v3 v3.1.0 // indirect\n\tgithub.com/go-logr/logr v1.4.2 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.20.2 // indirect\n\tgithub.com/go-openapi/jsonreference v0.20.4 // indirect\n\tgithub.com/go-openapi/swag v0.22.7 // indirect\n\tgithub.com/gobwas/glob v0.2.3 // indirect\n\tgithub.com/goccy/go-json v0.10.3 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/google/btree v1.1.2 // indirect\n\tgithub.com/google/gnostic-models v0.6.8 // indirect\n\tgithub.com/google/go-cmp v0.6.0 // indirect\n\tgithub.com/google/gofuzz v1.2.0 // indirect\n\tgithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect\n\tgithub.com/gorilla/mux v1.8.1 // indirect\n\tgithub.com/gorilla/websocket v1.5.1 // indirect\n\tgithub.com/gosuri/uitable v0.0.4 // indirect\n\tgithub.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/hashicorp/golang-lru/v2 v2.0.7 // indirect\n\tgithub.com/huandu/xstrings v1.4.0 // indirect\n\tgithub.com/imdario/mergo v0.3.16 // indirect\n\tgithub.com/jmoiron/sqlx v1.3.5 // indirect\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/compress v1.17.4 // indirect\n\tgithub.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 // indirect\n\tgithub.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect\n\tgithub.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect\n\tgithub.com/lib/pq v1.10.9 // indirect\n\tgithub.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect\n\tgithub.com/mailru/easyjson v0.7.7 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.15 // indirect\n\tgithub.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect\n\tgithub.com/mitchellh/copystructure v1.2.0 // indirect\n\tgithub.com/mitchellh/go-wordwrap v1.0.1 // indirect\n\tgithub.com/mitchellh/reflectwalk v1.0.2 // indirect\n\tgithub.com/moby/locker v1.0.1 // indirect\n\tgithub.com/moby/spdystream v0.2.0 // indirect\n\tgithub.com/moby/term v0.5.0 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect\n\tgithub.com/ncruces/go-strftime v0.1.9 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.0-rc6 // indirect\n\tgithub.com/peterbourgon/diskv v2.0.1+incompatible // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/prometheus/client_golang v1.18.0 // indirect\n\tgithub.com/prometheus/client_model v0.5.0 // indirect\n\tgithub.com/prometheus/common v0.45.0 // indirect\n\tgithub.com/prometheus/procfs v0.12.0 // indirect\n\tgithub.com/rivo/uniseg v0.4.4 // indirect\n\tgithub.com/rubenv/sql-migrate v1.6.0 // indirect\n\tgithub.com/russross/blackfriday/v2 v2.1.0 // indirect\n\tgithub.com/shopspring/decimal v1.3.1 // indirect\n\tgithub.com/sirupsen/logrus v1.9.3 // indirect\n\tgithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect\n\tgithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect\n\tgithub.com/xeipuuv/gojsonschema v1.2.0 // indirect\n\tgithub.com/xlab/treeprint v1.2.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect\n\tgo.opentelemetry.io/otel v1.24.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.24.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.24.0 // indirect\n\tgo.starlark.net v0.0.0-20231121155337-90ade8b19d09 // indirect\n\tgolang.org/x/sync v0.7.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a // indirect\n\tgopkg.in/evanphx/json-patch.v5 v5.7.0 // indirect\n\tgopkg.in/inf.v0 v0.9.1 // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tk8s.io/apiextensions-apiserver v0.30.3 // indirect\n\tk8s.io/apiserver v0.30.3 // indirect\n\tk8s.io/cli-runtime v0.30.3 // indirect\n\tk8s.io/component-base v0.30.3 // indirect\n\tk8s.io/klog/v2 v2.120.1 // indirect\n\tk8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect\n\tk8s.io/kubectl v0.30.3 // indirect\n\tmodernc.org/gc/v3 v3.0.0-20240722195230-4a140ff9c08e // indirect\n\toras.land/oras-go v1.2.5 // indirect\n\tsigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect\n\tsigs.k8s.io/kustomize/api v0.16.0 // indirect\n\tsigs.k8s.io/kustomize/kyaml v0.16.0 // indirect\n\tsigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect\n\tsigs.k8s.io/yaml v1.4.0 // indirect\n)\n\nrequire (\n\tgithub.com/AlecAivazis/survey/v2 v2.3.7 // indirect\n\tgithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect\n\tgithub.com/aws/aws-sdk-go-v2 v1.30.3 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.8 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/s3 v1.58.2 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect\n\tgithub.com/aws/smithy-go v1.20.3 // indirect\n\tgithub.com/disintegration/imaging v1.6.2 // indirect\n\tgithub.com/dlclark/regexp2 v1.11.0 // indirect\n\tgithub.com/domodwyer/mailyak/v3 v3.6.2 // indirect\n\tgithub.com/dop251/goja v0.0.0-20240627195025-eb1f15ee67d2 // indirect\n\tgithub.com/dop251/goja_nodejs v0.0.0-20240418154818-2aae10d4cbcf // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/fatih/color v1.17.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.4 // indirect\n\tgithub.com/ganigeorgiev/fexpr v0.4.1 // indirect\n\tgithub.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect\n\tgithub.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect\n\tgithub.com/golang-jwt/jwt/v4 v4.5.0 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/google/pprof v0.0.0-20240625030939-27f56978b8b0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.13.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-sqlite3 v1.14.22 // indirect\n\tgithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect\n\tgithub.com/mittwald/go-helm-client v0.12.13\n\tgithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect\n\tgithub.com/spf13/cast v1.6.0 // indirect\n\tgithub.com/spf13/cobra v1.8.1 // indirect\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasttemplate v1.2.2 // indirect\n\tgo.opencensus.io v0.24.0 // indirect\n\tgocloud.dev v0.37.0 // indirect\n\tgolang.org/x/crypto v0.25.0 // indirect\n\tgolang.org/x/image v0.18.0 // indirect\n\tgolang.org/x/net v0.27.0 // indirect\n\tgolang.org/x/oauth2 v0.21.0 // indirect\n\tgolang.org/x/sys v0.22.0 // indirect\n\tgolang.org/x/term v0.22.0 // indirect\n\tgolang.org/x/text v0.16.0 // indirect\n\tgolang.org/x/time v0.5.0 // indirect\n\tgolang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect\n\tgoogle.golang.org/api v0.189.0 // indirect\n\tgoogle.golang.org/grpc v1.65.0 // indirect\n\tgoogle.golang.org/protobuf v1.34.2 // indirect\n\tk8s.io/client-go v0.30.3\n\tmodernc.org/libc v1.55.3 // indirect\n\tmodernc.org/mathutil v1.6.0 // indirect\n\tmodernc.org/memory v1.8.0 // indirect\n\tmodernc.org/sqlite v1.31.1 // indirect\n\tmodernc.org/strutil v1.2.0 // indirect\n\tmodernc.org/token v1.1.0 // indirect\n)\n"
  },
  {
    "path": "kubelab-backend/go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=\ncloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=\ncloud.google.com/go/auth v0.7.2 h1:uiha352VrCDMXg+yoBtaD0tUF4Kv9vrtrWPYXwutnDE=\ncloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs=\ncloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=\ncloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=\ncloud.google.com/go/compute v1.25.0 h1:H1/4SqSUhjPFE7L5ddzHOfY2bCAvjwNRZPNl6Ni5oYU=\ncloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=\ncloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=\ncloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc=\ncloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI=\ncloud.google.com/go/storage v1.39.1 h1:MvraqHKhogCOTXTlct/9C3K3+Uy2jBmFYb3/Sp6dVtY=\ncloud.google.com/go/storage v1.39.1/go.mod h1:xK6xZmxZmo+fyP7+DEF6FhNc24/JAe95OLyOHCXFH1o=\nfilippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=\ngithub.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=\ngithub.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=\ngithub.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=\ngithub.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=\ngithub.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=\ngithub.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=\ngithub.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=\ngithub.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=\ngithub.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=\ngithub.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=\ngithub.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=\ngithub.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=\ngithub.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=\ngithub.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=\ngithub.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=\ngithub.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=\ngithub.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=\ngithub.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=\ngithub.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=\ngithub.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=\ngithub.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=\ngithub.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w=\ngithub.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=\ngithub.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=\ngithub.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=\ngithub.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=\ngithub.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=\ngithub.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=\ngithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=\ngithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=\ngithub.com/aws/aws-sdk-go v1.51.11 h1:El5VypsMIz7sFwAAj/j06JX9UGs4KAbAIEaZ57bNY4s=\ngithub.com/aws/aws-sdk-go v1.51.11/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=\ngithub.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=\ngithub.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=\ngithub.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=\ngithub.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=\ngithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.8 h1:u1KOU1S15ufyZqmH/rA3POkiRH6EcDANHj2xHRzq+zc=\ngithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.8/go.mod h1:WPv2FRnkIOoDv/8j2gSUsI4qDc7392w5anFB/I89GZ8=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.58.2 h1:sZXIzO38GZOU+O0C+INqbH7C2yALwfMWpd64tONS/NE=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.58.2/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=\ngithub.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=\ngithub.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=\ngithub.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=\ngithub.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng=\ngithub.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=\ngithub.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ=\ngithub.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=\ngithub.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o=\ngithub.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=\ngithub.com/caarlos0/env/v8 v8.0.0 h1:POhxHhSpuxrLMIdvTGARuZqR4Jjm8AYmoi/JKlcScs0=\ngithub.com/caarlos0/env/v8 v8.0.0/go.mod h1:7K4wMY9bH0esiXSSHlfHLX5xKGQMnkH5Fk4TDSSSzfo=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=\ngithub.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=\ngithub.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=\ngithub.com/containerd/containerd v1.7.12 h1:+KQsnv4VnzyxWcfO9mlxxELaoztsDEjOuCMPAuPqgU0=\ngithub.com/containerd/containerd v1.7.12/go.mod h1:/5OMpE1p0ylxtEUGY8kuCYkDRzJm9NO1TFMWjUpdevk=\ngithub.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM=\ngithub.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=\ngithub.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=\ngithub.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=\ngithub.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=\ngithub.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=\ngithub.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=\ngithub.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=\ngithub.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=\ngithub.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aBfCb7iqHmDEIp6fBvC/hQUddQfg+3qdYjwzaiP9Hnc=\ngithub.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI=\ngithub.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=\ngithub.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=\ngithub.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/docker/cli v25.0.1+incompatible h1:mFpqnrS6Hsm3v1k7Wa/BO23oz0k121MTbTO1lpcGSkU=\ngithub.com/docker/cli v25.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=\ngithub.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=\ngithub.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=\ngithub.com/docker/docker v25.0.6+incompatible h1:5cPwbwriIcsua2REJe8HqQV+6WlWc1byg2QSXzBxBGg=\ngithub.com/docker/docker v25.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8=\ngithub.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40=\ngithub.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=\ngithub.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=\ngithub.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=\ngithub.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=\ngithub.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=\ngithub.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=\ngithub.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4=\ngithub.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=\ngithub.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=\ngithub.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=\ngithub.com/dop251/goja v0.0.0-20240627195025-eb1f15ee67d2 h1:4Ew88p5s9dwIk5/woUyqI9BD89NgZoUNH4/rM/h2UDg=\ngithub.com/dop251/goja v0.0.0-20240627195025-eb1f15ee67d2/go.mod h1:o31y53rb/qiIAONF7w3FHJZRqqP3fzHUr1HqanthByw=\ngithub.com/dop251/goja_nodejs v0.0.0-20240418154818-2aae10d4cbcf h1:2JoVYP9iko8uuIW33BQafzaylDixXbdXCRw/vCoxL+s=\ngithub.com/dop251/goja_nodejs v0.0.0-20240418154818-2aae10d4cbcf/go.mod h1:bhGPmCgCCTSRfiMYWjpS46IDo9EUZXlsuUaPXSWGbv0=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/emicklei/go-restful/v3 v3.11.1 h1:S+9bSbua1z3FgCnV0KKOSSZ3mDthb5NyEPL5gEpCvyk=\ngithub.com/emicklei/go-restful/v3 v3.11.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI=\ngithub.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=\ngithub.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=\ngithub.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=\ngithub.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=\ngithub.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI=\ngithub.com/foxcpp/go-mockdns v1.0.0/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=\ngithub.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=\ngithub.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=\ngithub.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=\ngithub.com/ganigeorgiev/fexpr v0.4.1 h1:hpUgbUEEWIZhSDBtf4M9aUNfQQ0BZkGRaMePy7Gcx5k=\ngithub.com/ganigeorgiev/fexpr v0.4.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=\ngithub.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=\ngithub.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=\ngithub.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=\ngithub.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=\ngithub.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=\ngithub.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=\ngithub.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=\ngithub.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=\ngithub.com/go-openapi/swag v0.22.7 h1:JWrc1uc/P9cSomxfnsFSVWoE1FW6bNbrVPmpQYpCcR8=\ngithub.com/go-openapi/swag v0.22.7/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0=\ngithub.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=\ngithub.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=\ngithub.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=\ngithub.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=\ngithub.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=\ngithub.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=\ngithub.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4=\ngithub.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=\ngithub.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=\ngithub.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=\ngithub.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=\ngithub.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=\ngithub.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=\ngithub.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k=\ngithub.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=\ngithub.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=\ngithub.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=\ngithub.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=\ngithub.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=\ngithub.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/pprof v0.0.0-20240625030939-27f56978b8b0 h1:e+8XbKB6IMn8A4OAyZccO4pYfB3s7bt6azNIPE7AnPg=\ngithub.com/google/pprof v0.0.0-20240625030939-27f56978b8b0/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=\ngithub.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=\ngithub.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=\ngithub.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=\ngithub.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=\ngithub.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=\ngithub.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=\ngithub.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=\ngithub.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=\ngithub.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=\ngithub.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=\ngithub.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=\ngithub.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=\ngithub.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=\ngithub.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=\ngithub.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=\ngithub.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=\ngithub.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=\ngithub.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=\ngithub.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=\ngithub.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=\ngithub.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=\ngithub.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=\ngithub.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=\ngithub.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=\ngithub.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=\ngithub.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=\ngithub.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 h1:FwuzbVh87iLiUQj1+uQUsuw9x5t9m5n5g7rG7o4svW4=\ngithub.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61/go.mod h1:paQfF1YtHe+GrGg5fOgjsjoCX/UKDr9bc1DoWpZfns8=\ngithub.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=\ngithub.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=\ngithub.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=\ngithub.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=\ngithub.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=\ngithub.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=\ngithub.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=\ngithub.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=\ngithub.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=\ngithub.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=\ngithub.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=\ngithub.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/miekg/dns v1.1.25 h1:dFwPR6SfLtrSwgDcIq2bcU/gVutB4sNApq2HBdqcakg=\ngithub.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=\ngithub.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=\ngithub.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=\ngithub.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=\ngithub.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=\ngithub.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=\ngithub.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=\ngithub.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=\ngithub.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=\ngithub.com/mittwald/go-helm-client v0.12.13 h1:TzoHH3NmlUdgy4cbo2tAuGQTcXkUKdORhZSE/Cq72bA=\ngithub.com/mittwald/go-helm-client v0.12.13/go.mod h1:BMoJyfs5n2MTe1RmWjTHuRl7b5wXfe6l7Eik1ZaZ0JU=\ngithub.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=\ngithub.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=\ngithub.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=\ngithub.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=\ngithub.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=\ngithub.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=\ngithub.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=\ngithub.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=\ngithub.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=\ngithub.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=\ngithub.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=\ngithub.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=\ngithub.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY=\ngithub.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM=\ngithub.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE=\ngithub.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.0-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58yxX1Ov9HERHNqU=\ngithub.com/opencontainers/image-spec v1.1.0-rc6/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=\ngithub.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=\ngithub.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=\ngithub.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=\ngithub.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pocketbase/dbx v1.10.1 h1:cw+vsyfCJD8YObOVeqb93YErnlxwYMkNZ4rwN0G0AaA=\ngithub.com/pocketbase/dbx v1.10.1/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=\ngithub.com/pocketbase/pocketbase v0.22.18 h1:yVckUhi5GDORqCb0BbtlvRB1CVxHY9HO9btEaeZHVJU=\ngithub.com/pocketbase/pocketbase v0.22.18/go.mod h1:0QFvDOOW7ANId78ChZSagyHbmP6CgMxDQrQFXzeaDpA=\ngithub.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=\ngithub.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=\ngithub.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=\ngithub.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=\ngithub.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=\ngithub.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=\ngithub.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=\ngithub.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=\ngithub.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=\ngithub.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=\ngithub.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=\ngithub.com/rubenv/sql-migrate v1.6.0 h1:IZpcTlAx/VKXphWEpwWJ7BaMq05tYtE80zYz+8a5Il8=\ngithub.com/rubenv/sql-migrate v1.6.0/go.mod h1:m3ilnKP7sNb4eYkLsp6cGdPOl4OBcXM6rcbzU+Oqc5k=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=\ngithub.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=\ngithub.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=\ngithub.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=\ngithub.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=\ngithub.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=\ngithub.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=\ngithub.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=\ngithub.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=\ngithub.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=\ngithub.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=\ngithub.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=\ngithub.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=\ngithub.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=\ngithub.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI=\ngithub.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=\ngithub.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE=\ngithub.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=\ngithub.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY=\ngithub.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=\ngo.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=\ngo.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=\ngo.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=\ngo.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=\ngo.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=\ngo.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=\ngo.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=\ngo.starlark.net v0.0.0-20231121155337-90ade8b19d09 h1:hzy3LFnSN8kuQK8h9tHl4ndF6UruMj47OqwqsS+/Ai4=\ngo.starlark.net v0.0.0-20231121155337-90ade8b19d09/go.mod h1:LcLNIzVOMp4oV+uusnpk+VU+SzXaJakUuBjoCSWH5dM=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngocloud.dev v0.37.0 h1:XF1rN6R0qZI/9DYjN16Uy0durAmSlf58DHOcb28GPro=\ngocloud.dev v0.37.0/go.mod h1:7/O4kqdInCNsc6LqgmuFnS0GRew4XNNYWpA44yQnwco=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=\ngolang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=\ngolang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=\ngolang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=\ngolang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=\ngolang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=\ngolang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=\ngolang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=\ngolang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=\ngolang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=\ngolang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=\ngolang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=\ngolang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=\ngolang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=\ngolang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=\ngolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk=\ngolang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=\ngoogle.golang.org/api v0.189.0 h1:equMo30LypAkdkLMBqfeIqtyAnlyig1JSZArl4XPwdI=\ngoogle.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20240722135656-d784300faade h1:lKFsS7wpngDgSCeFn7MoLy+wBDQZ1UQIJD4UNM1Qvkg=\ngoogle.golang.org/genproto v0.0.0-20240722135656-d784300faade/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a h1:hqK4+jJZXCU4pW7jsAdGOVFIfLHQeV7LaizZKnZ84HI=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=\ngoogle.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=\ngoogle.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/evanphx/json-patch.v5 v5.7.0 h1:dGKGylPlZ/jus2g1YqhhyzfH0gPy2R8/MYUpW/OslTY=\ngopkg.in/evanphx/json-patch.v5 v5.7.0/go.mod h1:/kvTRh1TVm5wuM6OkHxqXtE/1nUZZpihg29RtuIyfvk=\ngopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=\ngopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=\ngotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=\nhelm.sh/helm/v3 v3.15.4 h1:UFHd6oZ1IN3FsUZ7XNhOQDyQ2QYknBNWRHH57e9cbHY=\nhelm.sh/helm/v3 v3.15.4/go.mod h1:phOwlxqGSgppCY/ysWBNRhG3MtnpsttOzxaTK+Mt40E=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nk8s.io/api v0.30.3 h1:ImHwK9DCsPA9uoU3rVh4QHAHHK5dTSv1nxJUapx8hoQ=\nk8s.io/api v0.30.3/go.mod h1:GPc8jlzoe5JG3pb0KJCSLX5oAFIW3/qNJITlDj8BH04=\nk8s.io/apiextensions-apiserver v0.30.3 h1:oChu5li2vsZHx2IvnGP3ah8Nj3KyqG3kRSaKmijhB9U=\nk8s.io/apiextensions-apiserver v0.30.3/go.mod h1:uhXxYDkMAvl6CJw4lrDN4CPbONkF3+XL9cacCT44kV4=\nk8s.io/apimachinery v0.30.3 h1:q1laaWCmrszyQuSQCfNB8cFgCuDAoPszKY4ucAjDwHc=\nk8s.io/apimachinery v0.30.3/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=\nk8s.io/apiserver v0.30.3 h1:QZJndA9k2MjFqpnyYv/PH+9PE0SHhx3hBho4X0vE65g=\nk8s.io/apiserver v0.30.3/go.mod h1:6Oa88y1CZqnzetd2JdepO0UXzQX4ZnOekx2/PtEjrOg=\nk8s.io/cli-runtime v0.30.3 h1:aG69oRzJuP2Q4o8dm+f5WJIX4ZBEwrvdID0+MXyUY6k=\nk8s.io/cli-runtime v0.30.3/go.mod h1:hwrrRdd9P84CXSKzhHxrOivAR9BRnkMt0OeP5mj7X30=\nk8s.io/client-go v0.30.3 h1:bHrJu3xQZNXIi8/MoxYtZBBWQQXwy16zqJwloXXfD3k=\nk8s.io/client-go v0.30.3/go.mod h1:8d4pf8vYu665/kUbsxWAQ/JDBNWqfFeZnvFiVdmx89U=\nk8s.io/component-base v0.30.3 h1:Ci0UqKWf4oiwy8hr1+E3dsnliKnkMLZMVbWzeorlk7s=\nk8s.io/component-base v0.30.3/go.mod h1:C1SshT3rGPCuNtBs14RmVD2xW0EhRSeLvBh7AGk1quA=\nk8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=\nk8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=\nk8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=\nk8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=\nk8s.io/kubectl v0.30.3 h1:YIBBvMdTW0xcDpmrOBzcpUVsn+zOgjMYIu7kAq+yqiI=\nk8s.io/kubectl v0.30.3/go.mod h1:IcR0I9RN2+zzTRUa1BzZCm4oM0NLOawE6RzlDvd1Fpo=\nk8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ=\nk8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=\nmodernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=\nmodernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=\nmodernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=\nmodernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=\nmodernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=\nmodernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=\nmodernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=\nmodernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=\nmodernc.org/gc/v3 v3.0.0-20240722195230-4a140ff9c08e h1:WPC4v0rNIFb2PY+nBBEEKyugPPRHPzUgyN3xZPpGK58=\nmodernc.org/gc/v3 v3.0.0-20240722195230-4a140ff9c08e/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=\nmodernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=\nmodernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=\nmodernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=\nmodernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=\nmodernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=\nmodernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=\nmodernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=\nmodernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=\nmodernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=\nmodernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=\nmodernc.org/sqlite v1.31.1 h1:XVU0VyzxrYHlBhIs1DiEgSl0ZtdnPtbLVy8hSkzxGrs=\nmodernc.org/sqlite v1.31.1/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=\nmodernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=\nmodernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=\nmodernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=\nmodernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=\noras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo=\noras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo=\nsigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=\nsigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=\nsigs.k8s.io/kustomize/api v0.16.0 h1:/zAR4FOQDCkgSDmVzV2uiFbuy9bhu3jEzthrHCuvm1g=\nsigs.k8s.io/kustomize/api v0.16.0/go.mod h1:MnFZ7IP2YqVyVwMWoRxPtgl/5hpA+eCCrQR/866cm5c=\nsigs.k8s.io/kustomize/kyaml v0.16.0 h1:6J33uKSoATlKZH16unr2XOhDI+otoe2sR3M8PDzW3K0=\nsigs.k8s.io/kustomize/kyaml v0.16.0/go.mod h1:xOK/7i+vmE14N2FdFyugIshB8eF6ALpy7jI87Q2nRh4=\nsigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=\nsigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=\nsigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=\nsigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=\n"
  },
  {
    "path": "kubelab-backend/hooks/hooks.go",
    "content": "package hooks\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/pocketbase/pocketbase/models\"\n)\n\nfunc PocketBaseInit(app *pocketbase.PocketBase) error {\n\tmodelHandler := func(event string) func(e *core.ModelEvent) error {\n\t\treturn func(e *core.ModelEvent) error {\n\t\t\ttable := e.Model.TableName()\n\t\t\t// we don't want to executeEventActions if the event is a system event (e.g. \"_collections\" changes)\n\t\t\tif record, ok := e.Model.(*models.Record); ok {\n\t\t\t\texecuteEventActions(app, event, table, record)\n\t\t\t} else {\n\t\t\t\tlog.Println(\"Skipping executeEventActions for table:\", table)\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\tapp.OnBeforeServe().Add(func(e *core.ServeEvent) error {\n\t\tapp.OnModelAfterCreate().Add(modelHandler(\"insert\"))\n\t\tapp.OnModelAfterUpdate().Add(modelHandler(\"update\"))\n\t\tapp.OnModelAfterDelete().Add(modelHandler(\"delete\"))\n\t\treturn nil\n\t})\n\treturn nil\n}\n\nfunc executeEventActions(app *pocketbase.PocketBase, event string, table string, record *models.Record) {\n\t// TODO: Load and cache this. Reload only on changes to \"hooks\" table\n\trows := []dbx.NullStringMap{}\n\tapp.DB().Select(\"action_type\", \"action\", \"action_params\", \"expands\").\n\t\tFrom(\"hooks\").\n\t\tWhere(dbx.HashExp{\"collection\": table, \"event\": event, \"disabled\": false}).\n\t\tAll(&rows)\n\tfor _, row := range rows {\n\t\taction_type := row[\"action_type\"].String\n\t\taction := row[\"action\"].String\n\t\taction_params := row[\"action_params\"].String\n\t\texpands := strings.Split(row[\"expands\"].String, \",\")\n\t\tapp.Dao().ExpandRecord(record, expands, func(c *models.Collection, ids []string) ([]*models.Record, error) {\n\t\t\treturn app.Dao().FindRecordsByIds(c.Name, ids, nil)\n\t\t})\n\t\tif err := executeEventAction(event, table, action_type, action, action_params, record); err != nil {\n\t\t\tlog.Println(\"ERROR\", err)\n\t\t}\n\t}\n}\n\nfunc executeEventAction(event, table, action_type, action, action_params string, record *models.Record) error {\n\tlog.Printf(\"event:%s, table: %s, action: %s\\n\", event, table, action)\n\tswitch action_type {\n\tcase \"command\":\n\t\treturn doCommand(action, action_params, record)\n\tcase \"post\":\n\t\treturn doPost(action, action_params, record)\n\tdefault:\n\t\treturn errors.New(fmt.Sprintf(\"Unknown action_type: %s\", action_type))\n\t}\n}\n\nfunc doCommand(action, action_params string, record *models.Record) error {\n\tcmd := exec.Command(action, action_params)\n\tif w, err := cmd.StdinPipe(); err != nil {\n\t\treturn err\n\t} else {\n\t\tif r, err := cmd.StdoutPipe(); err != nil {\n\t\t\treturn err\n\t\t} else {\n\t\t\tgo func() {\n\t\t\t\tdefer w.Close()\n\t\t\t\tdefer r.Close()\n\t\t\t\tlog.Println(\"-------------------------------\")\n\t\t\t\tdefer log.Println(\"-------------------------------\")\n\t\t\t\tif err := cmd.Start(); err != nil {\n\t\t\t\t\tlog.Printf(\"command start failed: %s %+v\\n\", action, err)\n\t\t\t\t} else {\n\t\t\t\t\t// write JSON into the pipe and close\n\t\t\t\t\tjson.NewEncoder(w).Encode(record)\n\t\t\t\t\tw.Close()\n\t\t\t\t\tif err := cmd.Wait(); err != nil {\n\t\t\t\t\t\tlog.Printf(\"command wait failed: %s %+v\\n\", action, err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}()\n\t\t\t// read pipe's stdout and copy to ours (in parallel to the above goroutine)\n\t\t\tio.Copy(os.Stdout, r)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc doPost(action, action_params string, record *models.Record) error {\n\tr, w := io.Pipe()\n\tdefer w.Close()\n\tgo func() {\n\t\tdefer r.Close()\n\t\tif resp, err := http.Post(action, \"application/json\", r); err != nil {\n\t\t\tlog.Println(\"POST failed\", action, err)\n\t\t} else {\n\t\t\tio.Copy(os.Stdout, resp.Body)\n\t\t}\n\t}()\n\tif err := json.NewEncoder(w).Encode(record); err != nil {\n\t\tlog.Println(\"ERROR writing to pipe\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "kubelab-backend/main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/natrontech/kubelab/hooks\"\n\t\"github.com/natrontech/kubelab/pkg/controller\"\n\t\"github.com/natrontech/kubelab/pkg/env\"\n\t\"github.com/natrontech/kubelab/pkg/k8s\"\n\t\"github.com/pocketbase/pocketbase\"\n\t\"github.com/pocketbase/pocketbase/apis\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/pocketbase/pocketbase/plugins/jsvm\"\n\t\"github.com/pocketbase/pocketbase/plugins/migratecmd\"\n\t\"github.com/pocketbase/pocketbase/tools/cron\"\n)\n\nfunc defaultPublicDir() string {\n\tif strings.HasPrefix(os.Args[0], os.TempDir()) {\n\t\t// most likely ran with go run\n\t\treturn \"./pb_public\"\n\t}\n\n\treturn filepath.Join(os.Args[0], \"../pb_public\")\n}\n\nfunc init() {\n\t// set the default public dir\n\tenv.Init()\n\tk8s.Init()\n}\n\nfunc main() {\n\tapp := pocketbase.New()\n\n\tvar publicDirFlag string\n\n\t// add \"--publicDir\" option flag\n\tapp.RootCmd.PersistentFlags().StringVar(\n\t\t&publicDirFlag,\n\t\t\"publicDir\",\n\t\tdefaultPublicDir(),\n\t\t\"the directory to serve static files\",\n\t)\n\tmigrationsDir := \"\" // default to \"pb_migrations\" (for js) and \"migrations\" (for go)\n\n\t// load js files to allow loading external JavaScript migrations\n\tjsvm.MustRegister(app, jsvm.Config{\n\t\t// Dir: migrationsDir,\n\t\tMigrationsDir: migrationsDir,\n\t})\n\n\t// register the `migrate` command\n\tmigratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{\n\t\tTemplateLang: migratecmd.TemplateLangJS, // or migratecmd.TemplateLangGo (default)\n\t\tDir:          migrationsDir,\n\t\tAutomigrate:  true,\n\t})\n\n\t// call this only if you want to use the configurable \"hooks\" functionality\n\thooks.PocketBaseInit(app)\n\n\tapp.OnBeforeServe().Add(func(e *core.ServeEvent) error {\n\t\t// serves static files from the provided public dir (if exists)\n\t\te.Router.GET(\"/*\", apis.StaticDirectoryHandler(os.DirFS(publicDirFlag), true))\n\n\t\treturn nil\n\t})\n\n\tapp.OnRecordBeforeUpdateRequest().Add(func(e *core.RecordUpdateEvent) error {\n\t\tswitch e.Collection.Name {\n\t\tcase \"lab_sessions\":\n\t\t\treturn controller.HandleLabSessions(e, app)\n\t\tcase \"exercise_sessions\":\n\t\t\treturn controller.HandleExerciseSessions(e, app)\n\t\t}\n\t\treturn nil\n\t})\n\n\t// scheduler for syncing lab and exercise sessions\n\tapp.OnBeforeBootstrap().Add(func(e *core.BootstrapEvent) error {\n\t\tscheduler := cron.New()\n\n\t\t// Run sync every minute\n\t\tscheduler.MustAdd(\"sessions_syncer\", env.Config.CronTick, func() {\n\t\t\terr := controller.AutoSessionSyncController(app)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error syncing sessions: %v\\n\", err)\n\t\t\t}\n\t\t})\n\n\t\tscheduler.Start()\n\t\treturn nil\n\t})\n\n\tif err := app.Start(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "kubelab-backend/modd.conf",
    "content": "# Run go test on ALL modules on startup, and subsequently only on modules\n# containing changes.\n**/*.go {\n    prep: go build\n    # prep: go test @dirmods\n    daemon +sigterm: ./kubelab serve --http 0.0.0.0:8090 --publicDir ../kubelab-ui/build\n}\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1671835039_created_hooks.js",
    "content": "migrate(\n    (db) => {\n        const collection = new Collection({\n            id: \"3fhw2mfr9zrgodj\",\n            created: \"2022-12-23 22:30:35.443Z\",\n            updated: \"2022-12-23 22:30:35.443Z\",\n            name: \"hooks\",\n            type: \"base\",\n            system: false,\n            schema: [\n                {\n                    system: false,\n                    id: \"j8mewfur\",\n                    name: \"collection\",\n                    type: \"text\",\n                    required: true,\n                    unique: false,\n                    options: {\n                        min: null,\n                        max: null,\n                        pattern: \"\",\n                    },\n                },\n                {\n                    system: false,\n                    id: \"4xcxcfuv\",\n                    name: \"event\",\n                    type: \"select\",\n                    required: true,\n                    unique: false,\n                    options: {\n                        maxSelect: 1,\n                        values: [\"insert\", \"update\", \"delete\"],\n                    },\n                },\n                {\n                    system: false,\n                    id: \"u3bmgjpb\",\n                    name: \"action_type\",\n                    type: \"select\",\n                    required: true,\n                    unique: false,\n                    options: {\n                        maxSelect: 1,\n                        values: [\"command\", \"post\"],\n                    },\n                },\n                {\n                    system: false,\n                    id: \"kayyu1l3\",\n                    name: \"action\",\n                    type: \"text\",\n                    required: true,\n                    unique: false,\n                    options: {\n                        min: null,\n                        max: null,\n                        pattern: \"\",\n                    },\n                },\n                {\n                    system: false,\n                    id: \"zkengev8\",\n                    name: \"action_params\",\n                    type: \"text\",\n                    required: false,\n                    unique: false,\n                    options: {\n                        min: null,\n                        max: null,\n                        pattern: \"\",\n                    },\n                },\n            ],\n            listRule: null,\n            viewRule: null,\n            createRule: null,\n            updateRule: null,\n            deleteRule: null,\n            options: {},\n        });\n\n        return Dao(db).saveCollection(collection);\n    },\n    (db) => {\n        const dao = new Dao(db);\n        const collection = dao.findCollectionByNameOrId(\"3fhw2mfr9zrgodj\");\n\n        return dao.deleteCollection(collection);\n    }\n);\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1671926343_updated_hooks.js",
    "content": "migrate(\n    (db) => {\n        const dao = new Dao(db);\n        const collection = dao.findCollectionByNameOrId(\"3fhw2mfr9zrgodj\");\n\n        // add\n        collection.schema.addField(\n            new SchemaField({\n                system: false,\n                id: \"balsaeka\",\n                name: \"expands\",\n                type: \"text\",\n                required: false,\n                unique: false,\n                options: {\n                    min: null,\n                    max: null,\n                    pattern: \"\",\n                },\n            })\n        );\n\n        // add\n        collection.schema.addField(\n            new SchemaField({\n                system: false,\n                id: \"emgxgcok\",\n                name: \"disabled\",\n                type: \"bool\",\n                required: false,\n                unique: false,\n                options: {},\n            })\n        );\n\n        return dao.saveCollection(collection);\n    },\n    (db) => {\n        const dao = new Dao(db);\n        const collection = dao.findCollectionByNameOrId(\"3fhw2mfr9zrgodj\");\n\n        // remove\n        collection.schema.removeField(\"balsaeka\");\n\n        // remove\n        collection.schema.removeField(\"emgxgcok\");\n\n        return dao.saveCollection(collection);\n    }\n);\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1685691966_created_sessions.js",
    "content": "migrate(\n    (db) => {\n        const collection = new Collection({\n            id: \"n7tne53bbobf2bt\",\n            created: \"2023-06-02 07:46:06.017Z\",\n            updated: \"2023-06-02 07:46:06.017Z\",\n            name: \"sessions\",\n            type: \"base\",\n            system: false,\n            schema: [\n                {\n                    system: false,\n                    id: \"rse3zear\",\n                    name: \"user\",\n                    type: \"relation\",\n                    required: false,\n                    unique: false,\n                    options: {\n                        collectionId: \"_pb_users_auth_\",\n                        cascadeDelete: false,\n                        minSelect: null,\n                        maxSelect: 1,\n                        displayFields: [],\n                    },\n                },\n            ],\n            indexes: [],\n            listRule: null,\n            viewRule: null,\n            createRule: null,\n            updateRule: null,\n            deleteRule: null,\n            options: {},\n        });\n\n        return Dao(db).saveCollection(collection);\n    },\n    (db) => {\n        const dao = new Dao(db);\n        const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\");\n\n        return dao.deleteCollection(collection);\n    }\n);\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686128320_updated_users.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"_pb_users_auth_\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"1hbozave\",\n    \"name\": \"totalScore\",\n    \"type\": \"number\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null\n    }\n  }))\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"q6psj4sr\",\n    \"name\": \"avgMinutesToSolution\",\n    \"type\": \"number\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"_pb_users_auth_\")\n\n  // remove\n  collection.schema.removeField(\"1hbozave\")\n\n  // remove\n  collection.schema.removeField(\"q6psj4sr\")\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686128357_created_labs.js",
    "content": "migrate((db) => {\n  const collection = new Collection({\n    \"id\": \"a1s1vqlm7141lcr\",\n    \"created\": \"2023-06-07 08:59:17.658Z\",\n    \"updated\": \"2023-06-07 08:59:17.658Z\",\n    \"name\": \"labs\",\n    \"type\": \"base\",\n    \"system\": false,\n    \"schema\": [\n      {\n        \"system\": false,\n        \"id\": \"00op3jnz\",\n        \"name\": \"title\",\n        \"type\": \"text\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {\n          \"min\": null,\n          \"max\": null,\n          \"pattern\": \"\"\n        }\n      },\n      {\n        \"system\": false,\n        \"id\": \"abvux9fb\",\n        \"name\": \"description\",\n        \"type\": \"text\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {\n          \"min\": null,\n          \"max\": null,\n          \"pattern\": \"\"\n        }\n      }\n    ],\n    \"indexes\": [],\n    \"listRule\": null,\n    \"viewRule\": null,\n    \"createRule\": null,\n    \"updateRule\": null,\n    \"deleteRule\": null,\n    \"options\": {}\n  });\n\n  return Dao(db).saveCollection(collection);\n}, (db) => {\n  const dao = new Dao(db);\n  const collection = dao.findCollectionByNameOrId(\"a1s1vqlm7141lcr\");\n\n  return dao.deleteCollection(collection);\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686128492_updated_sessions.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"ituarijg\",\n    \"name\": \"title\",\n    \"type\": \"text\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null,\n      \"pattern\": \"\"\n    }\n  }))\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"thwhdlu1\",\n    \"name\": \"startTime\",\n    \"type\": \"date\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": \"\",\n      \"max\": \"\"\n    }\n  }))\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"mapevfhq\",\n    \"name\": \"endTime\",\n    \"type\": \"date\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": \"\",\n      \"max\": \"\"\n    }\n  }))\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"l3csgcmv\",\n    \"name\": \"lab\",\n    \"type\": \"relation\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"a1s1vqlm7141lcr\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": 1,\n      \"displayFields\": [\n        \"title\"\n      ]\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  // remove\n  collection.schema.removeField(\"ituarijg\")\n\n  // remove\n  collection.schema.removeField(\"thwhdlu1\")\n\n  // remove\n  collection.schema.removeField(\"mapevfhq\")\n\n  // remove\n  collection.schema.removeField(\"l3csgcmv\")\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686128857_updated_sessions.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"6zraywm2\",\n    \"name\": \"score\",\n    \"type\": \"number\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": 0,\n      \"max\": null\n    }\n  }))\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"tr8kf1qh\",\n    \"name\": \"clusterRunning\",\n    \"type\": \"bool\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {}\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  // remove\n  collection.schema.removeField(\"6zraywm2\")\n\n  // remove\n  collection.schema.removeField(\"tr8kf1qh\")\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686129123_created_exercises.js",
    "content": "migrate((db) => {\n  const collection = new Collection({\n    \"id\": \"s4f0lpy3ibkgfqp\",\n    \"created\": \"2023-06-07 09:12:03.931Z\",\n    \"updated\": \"2023-06-07 09:12:03.931Z\",\n    \"name\": \"exercises\",\n    \"type\": \"base\",\n    \"system\": false,\n    \"schema\": [\n      {\n        \"system\": false,\n        \"id\": \"ow94rlnx\",\n        \"name\": \"title\",\n        \"type\": \"text\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {\n          \"min\": null,\n          \"max\": null,\n          \"pattern\": \"\"\n        }\n      },\n      {\n        \"system\": false,\n        \"id\": \"ukrbpgyw\",\n        \"name\": \"description\",\n        \"type\": \"text\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {\n          \"min\": null,\n          \"max\": null,\n          \"pattern\": \"\"\n        }\n      },\n      {\n        \"system\": false,\n        \"id\": \"qpleqnzf\",\n        \"name\": \"docs\",\n        \"type\": \"url\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {\n          \"exceptDomains\": null,\n          \"onlyDomains\": null\n        }\n      },\n      {\n        \"system\": false,\n        \"id\": \"lnv6grdi\",\n        \"name\": \"hint\",\n        \"type\": \"url\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {\n          \"exceptDomains\": null,\n          \"onlyDomains\": null\n        }\n      },\n      {\n        \"system\": false,\n        \"id\": \"lslokljx\",\n        \"name\": \"solution\",\n        \"type\": \"url\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {\n          \"exceptDomains\": null,\n          \"onlyDomains\": null\n        }\n      },\n      {\n        \"system\": false,\n        \"id\": \"1opfp3c3\",\n        \"name\": \"check\",\n        \"type\": \"url\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {\n          \"exceptDomains\": null,\n          \"onlyDomains\": null\n        }\n      },\n      {\n        \"system\": false,\n        \"id\": \"oud4eihs\",\n        \"name\": \"bootstrap\",\n        \"type\": \"url\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {\n          \"exceptDomains\": null,\n          \"onlyDomains\": null\n        }\n      },\n      {\n        \"system\": false,\n        \"id\": \"uao3gxwr\",\n        \"name\": \"agentRunning\",\n        \"type\": \"bool\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {}\n      }\n    ],\n    \"indexes\": [],\n    \"listRule\": null,\n    \"viewRule\": null,\n    \"createRule\": null,\n    \"updateRule\": null,\n    \"deleteRule\": null,\n    \"options\": {}\n  });\n\n  return Dao(db).saveCollection(collection);\n}, (db) => {\n  const dao = new Dao(db);\n  const collection = dao.findCollectionByNameOrId(\"s4f0lpy3ibkgfqp\");\n\n  return dao.deleteCollection(collection);\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686129154_updated_labs.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"a1s1vqlm7141lcr\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"pnavjyzl\",\n    \"name\": \"exercises\",\n    \"type\": \"relation\",\n    \"required\": true,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"s4f0lpy3ibkgfqp\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": null,\n      \"displayFields\": [\n        \"title\"\n      ]\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"a1s1vqlm7141lcr\")\n\n  // remove\n  collection.schema.removeField(\"pnavjyzl\")\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686129166_updated_sessions.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"l3csgcmv\",\n    \"name\": \"lab\",\n    \"type\": \"relation\",\n    \"required\": true,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"a1s1vqlm7141lcr\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": 1,\n      \"displayFields\": [\n        \"title\"\n      ]\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"l3csgcmv\",\n    \"name\": \"lab\",\n    \"type\": \"relation\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"a1s1vqlm7141lcr\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": 1,\n      \"displayFields\": [\n        \"title\"\n      ]\n    }\n  }))\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686144343_updated_labs.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"a1s1vqlm7141lcr\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"qe7ybzdk\",\n    \"name\": \"docs\",\n    \"type\": \"url\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"exceptDomains\": null,\n      \"onlyDomains\": null\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"a1s1vqlm7141lcr\")\n\n  // remove\n  collection.schema.removeField(\"qe7ybzdk\")\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686311410_updated_sessions.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  // remove\n  collection.schema.removeField(\"ituarijg\")\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"ituarijg\",\n    \"name\": \"title\",\n    \"type\": \"text\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null,\n      \"pattern\": \"\"\n    }\n  }))\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686311431_updated_sessions.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  collection.name = \"lab_sessions\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  collection.name = \"sessions\"\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686311476_created_exercise_sessions.js",
    "content": "migrate((db) => {\n  const collection = new Collection({\n    \"id\": \"qj6ssich32lcxru\",\n    \"created\": \"2023-06-09 11:51:16.171Z\",\n    \"updated\": \"2023-06-09 11:51:16.171Z\",\n    \"name\": \"exercise_sessions\",\n    \"type\": \"base\",\n    \"system\": false,\n    \"schema\": [\n      {\n        \"system\": false,\n        \"id\": \"nc43tbmx\",\n        \"name\": \"agentRunning\",\n        \"type\": \"bool\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {}\n      }\n    ],\n    \"indexes\": [],\n    \"listRule\": null,\n    \"viewRule\": null,\n    \"createRule\": null,\n    \"updateRule\": null,\n    \"deleteRule\": null,\n    \"options\": {}\n  });\n\n  return Dao(db).saveCollection(collection);\n}, (db) => {\n  const dao = new Dao(db);\n  const collection = dao.findCollectionByNameOrId(\"qj6ssich32lcxru\");\n\n  return dao.deleteCollection(collection);\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686311483_updated_exercises.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"s4f0lpy3ibkgfqp\")\n\n  // remove\n  collection.schema.removeField(\"uao3gxwr\")\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"s4f0lpy3ibkgfqp\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"uao3gxwr\",\n    \"name\": \"agentRunning\",\n    \"type\": \"bool\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {}\n  }))\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686311638_updated_exercise_sessions.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"qj6ssich32lcxru\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"iousqpjz\",\n    \"name\": \"user\",\n    \"type\": \"relation\",\n    \"required\": true,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"_pb_users_auth_\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": 1,\n      \"displayFields\": [\n        \"email\"\n      ]\n    }\n  }))\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"g8s5seza\",\n    \"name\": \"startTime\",\n    \"type\": \"date\",\n    \"required\": true,\n    \"unique\": false,\n    \"options\": {\n      \"min\": \"\",\n      \"max\": \"\"\n    }\n  }))\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"a3yguusb\",\n    \"name\": \"endTime\",\n    \"type\": \"date\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": \"\",\n      \"max\": \"\"\n    }\n  }))\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"9oftn8f7\",\n    \"name\": \"exercise\",\n    \"type\": \"relation\",\n    \"required\": true,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"s4f0lpy3ibkgfqp\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": 1,\n      \"displayFields\": [\n        \"title\"\n      ]\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"qj6ssich32lcxru\")\n\n  // remove\n  collection.schema.removeField(\"iousqpjz\")\n\n  // remove\n  collection.schema.removeField(\"g8s5seza\")\n\n  // remove\n  collection.schema.removeField(\"a3yguusb\")\n\n  // remove\n  collection.schema.removeField(\"9oftn8f7\")\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686311654_updated_lab_sessions.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  // remove\n  collection.schema.removeField(\"6zraywm2\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"rse3zear\",\n    \"name\": \"user\",\n    \"type\": \"relation\",\n    \"required\": true,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"_pb_users_auth_\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": 1,\n      \"displayFields\": []\n    }\n  }))\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"thwhdlu1\",\n    \"name\": \"startTime\",\n    \"type\": \"date\",\n    \"required\": true,\n    \"unique\": false,\n    \"options\": {\n      \"min\": \"\",\n      \"max\": \"\"\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"6zraywm2\",\n    \"name\": \"score\",\n    \"type\": \"number\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": 0,\n      \"max\": null\n    }\n  }))\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"rse3zear\",\n    \"name\": \"user\",\n    \"type\": \"relation\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"_pb_users_auth_\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": 1,\n      \"displayFields\": []\n    }\n  }))\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"thwhdlu1\",\n    \"name\": \"startTime\",\n    \"type\": \"date\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": \"\",\n      \"max\": \"\"\n    }\n  }))\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686311927_created_user_exercise_score.js",
    "content": "migrate((db) => {\n  const collection = new Collection({\n    \"id\": \"c0gc1ph97tdim19\",\n    \"created\": \"2023-06-09 11:58:47.771Z\",\n    \"updated\": \"2023-06-09 11:58:47.771Z\",\n    \"name\": \"user_exercise_score\",\n    \"type\": \"view\",\n    \"system\": false,\n    \"schema\": [],\n    \"indexes\": [],\n    \"listRule\": null,\n    \"viewRule\": null,\n    \"createRule\": null,\n    \"updateRule\": null,\n    \"deleteRule\": null,\n    \"options\": {\n      \"query\": \"SELECT lab_sessions.id\\nFROM lab_sessions\"\n    }\n  });\n\n  return Dao(db).saveCollection(collection);\n}, (db) => {\n  const dao = new Dao(db);\n  const collection = dao.findCollectionByNameOrId(\"c0gc1ph97tdim19\");\n\n  return dao.deleteCollection(collection);\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686312000_updated_user_exercise_score.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"c0gc1ph97tdim19\")\n\n  collection.options = {\n    \"query\": \"SELECT lab_sessions.id, COUNT(lab_sessions.id) as sum\\nFROM lab_sessions\"\n  }\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"wmj3s2se\",\n    \"name\": \"sum\",\n    \"type\": \"number\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"c0gc1ph97tdim19\")\n\n  collection.options = {\n    \"query\": \"SELECT lab_sessions.id\\nFROM lab_sessions\"\n  }\n\n  // remove\n  collection.schema.removeField(\"wmj3s2se\")\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686312029_updated_user_exercise_score.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"c0gc1ph97tdim19\")\n\n  collection.options = {\n    \"query\": \"SELECT (ROW_NUMBER() OVER()) as id, COUNT(lab_sessions.id) as sum\\nFROM lab_sessions\"\n  }\n\n  // remove\n  collection.schema.removeField(\"wmj3s2se\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"jgwml4ai\",\n    \"name\": \"sum\",\n    \"type\": \"number\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"c0gc1ph97tdim19\")\n\n  collection.options = {\n    \"query\": \"SELECT lab_sessions.id, COUNT(lab_sessions.id) as sum\\nFROM lab_sessions\"\n  }\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"wmj3s2se\",\n    \"name\": \"sum\",\n    \"type\": \"number\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null\n    }\n  }))\n\n  // remove\n  collection.schema.removeField(\"jgwml4ai\")\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686312069_updated_user_exercise_score.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"c0gc1ph97tdim19\")\n\n  collection.options = {\n    \"query\": \"SELECT (ROW_NUMBER() OVER()) as id, COUNT(lab_sessions.id) as sum\\nFROM lab_sessions WHERE lab_sessions.endTime NOTNULL;\"\n  }\n\n  // remove\n  collection.schema.removeField(\"jgwml4ai\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"4nfqwv0a\",\n    \"name\": \"sum\",\n    \"type\": \"number\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"c0gc1ph97tdim19\")\n\n  collection.options = {\n    \"query\": \"SELECT (ROW_NUMBER() OVER()) as id, COUNT(lab_sessions.id) as sum\\nFROM lab_sessions\"\n  }\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"jgwml4ai\",\n    \"name\": \"sum\",\n    \"type\": \"number\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null\n    }\n  }))\n\n  // remove\n  collection.schema.removeField(\"4nfqwv0a\")\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686312095_updated_user_exercise_score.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"c0gc1ph97tdim19\")\n\n  collection.options = {\n    \"query\": \"SELECT (ROW_NUMBER() OVER()) as id, COUNT(exercise_sessions.id) as sum\\nFROM exercise_sessions WHERE exercise_sessions.endTime NOTNULL;\"\n  }\n\n  // remove\n  collection.schema.removeField(\"4nfqwv0a\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"swlqrmos\",\n    \"name\": \"sum\",\n    \"type\": \"number\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"c0gc1ph97tdim19\")\n\n  collection.options = {\n    \"query\": \"SELECT (ROW_NUMBER() OVER()) as id, COUNT(lab_sessions.id) as sum\\nFROM lab_sessions WHERE lab_sessions.endTime NOTNULL;\"\n  }\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"4nfqwv0a\",\n    \"name\": \"sum\",\n    \"type\": \"number\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null\n    }\n  }))\n\n  // remove\n  collection.schema.removeField(\"swlqrmos\")\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686312146_updated_user_exercise_score.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"c0gc1ph97tdim19\")\n\n  collection.options = {\n    \"query\": \"SELECT exercise_sessions.user as id, COUNT(exercise_sessions.id) as sum\\nFROM exercise_sessions WHERE exercise_sessions.endTime NOTNULL;\"\n  }\n\n  // remove\n  collection.schema.removeField(\"swlqrmos\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"dvvwd0w4\",\n    \"name\": \"sum\",\n    \"type\": \"number\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"c0gc1ph97tdim19\")\n\n  collection.options = {\n    \"query\": \"SELECT (ROW_NUMBER() OVER()) as id, COUNT(exercise_sessions.id) as sum\\nFROM exercise_sessions WHERE exercise_sessions.endTime NOTNULL;\"\n  }\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"swlqrmos\",\n    \"name\": \"sum\",\n    \"type\": \"number\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null\n    }\n  }))\n\n  // remove\n  collection.schema.removeField(\"dvvwd0w4\")\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686312270_deleted_user_exercise_score.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db);\n  const collection = dao.findCollectionByNameOrId(\"c0gc1ph97tdim19\");\n\n  return dao.deleteCollection(collection);\n}, (db) => {\n  const collection = new Collection({\n    \"id\": \"c0gc1ph97tdim19\",\n    \"created\": \"2023-06-09 11:58:47.771Z\",\n    \"updated\": \"2023-06-09 12:02:26.128Z\",\n    \"name\": \"user_exercise_score\",\n    \"type\": \"view\",\n    \"system\": false,\n    \"schema\": [\n      {\n        \"system\": false,\n        \"id\": \"dvvwd0w4\",\n        \"name\": \"sum\",\n        \"type\": \"number\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {\n          \"min\": null,\n          \"max\": null\n        }\n      }\n    ],\n    \"indexes\": [],\n    \"listRule\": null,\n    \"viewRule\": null,\n    \"createRule\": null,\n    \"updateRule\": null,\n    \"deleteRule\": null,\n    \"options\": {\n      \"query\": \"SELECT exercise_sessions.user as id, COUNT(exercise_sessions.id) as sum\\nFROM exercise_sessions WHERE exercise_sessions.endTime NOTNULL;\"\n    }\n  });\n\n  return Dao(db).saveCollection(collection);\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686312735_updated_labs.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"a1s1vqlm7141lcr\")\n\n  // remove\n  collection.schema.removeField(\"pnavjyzl\")\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"a1s1vqlm7141lcr\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"pnavjyzl\",\n    \"name\": \"exercises\",\n    \"type\": \"relation\",\n    \"required\": true,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"s4f0lpy3ibkgfqp\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": null,\n      \"displayFields\": [\n        \"title\"\n      ]\n    }\n  }))\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686312758_updated_exercises.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"s4f0lpy3ibkgfqp\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"fqnmihgd\",\n    \"name\": \"lab\",\n    \"type\": \"relation\",\n    \"required\": true,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"a1s1vqlm7141lcr\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": 1,\n      \"displayFields\": [\n        \"title\"\n      ]\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"s4f0lpy3ibkgfqp\")\n\n  // remove\n  collection.schema.removeField(\"fqnmihgd\")\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686485128_updated_labs.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"a1s1vqlm7141lcr\")\n\n  collection.listRule = \"\"\n  collection.viewRule = \"\"\n  collection.createRule = \"\"\n  collection.updateRule = \"\"\n  collection.deleteRule = \"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"a1s1vqlm7141lcr\")\n\n  collection.listRule = null\n  collection.viewRule = null\n  collection.createRule = null\n  collection.updateRule = null\n  collection.deleteRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686485135_updated_lab_sessions.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  collection.listRule = \"\"\n  collection.viewRule = \"\"\n  collection.createRule = \"\"\n  collection.updateRule = \"\"\n  collection.deleteRule = \"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  collection.listRule = null\n  collection.viewRule = null\n  collection.createRule = null\n  collection.updateRule = null\n  collection.deleteRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686485142_updated_exercises.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"s4f0lpy3ibkgfqp\")\n\n  collection.listRule = \"\"\n  collection.viewRule = \"\"\n  collection.createRule = \"\"\n  collection.updateRule = \"\"\n  collection.deleteRule = \"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"s4f0lpy3ibkgfqp\")\n\n  collection.listRule = null\n  collection.viewRule = null\n  collection.createRule = null\n  collection.updateRule = null\n  collection.deleteRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686485148_updated_exercise_sessions.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"qj6ssich32lcxru\")\n\n  collection.listRule = \"\"\n  collection.viewRule = \"\"\n  collection.createRule = \"\"\n  collection.updateRule = \"\"\n  collection.deleteRule = \"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"qj6ssich32lcxru\")\n\n  collection.listRule = null\n  collection.viewRule = null\n  collection.createRule = null\n  collection.updateRule = null\n  collection.deleteRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686568901_updated_exercise_sessions.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"qj6ssich32lcxru\")\n\n  collection.listRule = \"@request.auth.id != user.id\"\n  collection.viewRule = \"@request.auth.id != user.id\"\n  collection.createRule = \"@request.auth.id != user.id\"\n  collection.updateRule = \"@request.auth.id != user.id\"\n  collection.deleteRule = \"@request.auth.id != user.id\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"qj6ssich32lcxru\")\n\n  collection.listRule = null\n  collection.viewRule = null\n  collection.createRule = null\n  collection.updateRule = null\n  collection.deleteRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686568909_updated_lab_sessions.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  collection.listRule = \"@request.auth.id != user.id\"\n  collection.viewRule = \"@request.auth.id != user.id\"\n  collection.createRule = \"@request.auth.id != user.id\"\n  collection.updateRule = \"@request.auth.id != user.id\"\n  collection.deleteRule = \"@request.auth.id != user.id\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  collection.listRule = null\n  collection.viewRule = null\n  collection.createRule = null\n  collection.updateRule = null\n  collection.deleteRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686569025_updated_lab_sessions.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  collection.listRule = \"\"\n  collection.viewRule = \"\"\n  collection.createRule = \"\"\n  collection.updateRule = \"\"\n  collection.deleteRule = \"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  collection.listRule = null\n  collection.viewRule = null\n  collection.createRule = null\n  collection.updateRule = null\n  collection.deleteRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686569075_updated_lab_sessions.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  collection.listRule = \"@request.auth.id = user.id \"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  collection.listRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686569086_updated_lab_sessions.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  collection.viewRule = \"@request.auth.id = user.id \"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  collection.viewRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686569099_updated_lab_sessions.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  collection.createRule = \"@request.auth.id = user.id \"\n  collection.updateRule = \"@request.auth.id = user.id \"\n  collection.deleteRule = \"@request.auth.id = user.id \"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  collection.createRule = null\n  collection.updateRule = null\n  collection.deleteRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686569141_updated_exercise_sessions.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"qj6ssich32lcxru\")\n\n  collection.listRule = \"@request.auth.id = user.id \"\n  collection.viewRule = \"@request.auth.id = user.id \"\n  collection.createRule = \"@request.auth.id = user.id \"\n  collection.updateRule = \"@request.auth.id = user.id \"\n  collection.deleteRule = \"@request.auth.id = user.id \"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"qj6ssich32lcxru\")\n\n  collection.listRule = null\n  collection.viewRule = null\n  collection.createRule = null\n  collection.updateRule = null\n  collection.deleteRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686600246_updated_exercise_sessions.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"qj6ssich32lcxru\")\n\n  collection.listRule = \"\"\n  collection.viewRule = \"\"\n  collection.createRule = \"\"\n  collection.updateRule = \"\"\n  collection.deleteRule = \"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"qj6ssich32lcxru\")\n\n  collection.listRule = null\n  collection.viewRule = null\n  collection.createRule = null\n  collection.updateRule = null\n  collection.deleteRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686600263_updated_exercise_sessions.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"qj6ssich32lcxru\")\n\n  collection.listRule = \"@request.auth.id = user.id\"\n  collection.viewRule = \"@request.auth.id = user.id\"\n  collection.createRule = \"@request.auth.id = user.id\"\n  collection.updateRule = \"@request.auth.id = user.id\"\n  collection.deleteRule = \"@request.auth.id = user.id\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"qj6ssich32lcxru\")\n\n  collection.listRule = null\n  collection.viewRule = null\n  collection.createRule = null\n  collection.updateRule = null\n  collection.deleteRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686990480_updated_exercise_sessions.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"qj6ssich32lcxru\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"g8s5seza\",\n    \"name\": \"startTime\",\n    \"type\": \"date\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": \"\",\n      \"max\": \"\"\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"qj6ssich32lcxru\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"g8s5seza\",\n    \"name\": \"startTime\",\n    \"type\": \"date\",\n    \"required\": true,\n    \"unique\": false,\n    \"options\": {\n      \"min\": \"\",\n      \"max\": \"\"\n    }\n  }))\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1686990524_updated_lab_sessions.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"thwhdlu1\",\n    \"name\": \"startTime\",\n    \"type\": \"date\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": \"\",\n      \"max\": \"\"\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"thwhdlu1\",\n    \"name\": \"startTime\",\n    \"type\": \"date\",\n    \"required\": true,\n    \"unique\": false,\n    \"options\": {\n      \"min\": \"\",\n      \"max\": \"\"\n    }\n  }))\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1687092568_updated_labs.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"a1s1vqlm7141lcr\")\n\n  collection.listRule = null\n  collection.viewRule = null\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"a1s1vqlm7141lcr\")\n\n  collection.listRule = \"\"\n  collection.viewRule = \"\"\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1687092574_updated_exercises.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"s4f0lpy3ibkgfqp\")\n\n  collection.listRule = null\n  collection.viewRule = null\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"s4f0lpy3ibkgfqp\")\n\n  collection.listRule = \"\"\n  collection.viewRule = \"\"\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1687092614_updated_exercises.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"s4f0lpy3ibkgfqp\")\n\n  collection.listRule = \"\"\n  collection.viewRule = \"\"\n  collection.createRule = null\n  collection.updateRule = null\n  collection.deleteRule = null\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"s4f0lpy3ibkgfqp\")\n\n  collection.listRule = null\n  collection.viewRule = null\n  collection.createRule = \"\"\n  collection.updateRule = \"\"\n  collection.deleteRule = \"\"\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1687092623_updated_labs.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"a1s1vqlm7141lcr\")\n\n  collection.listRule = \"\"\n  collection.viewRule = \"\"\n  collection.createRule = null\n  collection.updateRule = null\n  collection.deleteRule = null\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"a1s1vqlm7141lcr\")\n\n  collection.listRule = null\n  collection.viewRule = null\n  collection.createRule = \"\"\n  collection.updateRule = \"\"\n  collection.deleteRule = \"\"\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1687116582_updated_exercises.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"s4f0lpy3ibkgfqp\")\n\n  collection.createRule = \"\"\n  collection.updateRule = \"\"\n  collection.deleteRule = \"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"s4f0lpy3ibkgfqp\")\n\n  collection.createRule = null\n  collection.updateRule = null\n  collection.deleteRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1687116588_updated_labs.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"a1s1vqlm7141lcr\")\n\n  collection.createRule = \"\"\n  collection.updateRule = \"\"\n  collection.deleteRule = \"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"a1s1vqlm7141lcr\")\n\n  collection.createRule = null\n  collection.updateRule = null\n  collection.deleteRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1687200869_updated_users.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"_pb_users_auth_\")\n\n  collection.createRule = \"id = @request.auth.id\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"_pb_users_auth_\")\n\n  collection.createRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1687200885_updated_exercises.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"s4f0lpy3ibkgfqp\")\n\n  collection.listRule = null\n  collection.viewRule = null\n  collection.createRule = null\n  collection.updateRule = null\n  collection.deleteRule = null\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"s4f0lpy3ibkgfqp\")\n\n  collection.listRule = \"\"\n  collection.viewRule = \"\"\n  collection.createRule = \"\"\n  collection.updateRule = \"\"\n  collection.deleteRule = \"\"\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1687201122_updated_users.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"_pb_users_auth_\")\n\n  collection.createRule = \"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"_pb_users_auth_\")\n\n  collection.createRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1687201342_updated_exercises.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"s4f0lpy3ibkgfqp\")\n\n  collection.listRule = \"\"\n  collection.viewRule = \"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"s4f0lpy3ibkgfqp\")\n\n  collection.listRule = null\n  collection.viewRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1687201352_updated_labs.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"a1s1vqlm7141lcr\")\n\n  collection.createRule = null\n  collection.updateRule = null\n  collection.deleteRule = null\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"a1s1vqlm7141lcr\")\n\n  collection.createRule = \"\"\n  collection.updateRule = \"\"\n  collection.deleteRule = \"\"\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1687208987_created_material.js",
    "content": "migrate((db) => {\n  const collection = new Collection({\n    \"id\": \"19zg2e2qeca2b7d\",\n    \"created\": \"2023-06-19 21:09:47.996Z\",\n    \"updated\": \"2023-06-19 21:09:47.996Z\",\n    \"name\": \"material\",\n    \"type\": \"base\",\n    \"system\": false,\n    \"schema\": [\n      {\n        \"system\": false,\n        \"id\": \"bql7kw5k\",\n        \"name\": \"name\",\n        \"type\": \"text\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {\n          \"min\": null,\n          \"max\": null,\n          \"pattern\": \"\"\n        }\n      },\n      {\n        \"system\": false,\n        \"id\": \"tkpuopqe\",\n        \"name\": \"file\",\n        \"type\": \"file\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {\n          \"maxSelect\": 1,\n          \"maxSize\": 5242880,\n          \"mimeTypes\": [],\n          \"thumbs\": [],\n          \"protected\": false\n        }\n      }\n    ],\n    \"indexes\": [],\n    \"listRule\": \"\",\n    \"viewRule\": \"\",\n    \"createRule\": null,\n    \"updateRule\": null,\n    \"deleteRule\": null,\n    \"options\": {}\n  });\n\n  return Dao(db).saveCollection(collection);\n}, (db) => {\n  const dao = new Dao(db);\n  const collection = dao.findCollectionByNameOrId(\"19zg2e2qeca2b7d\");\n\n  return dao.deleteCollection(collection);\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1687209270_updated_material.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"19zg2e2qeca2b7d\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"tkpuopqe\",\n    \"name\": \"file\",\n    \"type\": \"file\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"maxSelect\": 1,\n      \"maxSize\": 10485760,\n      \"mimeTypes\": [],\n      \"thumbs\": [],\n      \"protected\": false\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"19zg2e2qeca2b7d\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"tkpuopqe\",\n    \"name\": \"file\",\n    \"type\": \"file\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"maxSelect\": 1,\n      \"maxSize\": 5242880,\n      \"mimeTypes\": [],\n      \"thumbs\": [],\n      \"protected\": false\n    }\n  }))\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1687209711_deleted_material.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db);\n  const collection = dao.findCollectionByNameOrId(\"19zg2e2qeca2b7d\");\n\n  return dao.deleteCollection(collection);\n}, (db) => {\n  const collection = new Collection({\n    \"id\": \"19zg2e2qeca2b7d\",\n    \"created\": \"2023-06-19 21:09:47.996Z\",\n    \"updated\": \"2023-06-19 21:14:30.500Z\",\n    \"name\": \"material\",\n    \"type\": \"base\",\n    \"system\": false,\n    \"schema\": [\n      {\n        \"system\": false,\n        \"id\": \"bql7kw5k\",\n        \"name\": \"name\",\n        \"type\": \"text\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {\n          \"min\": null,\n          \"max\": null,\n          \"pattern\": \"\"\n        }\n      },\n      {\n        \"system\": false,\n        \"id\": \"tkpuopqe\",\n        \"name\": \"file\",\n        \"type\": \"file\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {\n          \"maxSelect\": 1,\n          \"maxSize\": 10485760,\n          \"mimeTypes\": [],\n          \"thumbs\": [],\n          \"protected\": false\n        }\n      }\n    ],\n    \"indexes\": [],\n    \"listRule\": \"\",\n    \"viewRule\": \"\",\n    \"createRule\": null,\n    \"updateRule\": null,\n    \"deleteRule\": null,\n    \"options\": {}\n  });\n\n  return Dao(db).saveCollection(collection);\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1692004564_created_plans.js",
    "content": "migrate((db) => {\n  const collection = new Collection({\n    \"id\": \"twr8eflpoom78k9\",\n    \"created\": \"2023-08-14 09:16:04.916Z\",\n    \"updated\": \"2023-08-14 09:16:04.916Z\",\n    \"name\": \"plans\",\n    \"type\": \"base\",\n    \"system\": false,\n    \"schema\": [\n      {\n        \"system\": false,\n        \"id\": \"jhgt65nx\",\n        \"name\": \"name\",\n        \"type\": \"text\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {\n          \"min\": null,\n          \"max\": null,\n          \"pattern\": \"\"\n        }\n      },\n      {\n        \"system\": false,\n        \"id\": \"mbuxpqro\",\n        \"name\": \"description\",\n        \"type\": \"text\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {\n          \"min\": null,\n          \"max\": null,\n          \"pattern\": \"\"\n        }\n      },\n      {\n        \"system\": false,\n        \"id\": \"qe5g2utb\",\n        \"name\": \"price\",\n        \"type\": \"number\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {\n          \"min\": null,\n          \"max\": null\n        }\n      }\n    ],\n    \"indexes\": [],\n    \"listRule\": \"\",\n    \"viewRule\": \"\",\n    \"createRule\": null,\n    \"updateRule\": null,\n    \"deleteRule\": null,\n    \"options\": {}\n  });\n\n  return Dao(db).saveCollection(collection);\n}, (db) => {\n  const dao = new Dao(db);\n  const collection = dao.findCollectionByNameOrId(\"twr8eflpoom78k9\");\n\n  return dao.deleteCollection(collection);\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1692004578_created_features.js",
    "content": "migrate((db) => {\n  const collection = new Collection({\n    \"id\": \"vknl4jpc8e5wlbv\",\n    \"created\": \"2023-08-14 09:16:18.702Z\",\n    \"updated\": \"2023-08-14 09:16:18.702Z\",\n    \"name\": \"features\",\n    \"type\": \"base\",\n    \"system\": false,\n    \"schema\": [\n      {\n        \"system\": false,\n        \"id\": \"0zkeyduh\",\n        \"name\": \"feature\",\n        \"type\": \"text\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {\n          \"min\": null,\n          \"max\": null,\n          \"pattern\": \"\"\n        }\n      }\n    ],\n    \"indexes\": [],\n    \"listRule\": null,\n    \"viewRule\": null,\n    \"createRule\": null,\n    \"updateRule\": null,\n    \"deleteRule\": null,\n    \"options\": {}\n  });\n\n  return Dao(db).saveCollection(collection);\n}, (db) => {\n  const dao = new Dao(db);\n  const collection = dao.findCollectionByNameOrId(\"vknl4jpc8e5wlbv\");\n\n  return dao.deleteCollection(collection);\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1692004634_updated_plans.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"twr8eflpoom78k9\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"eldb7j7p\",\n    \"name\": \"features\",\n    \"type\": \"relation\",\n    \"required\": true,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"vknl4jpc8e5wlbv\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": null,\n      \"displayFields\": [\n        \"feature\"\n      ]\n    }\n  }))\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"srzysrtq\",\n    \"name\": \"optionalFeatures\",\n    \"type\": \"relation\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"vknl4jpc8e5wlbv\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": null,\n      \"displayFields\": [\n        \"feature\"\n      ]\n    }\n  }))\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"jhgt65nx\",\n    \"name\": \"name\",\n    \"type\": \"text\",\n    \"required\": true,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null,\n      \"pattern\": \"\"\n    }\n  }))\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"mbuxpqro\",\n    \"name\": \"description\",\n    \"type\": \"text\",\n    \"required\": true,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null,\n      \"pattern\": \"\"\n    }\n  }))\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"qe5g2utb\",\n    \"name\": \"price\",\n    \"type\": \"number\",\n    \"required\": true,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"twr8eflpoom78k9\")\n\n  // remove\n  collection.schema.removeField(\"eldb7j7p\")\n\n  // remove\n  collection.schema.removeField(\"srzysrtq\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"jhgt65nx\",\n    \"name\": \"name\",\n    \"type\": \"text\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null,\n      \"pattern\": \"\"\n    }\n  }))\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"mbuxpqro\",\n    \"name\": \"description\",\n    \"type\": \"text\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null,\n      \"pattern\": \"\"\n    }\n  }))\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"qe5g2utb\",\n    \"name\": \"price\",\n    \"type\": \"number\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null\n    }\n  }))\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1692004755_updated_plans.js",
    "content": "migrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"twr8eflpoom78k9\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"qe5g2utb\",\n    \"name\": \"price\",\n    \"type\": \"number\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"twr8eflpoom78k9\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"qe5g2utb\",\n    \"name\": \"price\",\n    \"type\": \"number\",\n    \"required\": true,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null\n    }\n  }))\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1692006976_updated_users.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"_pb_users_auth_\")\n\n  // remove\n  collection.schema.removeField(\"1hbozave\")\n\n  // remove\n  collection.schema.removeField(\"q6psj4sr\")\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"_pb_users_auth_\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"1hbozave\",\n    \"name\": \"totalScore\",\n    \"type\": \"number\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null\n    }\n  }))\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"q6psj4sr\",\n    \"name\": \"avgMinutesToSolution\",\n    \"type\": \"number\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"min\": null,\n      \"max\": null\n    }\n  }))\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1692013150_updated_users.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"_pb_users_auth_\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"qrhpiuil\",\n    \"name\": \"plan\",\n    \"type\": \"relation\",\n    \"required\": false,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"twr8eflpoom78k9\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": 1,\n      \"displayFields\": []\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"_pb_users_auth_\")\n\n  // remove\n  collection.schema.removeField(\"qrhpiuil\")\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1692013264_updated_plans.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"twr8eflpoom78k9\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"eldb7j7p\",\n    \"name\": \"features\",\n    \"type\": \"relation\",\n    \"required\": true,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"vknl4jpc8e5wlbv\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": 1,\n      \"displayFields\": [\n        \"feature\"\n      ]\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"twr8eflpoom78k9\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"eldb7j7p\",\n    \"name\": \"features\",\n    \"type\": \"relation\",\n    \"required\": true,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"vknl4jpc8e5wlbv\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": null,\n      \"displayFields\": [\n        \"feature\"\n      ]\n    }\n  }))\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1692013276_updated_plans.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"twr8eflpoom78k9\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"eldb7j7p\",\n    \"name\": \"features\",\n    \"type\": \"relation\",\n    \"required\": true,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"vknl4jpc8e5wlbv\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": null,\n      \"displayFields\": [\n        \"feature\"\n      ]\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"twr8eflpoom78k9\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"eldb7j7p\",\n    \"name\": \"features\",\n    \"type\": \"relation\",\n    \"required\": true,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"vknl4jpc8e5wlbv\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": 1,\n      \"displayFields\": [\n        \"feature\"\n      ]\n    }\n  }))\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1692013682_updated_features.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"vknl4jpc8e5wlbv\")\n\n  collection.listRule = \"\"\n  collection.viewRule = \"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"vknl4jpc8e5wlbv\")\n\n  collection.listRule = null\n  collection.viewRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1692029251_created_faqs.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const collection = new Collection({\n    \"id\": \"jibt2vep48ufd22\",\n    \"created\": \"2023-08-14 16:07:31.811Z\",\n    \"updated\": \"2023-08-14 16:07:31.811Z\",\n    \"name\": \"faqs\",\n    \"type\": \"base\",\n    \"system\": false,\n    \"schema\": [\n      {\n        \"system\": false,\n        \"id\": \"e9bldrjo\",\n        \"name\": \"question\",\n        \"type\": \"text\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {\n          \"min\": null,\n          \"max\": null,\n          \"pattern\": \"\"\n        }\n      },\n      {\n        \"system\": false,\n        \"id\": \"qrlumdkc\",\n        \"name\": \"answer\",\n        \"type\": \"text\",\n        \"required\": false,\n        \"unique\": false,\n        \"options\": {\n          \"min\": null,\n          \"max\": null,\n          \"pattern\": \"\"\n        }\n      }\n    ],\n    \"indexes\": [],\n    \"listRule\": \"\",\n    \"viewRule\": \"\",\n    \"createRule\": null,\n    \"updateRule\": null,\n    \"deleteRule\": null,\n    \"options\": {}\n  });\n\n  return Dao(db).saveCollection(collection);\n}, (db) => {\n  const dao = new Dao(db);\n  const collection = dao.findCollectionByNameOrId(\"jibt2vep48ufd22\");\n\n  return dao.deleteCollection(collection);\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1692030049_created_companies.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const collection = new Collection({\n    \"id\": \"w8voaruazjzwfdy\",\n    \"created\": \"2023-08-14 16:20:49.199Z\",\n    \"updated\": \"2023-08-14 16:20:49.199Z\",\n    \"name\": \"companies\",\n    \"type\": \"base\",\n    \"system\": false,\n    \"schema\": [\n      {\n        \"system\": false,\n        \"id\": \"ivpolooz\",\n        \"name\": \"name\",\n        \"type\": \"text\",\n        \"required\": true,\n        \"unique\": false,\n        \"options\": {\n          \"min\": null,\n          \"max\": null,\n          \"pattern\": \"\"\n        }\n      },\n      {\n        \"system\": false,\n        \"id\": \"vl0yntjz\",\n        \"name\": \"logo\",\n        \"type\": \"file\",\n        \"required\": true,\n        \"unique\": false,\n        \"options\": {\n          \"maxSelect\": 1,\n          \"maxSize\": 5242880,\n          \"mimeTypes\": [\n            \"image/jpeg\",\n            \"image/png\",\n            \"image/svg+xml\",\n            \"image/gif\",\n            \"image/webp\"\n          ],\n          \"thumbs\": [],\n          \"protected\": false\n        }\n      }\n    ],\n    \"indexes\": [],\n    \"listRule\": null,\n    \"viewRule\": null,\n    \"createRule\": null,\n    \"updateRule\": null,\n    \"deleteRule\": null,\n    \"options\": {}\n  });\n\n  return Dao(db).saveCollection(collection);\n}, (db) => {\n  const dao = new Dao(db);\n  const collection = dao.findCollectionByNameOrId(\"w8voaruazjzwfdy\");\n\n  return dao.deleteCollection(collection);\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1692030210_updated_companies.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"w8voaruazjzwfdy\")\n\n  collection.listRule = \"\"\n  collection.viewRule = \"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"w8voaruazjzwfdy\")\n\n  collection.listRule = null\n  collection.viewRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1697824916_updated_users.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"_pb_users_auth_\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"tep58ydu\",\n    \"name\": \"role\",\n    \"type\": \"select\",\n    \"required\": false,\n    \"presentable\": false,\n    \"unique\": false,\n    \"options\": {\n      \"maxSelect\": 1,\n      \"values\": [\n        \"user\",\n        \"admin\"\n      ]\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"_pb_users_auth_\")\n\n  // remove\n  collection.schema.removeField(\"tep58ydu\")\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1697824932_updated_users.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"_pb_users_auth_\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"tep58ydu\",\n    \"name\": \"role\",\n    \"type\": \"select\",\n    \"required\": true,\n    \"presentable\": false,\n    \"unique\": false,\n    \"options\": {\n      \"maxSelect\": 1,\n      \"values\": [\n        \"user\",\n        \"admin\"\n      ]\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"_pb_users_auth_\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"tep58ydu\",\n    \"name\": \"role\",\n    \"type\": \"select\",\n    \"required\": false,\n    \"presentable\": false,\n    \"unique\": false,\n    \"options\": {\n      \"maxSelect\": 1,\n      \"values\": [\n        \"user\",\n        \"admin\"\n      ]\n    }\n  }))\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1697982589_created_exercise_logs.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const collection = new Collection({\n    \"id\": \"juzwmxmth0oqv89\",\n    \"created\": \"2023-10-22 13:49:49.779Z\",\n    \"updated\": \"2023-10-22 13:49:49.779Z\",\n    \"name\": \"exercise_logs\",\n    \"type\": \"base\",\n    \"system\": false,\n    \"schema\": [\n      {\n        \"system\": false,\n        \"id\": \"qy80deun\",\n        \"name\": \"user\",\n        \"type\": \"relation\",\n        \"required\": true,\n        \"presentable\": true,\n        \"unique\": false,\n        \"options\": {\n          \"collectionId\": \"_pb_users_auth_\",\n          \"cascadeDelete\": false,\n          \"minSelect\": null,\n          \"maxSelect\": 1,\n          \"displayFields\": null\n        }\n      },\n      {\n        \"system\": false,\n        \"id\": \"c94ndl04\",\n        \"name\": \"exercise\",\n        \"type\": \"relation\",\n        \"required\": true,\n        \"presentable\": true,\n        \"unique\": false,\n        \"options\": {\n          \"collectionId\": \"s4f0lpy3ibkgfqp\",\n          \"cascadeDelete\": false,\n          \"minSelect\": null,\n          \"maxSelect\": 1,\n          \"displayFields\": null\n        }\n      },\n      {\n        \"system\": false,\n        \"id\": \"40uyszyy\",\n        \"name\": \"timestamp\",\n        \"type\": \"date\",\n        \"required\": true,\n        \"presentable\": true,\n        \"unique\": false,\n        \"options\": {\n          \"min\": \"\",\n          \"max\": \"\"\n        }\n      }\n    ],\n    \"indexes\": [],\n    \"listRule\": null,\n    \"viewRule\": null,\n    \"createRule\": null,\n    \"updateRule\": null,\n    \"deleteRule\": null,\n    \"options\": {}\n  });\n\n  return Dao(db).saveCollection(collection);\n}, (db) => {\n  const dao = new Dao(db);\n  const collection = dao.findCollectionByNameOrId(\"juzwmxmth0oqv89\");\n\n  return dao.deleteCollection(collection);\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1697982994_updated_exercise_logs.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"juzwmxmth0oqv89\")\n\n  collection.listRule = \"@request.auth.id != \\\"\\\"\"\n  collection.viewRule = \"@request.auth.id != \\\"\\\"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"juzwmxmth0oqv89\")\n\n  collection.listRule = null\n  collection.viewRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1697983253_updated_exercise_logs.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"juzwmxmth0oqv89\")\n\n  collection.name = \"exercise_session_logs\"\n\n  // remove\n  collection.schema.removeField(\"c94ndl04\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"flngas50\",\n    \"name\": \"exercise_session\",\n    \"type\": \"relation\",\n    \"required\": false,\n    \"presentable\": false,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"qj6ssich32lcxru\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": 1,\n      \"displayFields\": null\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"juzwmxmth0oqv89\")\n\n  collection.name = \"exercise_logs\"\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"c94ndl04\",\n    \"name\": \"exercise\",\n    \"type\": \"relation\",\n    \"required\": true,\n    \"presentable\": true,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"s4f0lpy3ibkgfqp\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": 1,\n      \"displayFields\": null\n    }\n  }))\n\n  // remove\n  collection.schema.removeField(\"flngas50\")\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1697983426_updated_exercise_session_logs.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"juzwmxmth0oqv89\")\n\n  collection.listRule = \"@request.auth.id != \\\"\\\" && @request.auth.role = \\\"admin\\\"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"juzwmxmth0oqv89\")\n\n  collection.listRule = \"@request.auth.id != \\\"\\\"\"\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1697983434_updated_exercise_session_logs.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"juzwmxmth0oqv89\")\n\n  collection.viewRule = \"@request.auth.id != \\\"\\\" && @request.auth.role = \\\"admin\\\"\"\n  collection.createRule = \"@request.auth.id != \\\"\\\" && @request.auth.role = \\\"admin\\\"\"\n  collection.updateRule = \"@request.auth.id != \\\"\\\" && @request.auth.role = \\\"admin\\\"\"\n  collection.deleteRule = \"@request.auth.id != \\\"\\\" && @request.auth.role = \\\"admin\\\"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"juzwmxmth0oqv89\")\n\n  collection.viewRule = \"@request.auth.id != \\\"\\\"\"\n  collection.createRule = null\n  collection.updateRule = null\n  collection.deleteRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1697983470_updated_users.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"_pb_users_auth_\")\n\n  collection.listRule = \"id = @request.auth.id || @request.auth.role = \\\"admin\\\"\"\n  collection.viewRule = \"id = @request.auth.id || @request.auth.role = \\\"admin\\\"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"_pb_users_auth_\")\n\n  collection.listRule = \"id = @request.auth.id\"\n  collection.viewRule = \"id = @request.auth.id\"\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1697983479_updated_exercise_sessions.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"qj6ssich32lcxru\")\n\n  collection.listRule = \"@request.auth.id = user.id || @request.auth.role = \\\"admin\\\"\"\n  collection.viewRule = \"@request.auth.id = user.id || @request.auth.role = \\\"admin\\\"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"qj6ssich32lcxru\")\n\n  collection.listRule = \"@request.auth.id = user.id\"\n  collection.viewRule = \"@request.auth.id = user.id\"\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1697983500_updated_lab_sessions.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  collection.listRule = \"@request.auth.id = user.id || @request.auth.role = \\\"admin\\\"\"\n  collection.viewRule = \"@request.auth.id = user.id || @request.auth.role = \\\"admin\\\"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"n7tne53bbobf2bt\")\n\n  collection.listRule = \"@request.auth.id = user.id \"\n  collection.viewRule = \"@request.auth.id = user.id \"\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1697983560_updated_exercise_session_logs.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"juzwmxmth0oqv89\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"flngas50\",\n    \"name\": \"exercise_session\",\n    \"type\": \"relation\",\n    \"required\": true,\n    \"presentable\": true,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"qj6ssich32lcxru\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": 1,\n      \"displayFields\": null\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"juzwmxmth0oqv89\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"flngas50\",\n    \"name\": \"exercise_session\",\n    \"type\": \"relation\",\n    \"required\": false,\n    \"presentable\": false,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"qj6ssich32lcxru\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": 1,\n      \"displayFields\": null\n    }\n  }))\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1697984665_updated_exercise_session_logs.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"juzwmxmth0oqv89\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"2pfxi0ho\",\n    \"name\": \"type\",\n    \"type\": \"select\",\n    \"required\": false,\n    \"presentable\": false,\n    \"unique\": false,\n    \"options\": {\n      \"maxSelect\": 1,\n      \"values\": [\n        \"start\",\n        \"end\"\n      ]\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"juzwmxmth0oqv89\")\n\n  // remove\n  collection.schema.removeField(\"2pfxi0ho\")\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1697984707_updated_exercise_session_logs.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"juzwmxmth0oqv89\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"2pfxi0ho\",\n    \"name\": \"type\",\n    \"type\": \"select\",\n    \"required\": true,\n    \"presentable\": true,\n    \"unique\": false,\n    \"options\": {\n      \"maxSelect\": 1,\n      \"values\": [\n        \"start\",\n        \"end\"\n      ]\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"juzwmxmth0oqv89\")\n\n  // update\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"2pfxi0ho\",\n    \"name\": \"type\",\n    \"type\": \"select\",\n    \"required\": false,\n    \"presentable\": false,\n    \"unique\": false,\n    \"options\": {\n      \"maxSelect\": 1,\n      \"values\": [\n        \"start\",\n        \"end\"\n      ]\n    }\n  }))\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1697986578_created_notifications.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const collection = new Collection({\n    \"id\": \"r2q6rrbyred18kq\",\n    \"created\": \"2023-10-22 14:56:18.570Z\",\n    \"updated\": \"2023-10-22 14:56:18.570Z\",\n    \"name\": \"notifications\",\n    \"type\": \"base\",\n    \"system\": false,\n    \"schema\": [\n      {\n        \"system\": false,\n        \"id\": \"1oiscevm\",\n        \"name\": \"type\",\n        \"type\": \"select\",\n        \"required\": true,\n        \"presentable\": true,\n        \"unique\": false,\n        \"options\": {\n          \"maxSelect\": 1,\n          \"values\": [\n            \"help\"\n          ]\n        }\n      },\n      {\n        \"system\": false,\n        \"id\": \"a3furdsq\",\n        \"name\": \"user\",\n        \"type\": \"relation\",\n        \"required\": true,\n        \"presentable\": true,\n        \"unique\": false,\n        \"options\": {\n          \"collectionId\": \"_pb_users_auth_\",\n          \"cascadeDelete\": false,\n          \"minSelect\": null,\n          \"maxSelect\": 1,\n          \"displayFields\": null\n        }\n      },\n      {\n        \"system\": false,\n        \"id\": \"wwksthrr\",\n        \"name\": \"done\",\n        \"type\": \"bool\",\n        \"required\": false,\n        \"presentable\": true,\n        \"unique\": false,\n        \"options\": {}\n      }\n    ],\n    \"indexes\": [],\n    \"listRule\": null,\n    \"viewRule\": null,\n    \"createRule\": null,\n    \"updateRule\": null,\n    \"deleteRule\": null,\n    \"options\": {}\n  });\n\n  return Dao(db).saveCollection(collection);\n}, (db) => {\n  const dao = new Dao(db);\n  const collection = dao.findCollectionByNameOrId(\"r2q6rrbyred18kq\");\n\n  return dao.deleteCollection(collection);\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1697987216_updated_notifications.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"r2q6rrbyred18kq\")\n\n  collection.listRule = \"@request.auth.id != \\\"\\\" && @request.auth.role = \\\"admin\\\"\"\n  collection.viewRule = \"@request.auth.id != \\\"\\\" && @request.auth.role = \\\"admin\\\"\"\n  collection.createRule = \"@request.auth.id != \\\"\\\"\"\n  collection.updateRule = \"@request.auth.id != \\\"\\\" && @request.auth.role = \\\"admin\\\"\"\n  collection.deleteRule = \"@request.auth.id != \\\"\\\" && @request.auth.role = \\\"admin\\\"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"r2q6rrbyred18kq\")\n\n  collection.listRule = null\n  collection.viewRule = null\n  collection.createRule = null\n  collection.updateRule = null\n  collection.deleteRule = null\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1697988059_updated_users.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"_pb_users_auth_\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"arqpjvok\",\n    \"name\": \"company\",\n    \"type\": \"relation\",\n    \"required\": false,\n    \"presentable\": true,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"w8voaruazjzwfdy\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": 1,\n      \"displayFields\": null\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"_pb_users_auth_\")\n\n  // remove\n  collection.schema.removeField(\"arqpjvok\")\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1697992209_updated_exercise_session_logs.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"juzwmxmth0oqv89\")\n\n  collection.listRule = \"@request.auth.id != \\\"\\\"\"\n  collection.viewRule = \"@request.auth.id != \\\"\\\"\"\n  collection.createRule = \"@request.auth.id != \\\"\\\" \"\n  collection.updateRule = \"@request.auth.id != \\\"\\\"\"\n  collection.deleteRule = \"@request.auth.id != \\\"\\\"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"juzwmxmth0oqv89\")\n\n  collection.listRule = \"@request.auth.id != \\\"\\\" && @request.auth.role = \\\"admin\\\"\"\n  collection.viewRule = \"@request.auth.id != \\\"\\\" && @request.auth.role = \\\"admin\\\"\"\n  collection.createRule = \"@request.auth.id != \\\"\\\" && @request.auth.role = \\\"admin\\\"\"\n  collection.updateRule = \"@request.auth.id != \\\"\\\" && @request.auth.role = \\\"admin\\\"\"\n  collection.deleteRule = \"@request.auth.id != \\\"\\\" && @request.auth.role = \\\"admin\\\"\"\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1697992245_updated_notifications.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"r2q6rrbyred18kq\")\n\n  collection.listRule = \"@request.auth.id != \\\"\\\"\"\n  collection.viewRule = \"@request.auth.id != \\\"\\\"\"\n  collection.updateRule = \"@request.auth.id != \\\"\\\"\"\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"r2q6rrbyred18kq\")\n\n  collection.listRule = \"@request.auth.id != \\\"\\\" && @request.auth.role = \\\"admin\\\"\"\n  collection.viewRule = \"@request.auth.id != \\\"\\\" && @request.auth.role = \\\"admin\\\"\"\n  collection.updateRule = \"@request.auth.id != \\\"\\\" && @request.auth.role = \\\"admin\\\"\"\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1697992793_updated_notifications.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"r2q6rrbyred18kq\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"qoqrd85a\",\n    \"name\": \"exercise\",\n    \"type\": \"relation\",\n    \"required\": false,\n    \"presentable\": false,\n    \"unique\": false,\n    \"options\": {\n      \"collectionId\": \"s4f0lpy3ibkgfqp\",\n      \"cascadeDelete\": false,\n      \"minSelect\": null,\n      \"maxSelect\": 1,\n      \"displayFields\": null\n    }\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"r2q6rrbyred18kq\")\n\n  // remove\n  collection.schema.removeField(\"qoqrd85a\")\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pb_migrations/1697995266_updated_users.js",
    "content": "/// <reference path=\"../pb_data/types.d.ts\" />\nmigrate((db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"_pb_users_auth_\")\n\n  // add\n  collection.schema.addField(new SchemaField({\n    \"system\": false,\n    \"id\": \"3wxrrkpg\",\n    \"name\": \"workshop\",\n    \"type\": \"bool\",\n    \"required\": false,\n    \"presentable\": true,\n    \"unique\": false,\n    \"options\": {}\n  }))\n\n  return dao.saveCollection(collection)\n}, (db) => {\n  const dao = new Dao(db)\n  const collection = dao.findCollectionByNameOrId(\"_pb_users_auth_\")\n\n  // remove\n  collection.schema.removeField(\"3wxrrkpg\")\n\n  return dao.saveCollection(collection)\n})\n"
  },
  {
    "path": "kubelab-backend/pkg/controller/exercise.go",
    "content": "package controller\n\nimport (\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/natrontech/kubelab/pkg/env\"\n\t\"github.com/natrontech/kubelab/pkg/helm\"\n\t\"github.com/natrontech/kubelab/pkg/k8s\"\n\t\"github.com/pocketbase/pocketbase\"\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\nfunc setupExerciseResources(e *core.RecordUpdateEvent, app *pocketbase.PocketBase) error {\n\texercise, err := app.Dao().FindRecordById(\"exercises\", e.Record.GetString(\"exercise\"))\n\tif err != nil {\n\t\treturn logAndReturnErr(err)\n\t}\n\n\tuser, err := app.Dao().FindRecordById(\"users\", e.Record.GetString(\"user\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnamespaceParams := k8s.NamespaceParams{\n\t\tName:       namespaceName(e, exercise.GetString(\"lab\")),\n\t\tUserRecord: user,\n\t}\n\n\tif err = k8s.CreateNamespace(namespaceParams); err != nil {\n\t\tlog.Println(err)\n\t}\n\n\tif _, err = k8s.GetPodByName(namespaceName(e, exercise.GetString(\"lab\")), \"vcluster-0\"); err != nil {\n\t\treturn logAndReturnErr(err)\n\t}\n\n\tsecret, err := k8s.GetSecretByName(namespaceName(e, exercise.GetString(\"lab\")), \"vc-vcluster\")\n\tif err != nil {\n\t\treturn logAndReturnErr(err)\n\t}\n\n\tbootstrapBody, err := fetchBodyFromURL(exercise.GetString(\"bootstrap\"))\n\tif err != nil {\n\t\treturn logAndReturnErr(err)\n\t}\n\n\tcheckBody, err := fetchBodyFromURL(exercise.GetString(\"check\"))\n\tif err != nil {\n\t\treturn logAndReturnErr(err)\n\t}\n\n\tdeploymentParams := k8s.DeploymentParams{\n\t\tName:           \"kubelab-agent-\" + exercise.Id,\n\t\tNamespace:      helm.GetNamespaceName(exercise.GetString(\"lab\"), e.Record.GetString(\"user\")),\n\t\tImage:          env.Config.KubelabImage,\n\t\tReplicas:       1,\n\t\tKubeconfig:     string(secret.Data[\"config\"]),\n\t\tBootstrap:      string(bootstrapBody),\n\t\tCheck:          string(checkBody),\n\t\tHost:           env.Config.AllowedHosts,\n\t\tUserRecord:     user,\n\t\tCodeServerPath: \"kubelab-\" + exercise.GetString(\"lab\") + \"-\" + exercise.Id + \"-\" + e.Record.GetString(\"user\"),\n\t}\n\n\t// create a new deployment\n\t_, err = k8s.CreateDeployment(deploymentParams)\n\tif err != nil {\n\t\tlog.Println(err)\n\t}\n\n\tserviceParams := k8s.ServiceParams{\n\t\tName:       \"kubelab-agent-\" + exercise.Id,\n\t\tNamespace:  helm.GetNamespaceName(exercise.GetString(\"lab\"), e.Record.GetString(\"user\")),\n\t\tPort:       8376,\n\t\tUserRecord: user,\n\t}\n\n\t// create a new service\n\t_, err = k8s.CreateService(serviceParams)\n\tif err != nil {\n\t\tlog.Println(err)\n\t}\n\n\tingressParams := k8s.IngressParams{\n\t\tName:        \"kubelab-agent-\" + exercise.Id,\n\t\tNamespace:   helm.GetNamespaceName(exercise.GetString(\"lab\"), e.Record.GetString(\"user\")),\n\t\tServiceName: \"kubelab-agent-\" + exercise.Id,\n\t\tHost:        env.Config.AllowedHosts,\n\t\tPath:        \"kubelab-\" + exercise.GetString(\"lab\") + \"-\" + exercise.Id + \"-\" + e.Record.GetString(\"user\"),\n\t\tUserRecord:  user,\n\t}\n\n\t// create a first ingress\n\tingressParams.UseFirstRule = true\n\t_, err = k8s.CreateIngress(ingressParams)\n\tif err != nil {\n\t\tlog.Println(err)\n\t}\n\n\t// create a second ingress\n\tingressParams.UseFirstRule = false\n\t_, err = k8s.CreateIngress(ingressParams)\n\tif err != nil {\n\t\tlog.Println(err)\n\t}\n\n\t// check if deployment is ready\n\terr = k8s.WaitForDeployment(helm.GetNamespaceName(exercise.GetString(\"lab\"), e.Record.GetString(\"user\")), \"kubelab-agent-\"+exercise.Id)\n\tif err != nil {\n\t\tlog.Println(err)\n\t}\n\n\t// sleep for 5 seconds\n\ttime.Sleep(5 * time.Second)\n\treturn nil\n}\n\nfunc deleteExerciseResources(e *core.RecordUpdateEvent, app *pocketbase.PocketBase) error {\n\texercise, err := app.Dao().FindRecordById(\"exercises\", e.Record.GetString(\"exercise\"))\n\tif err != nil {\n\t\treturn logAndReturnErr(err)\n\t}\n\n\tdeleteFuncs := []func(string, string) error{\n\t\tk8s.DeleteDeployment,\n\t\tk8s.DeleteService,\n\t\tk8s.DeleteIngress,\n\t}\n\n\tfor _, fn := range deleteFuncs {\n\t\tif err = fn(namespaceName(e, exercise.GetString(\"lab\")), \"kubelab-agent-\"+exercise.Id); err != nil {\n\t\t\tlog.Println(err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "kubelab-backend/pkg/controller/handler.go",
    "content": "package controller\n\nimport (\n\t\"github.com/pocketbase/pocketbase\"\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\nfunc HandleLabSessions(e *core.RecordUpdateEvent, app *pocketbase.PocketBase) error {\n\tif e.Record.GetBool(\"clusterRunning\") {\n\t\treturn deployVCluster(e, app)\n\t}\n\treturn deleteClusterResources(e, app)\n}\n\nfunc HandleExerciseSessions(e *core.RecordUpdateEvent, app *pocketbase.PocketBase) error {\n\tif e.Record.GetBool(\"agentRunning\") {\n\t\treturn setupExerciseResources(e, app)\n\t}\n\treturn deleteExerciseResources(e, app)\n}\n"
  },
  {
    "path": "kubelab-backend/pkg/controller/lab.go",
    "content": "package controller\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/natrontech/kubelab/pkg/env\"\n\t\"github.com/natrontech/kubelab/pkg/helm\"\n\t\"github.com/natrontech/kubelab/pkg/k8s\"\n\t\"github.com/natrontech/kubelab/pkg/util\"\n\t\"github.com/pocketbase/pocketbase\"\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\nfunc deployVCluster(e *core.RecordUpdateEvent, app *pocketbase.PocketBase) error {\n\thelmclient, err := helm.CreateHelmClient(e.Record.GetString(\"lab\"), e.Record.GetString(\"user\"))\n\tif err != nil {\n\t\treturn logAndReturnErr(err)\n\t}\n\n\tuser, err := app.Dao().FindRecordById(\"users\", e.Record.GetString(\"user\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err = helm.AddHelmRepositoryToClient(helmclient, \"loft-sh\", \"https://charts.loft.sh\"); err != nil {\n\t\treturn logAndReturnErr(err)\n\t}\n\n\tyamlValues, err := os.ReadFile(env.Config.VClusterValuesFilePath)\n\tif err != nil {\n\t\treturn logAndReturnErr(err)\n\t}\n\n\tlabels := map[string]string{\n\t\t\"kubelab.ch\":             e.Record.GetString(\"lab\"),\n\t\t\"kubelab.ch/userId\":      e.Record.GetString(\"user\"),\n\t\t\"kubelab.ch/username\":    user.GetString(\"username\"),\n\t\t\"kubelab.ch/displayName\": util.StringParser(user.GetString(\"name\")),\n\t}\n\n\t// add string at the end of yamlValues\n\tyamlValues = append(yamlValues, []byte(\"\\nlabels:\\n\")...)\n\tfor k, v := range labels {\n\t\tyamlValues = append(yamlValues, []byte(\"  \"+k+\": \"+v+\"\\n\")...)\n\t}\n\n\t// add string at the end of yamlValues\n\tyamlValues = append(yamlValues, []byte(\"\\npodLabels:\\n\")...)\n\tfor k, v := range labels {\n\t\tyamlValues = append(yamlValues, []byte(\"  \"+k+\": \"+v+\"\\n\")...)\n\t}\n\n\t// add string at the end of yamlValues\n\tyamlValues = append(yamlValues, []byte(\"\\ncoredns:\\n  podLabels:\\n\")...)\n\tfor k, v := range labels {\n\t\tyamlValues = append(yamlValues, []byte(\"    \"+k+\": \"+v+\"\\n\")...)\n\t}\n\n\tif _, err = helm.CreateOrUpdateHelmRelease(\n\t\thelmclient,\n\t\t\"loft-sh/vcluster\",\n\t\t\"vcluster\",\n\t\tnamespaceName(e, e.Record.GetString(\"lab\")),\n\t\tenv.Config.VClusterChartVersion,\n\t\tstring(yamlValues),\n\t); err != nil {\n\t\treturn logAndReturnErr(err)\n\t}\n\n\tif err = k8s.CreateResourceQuota(namespaceName(e, e.Record.GetString(\"lab\")), env.Config.ResourceName, env.Config.PodsLimit, env.Config.StorageLimit); err != nil {\n\t\treturn logAndReturnErr(err)\n\t}\n\n\ttime.Sleep(15 * time.Second)\n\treturn nil\n}\n\nfunc deleteClusterResources(e *core.RecordUpdateEvent, app *pocketbase.PocketBase) error {\n\tif err := k8s.DeleteNamespace(namespaceName(e, e.Record.GetString(\"lab\"))); err != nil {\n\t\tlog.Println(err)\n\t}\n\ttime.Sleep(15 * time.Second)\n\treturn nil\n}\n"
  },
  {
    "path": "kubelab-backend/pkg/controller/sessions.go",
    "content": "package controller\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase\"\n\t\"github.com/pocketbase/pocketbase/forms\"\n\t\"github.com/pocketbase/pocketbase/models\"\n)\n\nfunc AutoSessionSyncController(app *pocketbase.PocketBase) error {\n\n\t// get each user with role \"user\"\n\tusers, err := app.Dao().FindRecordsByExpr(\"users\", dbx.NewExp(\"LOWER(role) = {:role}\", dbx.Params{\"role\": \"user\"}))\n\tif err != nil {\n\t\tfmt.Println(\"Error getting users: \", err)\n\t\treturn err\n\t}\n\n\t// get each lab\n\tlabs, err := app.Dao().FindRecordsByExpr(\"labs\")\n\tif err != nil {\n\t\tfmt.Println(\"Error getting labs: \", err)\n\t\treturn err\n\t}\n\n\t// get each exercise\n\texercises, err := app.Dao().FindRecordsByExpr(\"exercises\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlabSessionsCollection, err := app.Dao().FindCollectionByNameOrId(\"lab_sessions\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\texerciseSessionsCollection, err := app.Dao().FindCollectionByNameOrId(\"exercise_sessions\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// go through each user\n\tfor _, user := range users {\n\t\t// get lab_sessions for user\n\t\tlabSessions, err := app.Dao().FindRecordsByExpr(\"lab_sessions\", dbx.NewExp(\"user = {:user}\", dbx.Params{\"user\": user.Id}))\n\t\tif err != nil {\n\t\t\tfmt.Println(\"Error getting lab sessions: \", err)\n\t\t\treturn err\n\t\t}\n\n\t\t// check if there exists for each lab a lab_session\n\t\tfor _, lab := range labs {\n\t\t\tfound := false\n\t\t\tfor _, labSession := range labSessions {\n\t\t\t\tif labSession.Get(\"lab\") == lab.Id {\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !found {\n\t\t\t\trecord := models.NewRecord(labSessionsCollection)\n\n\t\t\t\tform := forms.NewRecordUpsert(app, record)\n\n\t\t\t\tform.LoadData(map[string]any{\n\t\t\t\t\t\"user\":           user.Id,\n\t\t\t\t\t\"lab\":            lab.Id,\n\t\t\t\t\t\"clusterRunning\": false,\n\t\t\t\t})\n\n\t\t\t\terr := form.Validate()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\terr = form.Submit()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// get exercise_sessions for user\n\t\texerciseSessions, err := app.Dao().FindRecordsByExpr(\"exercise_sessions\", dbx.NewExp(\"user = {:user}\", dbx.Params{\"user\": user.Id}))\n\t\tif err != nil {\n\t\t\tfmt.Println(\"Error getting exercise sessions: \", err)\n\t\t\treturn err\n\t\t}\n\n\t\t// check if there exists for each exercise an exercise_session\n\t\tfor _, exercise := range exercises {\n\t\t\tfound := false\n\t\t\tfor _, exerciseSession := range exerciseSessions {\n\t\t\t\tif exerciseSession.Get(\"exercise\") == exercise.Id {\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !found {\n\t\t\t\trecord := models.NewRecord(exerciseSessionsCollection)\n\n\t\t\t\tform := forms.NewRecordUpsert(app, record)\n\n\t\t\t\tform.LoadData(map[string]any{\n\t\t\t\t\t\"user\":         user.Id,\n\t\t\t\t\t\"exercise\":     exercise.Id,\n\t\t\t\t\t\"agentRunning\": false,\n\t\t\t\t})\n\n\t\t\t\terr := form.Validate()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\terr = form.Submit()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// check if there exist sessions which don't belong to any lab or exercise and delete them\n\t\tfor _, labSession := range labSessions {\n\t\t\tfound := false\n\t\t\tfor _, lab := range labs {\n\t\t\t\tif labSession.Get(\"lab\") == lab.Id {\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !found {\n\t\t\t\terr := app.Dao().DeleteRecord(labSession)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor _, exerciseSession := range exerciseSessions {\n\t\t\tfound := false\n\t\t\tfor _, exercise := range exercises {\n\t\t\t\tif exerciseSession.Get(\"exercise\") == exercise.Id {\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !found {\n\t\t\t\terr := app.Dao().DeleteRecord(exerciseSession)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "kubelab-backend/pkg/controller/util.go",
    "content": "package controller\n\nimport (\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/natrontech/kubelab/pkg/helm\"\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\nfunc namespaceName(e *core.RecordUpdateEvent, lab string) string {\n\treturn helm.GetNamespaceName(lab, e.Record.GetString(\"user\"))\n}\n\nfunc logAndReturnErr(err error) error {\n\tlog.Println(err)\n\treturn err\n}\n\nfunc fetchBodyFromURL(url string) ([]byte, error) {\n\tresponse, err := http.Get(url)\n\tif err != nil {\n\t\treturn nil, logAndReturnErr(err)\n\t}\n\tdefer response.Body.Close()\n\treturn io.ReadAll(response.Body)\n}\n"
  },
  {
    "path": "kubelab-backend/pkg/env/env.go",
    "content": "package env\n\nimport (\n\t\"log\"\n\n\t\"github.com/caarlos0/env/v8\"\n)\n\ntype config struct {\n\tLocal                  bool   `env:\"LOCAL\"`\n\tKubelabImage           string `env:\"KUBELAB_AGENT_IMAGE\" envDefault:\"ghcr.io/natrontech/kubelab-agent:latest\"`\n\tCodeServerImage        string `env:\"CODE_SERVER_IMAGE\" envDefault:\"ghcr.io/natrontech/kubelab-code-server:latest\"`\n\tAllowedHosts           string `env:\"ALLOWED_HOSTS\" envDefault:\"*\"`\n\tResourceName           string `env:\"RESOURCE_NAME\" envDefault:\"kubelab\"`\n\tIngressClass           string `env:\"AGENT_INGRESS_CLASS\" envDefault:\"nginx\"`\n\tPodsLimit              string `env:\"PODS_LIMIT\" envDefault:\"70\"`\n\tStorageLimit           string `env:\"STORAGE_LIMIT\" envDefault:\"50Gi\"`\n\tVClusterChartVersion   string `env:\"VCLUSTER_CHART_VERSION\" envDefault:\"0.16.4\"`\n\tVClusterValuesFilePath string `env:\"VCLUSTER_VALUES_FILE_PATH\" envDefault:\"./vcluster-values.yaml\"`\n\tCronTick               string `env:\"CRON_TICK\" envDefault:\"* * * * *\"`\n\tTlsSecretName          string `env:\"TLS_SECRET_NAME\" envDefault:\"kubelab-ch-wildcard-cert\"`\n}\n\nvar Config config\n\nfunc Init() {\n\tif err := env.Parse(&Config); err != nil {\n\t\tlog.Printf(\"%+v\\n\", err)\n\t}\n\n\tif Config.Local {\n\t\tlog.Println(\"Running in local mode\")\n\t}\n}\n"
  },
  {
    "path": "kubelab-backend/pkg/helm/helm.go",
    "content": "package helm\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"time\"\n\n\thelmclient \"github.com/mittwald/go-helm-client\"\n\t\"github.com/natrontech/kubelab/pkg/util\"\n\t\"helm.sh/helm/v3/pkg/release\"\n\t\"helm.sh/helm/v3/pkg/repo\"\n)\n\nconst Prefix = \"kubelab\"\n\nfunc GetNamespaceName(labName, username string) string {\n\treturn Prefix + \"-\" + util.StringParser(labName) + \"-\" + util.StringParser(username)\n}\n\nfunc CreateHelmClient(labName, username string) (helmclient.Client, error) {\n\topt := &helmclient.Options{\n\t\tNamespace: GetNamespaceName(labName, username),\n\t\tDebug:     true,\n\t\tLinting:   true,\n\t\tDebugLog:  func(format string, v ...interface{}) {},\n\t}\n\n\treturn helmclient.New(opt)\n}\n\nfunc AddHelmRepositoryToClient(helmClient helmclient.Client, repositoryName, repositoryURL string) error {\n\tchartRepo := repo.Entry{\n\t\tName: strings.ToLower(repositoryName),\n\t\tURL:  repositoryURL,\n\t}\n\n\treturn helmClient.AddOrUpdateChartRepo(chartRepo)\n}\n\nfunc CreateOrUpdateHelmRelease(helmClient helmclient.Client, chartName, releaseName, namespace, version, valuesYaml string) (rel *release.Release, err error) {\n\tchartSpec := helmclient.ChartSpec{\n\t\tChartName:       strings.ToLower(chartName),\n\t\tReleaseName:     strings.ToLower(releaseName),\n\t\tNamespace:       namespace,\n\t\tCreateNamespace: true,\n\t\tTimeout:         32 * time.Second,\n\t\tVersion:         version,\n\t\tValuesYaml:      valuesYaml,\n\t}\n\n\trel, err = helmClient.InstallOrUpgradeChart(context.Background(), &chartSpec, nil)\n\treturn\n}\n\nfunc GetHelmRelease(helmClient helmclient.Client, releaseName string) (*release.Release, error) {\n\treturn helmClient.GetRelease(releaseName)\n}\n"
  },
  {
    "path": "kubelab-backend/pkg/k8s/config.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"path/filepath\"\n\n\t\"github.com/natrontech/kubelab/pkg/env\"\n\t\"k8s.io/client-go/discovery\"\n\t\"k8s.io/client-go/kubernetes\"\n\t\"k8s.io/client-go/rest\"\n\t\"k8s.io/client-go/tools/clientcmd\"\n\t\"k8s.io/client-go/util/homedir\"\n)\n\nvar (\n\tClientset       *kubernetes.Clientset\n\tKubeconfig      *rest.Config\n\tDiscoveryClient *discovery.DiscoveryClient\n\tCtx             context.Context\n)\n\nfunc Init() {\n\tvar err error\n\tif env.Config.Local {\n\t\tvar kubeconfig *string\n\t\tif home := homedir.HomeDir(); home != \"\" {\n\t\t\tkubeconfig = flag.String(\"kubeconfig\", filepath.Join(home, \".kube\", \"config\"), \"(optional) absolute path to the kubeconfig file\")\n\t\t} else {\n\t\t\tkubeconfig = flag.String(\"kubeconfig\", \"\", \"absolute path to the kubeconfig file\")\n\t\t}\n\t\tflag.Parse()\n\n\t\tKubeconfig, err = clientcmd.BuildConfigFromFlags(\"\", *kubeconfig)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tDiscoveryClient, err = discovery.NewDiscoveryClientForConfig(Kubeconfig)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tClientset, err = kubernetes.NewForConfig(Kubeconfig)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tCtx = context.Background()\n\t} else {\n\t\tKubeconfig, err = rest.InClusterConfig()\n\t\tif err != nil {\n\t\t\tpanic(err.Error())\n\t\t}\n\n\t\tDiscoveryClient, err = discovery.NewDiscoveryClientForConfig(Kubeconfig)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tClientset, err = kubernetes.NewForConfig(Kubeconfig)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tCtx = context.Background()\n\t}\n}\n\nfunc GetClusterVersion() (string, error) {\n\tif DiscoveryClient != nil {\n\t\tclusterVersion, err := DiscoveryClient.ServerVersion()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn clusterVersion.GitVersion, nil\n\t}\n\n\treturn \"unknown\", nil\n}\n\nfunc GetClusterApi() (string, error) {\n\tvar clusterName string\n\n\tif Kubeconfig != nil {\n\t\tclusterName = Kubeconfig.Host\n\t} else {\n\t\tclusterName = \"unknown\"\n\t}\n\n\treturn clusterName, nil\n}\n"
  },
  {
    "path": "kubelab-backend/pkg/k8s/deployment.go",
    "content": "package k8s\n\nimport (\n\t\"log\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/natrontech/kubelab/pkg/env\"\n\t\"github.com/natrontech/kubelab/pkg/util\"\n\t\"github.com/pocketbase/pocketbase/models\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/utils/pointer\"\n)\n\ntype DeploymentParams struct {\n\tName           string\n\tNamespace      string\n\tImage          string\n\tReplicas       int32\n\tKubeconfig     string\n\tBootstrap      string\n\tCheck          string\n\tHost           string\n\tUserRecord     *models.Record\n\tCodeServerPath string\n}\n\nfunc CreateDeployment(params DeploymentParams) (*appsv1.Deployment, error) {\n\tparams.Kubeconfig = strings.Replace(params.Kubeconfig, \"localhost:8443\", \"vcluster:443\", -1)\n\n\tcreateConfigMap(params.Namespace, \"kubeconfig\", map[string]string{\"config\": params.Kubeconfig})\n\tcreateConfigMap(params.Namespace, \"scripts-\"+params.Name, map[string]string{\n\t\t\"check.sh\":     params.Check,\n\t\t\"bootstrap.sh\": params.Bootstrap,\n\t})\n\n\tdeployment := constructDeployment(params.Name, params.Namespace, params.Image, params.Replicas, params.Host, params.UserRecord, params.CodeServerPath)\n\tdeployed, err := Clientset.AppsV1().Deployments(params.Namespace).Create(Ctx, deployment, metav1.CreateOptions{})\n\tif err != nil {\n\t\tlog.Println(err)\n\t}\n\treturn deployed, nil\n}\n\nfunc createConfigMap(namespace, name string, data map[string]string) {\n\tconfigMap := &v1.ConfigMap{\n\t\tObjectMeta: metav1.ObjectMeta{Name: name},\n\t\tData:       data,\n\t}\n\tif _, err := Clientset.CoreV1().ConfigMaps(namespace).Create(Ctx, configMap, metav1.CreateOptions{}); err != nil {\n\t\tlog.Println(err)\n\t}\n}\n\nfunc constructDeployment(name, namespace, image string, replicas int32, host string, userRecord *models.Record, codeServerPath string) *appsv1.Deployment {\n\tscriptVolumeName := \"scripts-\" + name\n\treturn &appsv1.Deployment{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:      name,\n\t\t\tNamespace: namespace,\n\t\t},\n\t\tSpec: appsv1.DeploymentSpec{\n\t\t\tReplicas: &replicas,\n\t\t\tSelector: &metav1.LabelSelector{\n\t\t\t\tMatchLabels: map[string]string{\n\t\t\t\t\t\"kubelab.ch\":             name,\n\t\t\t\t\t\"kubelab.ch/userId\":      userRecord.GetString(\"id\"),\n\t\t\t\t\t\"kubelab.ch/username\":    userRecord.GetString(\"username\"),\n\t\t\t\t\t\"kubelab.ch/displayName\": util.StringParser(userRecord.GetString(\"name\")),\n\t\t\t\t},\n\t\t\t},\n\t\t\tTemplate: v1.PodTemplateSpec{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\t\"kubelab.ch\":                  name,\n\t\t\t\t\t\t\"kubelab.ch/userId\":           userRecord.GetString(\"id\"),\n\t\t\t\t\t\t\"kubelab.ch/username\":         userRecord.GetString(\"username\"),\n\t\t\t\t\t\t\"kubelab.ch/displayName\":      util.StringParser(userRecord.GetString(\"name\")),\n\t\t\t\t\t\t\"vcluster.loft.sh/managed-by\": \"vcluster\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tSpec: v1.PodSpec{\n\t\t\t\t\tInitContainers: []v1.Container{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:    \"init-copy-chmod-chown\",\n\t\t\t\t\t\t\tImage:   \"busybox\",\n\t\t\t\t\t\t\tCommand: []string{\"sh\", \"-c\", \"cp /config/config /config-writable/kubeconfig && chmod 0600 /config-writable/kubeconfig && chown 1001:1001 /config-writable/kubeconfig && cp /scripts-source/* /scripts-writable && chmod 0700 /scripts-writable/* && chown 1001:1001 /scripts-writable/*\"},\n\t\t\t\t\t\t\tVolumeMounts: []v1.VolumeMount{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tName:      \"kubeconfig\",\n\t\t\t\t\t\t\t\t\tMountPath: \"/config\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tName:      \"kubeconfig-writable\",\n\t\t\t\t\t\t\t\t\tMountPath: \"/config-writable\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tName:      scriptVolumeName,  // The name of the ConfigMap volume\n\t\t\t\t\t\t\t\t\tMountPath: \"/scripts-source\", // Source directory for the scripts\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tName:      scriptVolumeName + \"-writable\", // The writable directory\n\t\t\t\t\t\t\t\t\tMountPath: \"/scripts-writable\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:    \"init-kubelab-agent-home\",\n\t\t\t\t\t\t\tImage:   image, // using the same image as kubelab-container\n\t\t\t\t\t\t\tCommand: []string{\"sh\", \"-c\", \"mkdir -p /shared-kubelab-agent-home && cp -r /home/kubelab-agent/. /shared-kubelab-agent-home/\"},\n\t\t\t\t\t\t\tSecurityContext: &v1.SecurityContext{\n\t\t\t\t\t\t\t\tRunAsUser:  pointer.Int64(1001),\n\t\t\t\t\t\t\t\tRunAsGroup: pointer.Int64(1001),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tVolumeMounts: []v1.VolumeMount{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tName:      \"kubelab-agent-home\",\n\t\t\t\t\t\t\t\t\tMountPath: \"/shared-kubelab-agent-home\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tContainers: []v1.Container{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"kubelab-container\",\n\t\t\t\t\t\t\tImage: image,\n\t\t\t\t\t\t\tPorts: []v1.ContainerPort{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tContainerPort: 8376,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t// --allowed-hostnames\n\t\t\t\t\t\t\tArgs: []string{\"--allowed-hostnames\", \"*,\" + host},\n\t\t\t\t\t\t\tVolumeMounts: []v1.VolumeMount{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tName:      \"kubeconfig-writable\",\n\t\t\t\t\t\t\t\t\tMountPath: \"/home/kubelab-agent/.kube/config\",\n\t\t\t\t\t\t\t\t\tSubPath:   \"kubeconfig\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tName:      scriptVolumeName + \"-writable\",\n\t\t\t\t\t\t\t\t\tMountPath: \"/scripts\",\n\t\t\t\t\t\t\t\t\tReadOnly:  true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tName:      \"kubelab-agent-home\",\n\t\t\t\t\t\t\t\t\tMountPath: \"/home/kubelab-agent\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"code-server\",\n\t\t\t\t\t\t\tImage: env.Config.CodeServerImage,\n\t\t\t\t\t\t\tEnv: []v1.EnvVar{\n\t\t\t\t\t\t\t\t// {\n\t\t\t\t\t\t\t\t// \tName:  \"PUID\",\n\t\t\t\t\t\t\t\t// \tValue: \"1001\",\n\t\t\t\t\t\t\t\t// },\n\t\t\t\t\t\t\t\t// {\n\t\t\t\t\t\t\t\t// \tName:  \"PGID\",\n\t\t\t\t\t\t\t\t// \tValue: \"1001\",\n\t\t\t\t\t\t\t\t// },\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tName:  \"DEFAULT_WORKSPACE\",\n\t\t\t\t\t\t\t\t\tValue: \"/home/kubelab-agent/exercise\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t// {\n\t\t\t\t\t\t\t\t// \tName:  \"PROXY_DOMAIN\",\n\t\t\t\t\t\t\t\t// \tValue: codeServerPath + \".\" + host,\n\t\t\t\t\t\t\t\t// },\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tName:  \"CS_DISABLE_PROXY\",\n\t\t\t\t\t\t\t\t\tValue: \"true\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tPorts: []v1.ContainerPort{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tContainerPort: 8443,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tVolumeMounts: []v1.VolumeMount{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tName:      \"kubeconfig-writable\",\n\t\t\t\t\t\t\t\t\tMountPath: \"/config/.kube/config\",\n\t\t\t\t\t\t\t\t\tSubPath:   \"kubeconfig\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tName:      scriptVolumeName + \"-writable\",\n\t\t\t\t\t\t\t\t\tMountPath: \"/scripts\",\n\t\t\t\t\t\t\t\t\tReadOnly:  true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tName:      \"kubelab-agent-home\",\n\t\t\t\t\t\t\t\t\tMountPath: \"/home/kubelab-agent\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tVolumes: []v1.Volume{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"kubeconfig\",\n\t\t\t\t\t\t\tVolumeSource: v1.VolumeSource{\n\t\t\t\t\t\t\t\tConfigMap: &v1.ConfigMapVolumeSource{\n\t\t\t\t\t\t\t\t\tLocalObjectReference: v1.LocalObjectReference{\n\t\t\t\t\t\t\t\t\t\tName: \"kubeconfig\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"kubeconfig-writable\",\n\t\t\t\t\t\t\tVolumeSource: v1.VolumeSource{\n\t\t\t\t\t\t\t\tEmptyDir: &v1.EmptyDirVolumeSource{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: scriptVolumeName,\n\t\t\t\t\t\t\tVolumeSource: v1.VolumeSource{\n\t\t\t\t\t\t\t\tConfigMap: &v1.ConfigMapVolumeSource{\n\t\t\t\t\t\t\t\t\tLocalObjectReference: v1.LocalObjectReference{\n\t\t\t\t\t\t\t\t\t\tName: scriptVolumeName,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: scriptVolumeName + \"-writable\",\n\t\t\t\t\t\t\tVolumeSource: v1.VolumeSource{\n\t\t\t\t\t\t\t\tEmptyDir: &v1.EmptyDirVolumeSource{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"kubelab-agent-home\",\n\t\t\t\t\t\t\tVolumeSource: v1.VolumeSource{\n\t\t\t\t\t\t\t\tEmptyDir: &v1.EmptyDirVolumeSource{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc DeleteDeployment(namespace, name string) error {\n\tdeleteConfigMap(namespace, \"kubeconfig\")\n\tdeleteConfigMap(namespace, \"scripts-\"+name)\n\n\treturn Clientset.AppsV1().Deployments(namespace).Delete(Ctx, name, metav1.DeleteOptions{})\n}\n\nfunc deleteConfigMap(namespace, name string) {\n\tif err := Clientset.CoreV1().ConfigMaps(namespace).Delete(Ctx, name, metav1.DeleteOptions{}); err != nil {\n\t\tlog.Println(err)\n\t}\n}\n\nfunc WaitForDeployment(namespace, name string) error {\n\tfor {\n\t\tdeployment, err := Clientset.AppsV1().Deployments(namespace).Get(Ctx, name, metav1.GetOptions{})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif deployment.Status.ReadyReplicas == *deployment.Spec.Replicas {\n\t\t\treturn nil\n\t\t}\n\t\ttime.Sleep(1 * time.Second)\n\t}\n}\n"
  },
  {
    "path": "kubelab-backend/pkg/k8s/ingress.go",
    "content": "package k8s\n\nimport (\n\t\"github.com/natrontech/kubelab/pkg/env\"\n\t\"github.com/natrontech/kubelab/pkg/util\"\n\t\"github.com/pocketbase/pocketbase/models\"\n\tnetworkingv1 \"k8s.io/api/networking/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\ntype IngressParams struct {\n\tNamespace    string\n\tName         string\n\tHost         string\n\tServiceName  string\n\tPath         string\n\tUserRecord   *models.Record\n\tUseFirstRule bool\n}\n\n// create a ingress with a hostpath with the namespace name pointed to a service\nfunc CreateIngress(params IngressParams) (*networkingv1.Ingress, error) {\n\n\t// Annotations common to both rules\n\tannotations := map[string]string{\n\t\t\"nginx.ingress.kubernetes.io/affinity\":                    \"cookie\",\n\t\t\"nginx.ingress.kubernetes.io/proxy-connect-timeout\":       \"3600\",\n\t\t\"nginx.ingress.kubernetes.io/proxy-next-upstream-timeout\": \"3600\",\n\t\t\"nginx.ingress.kubernetes.io/proxy-read-timeout\":          \"3600\",\n\t\t\"nginx.ingress.kubernetes.io/proxy-send-timeout\":          \"3600\",\n\t\t\"nginx.ingress.kubernetes.io/session-cookie-expires\":      \"172800\",\n\t\t\"nginx.ingress.kubernetes.io/session-cookie-max-age\":      \"172800\",\n\t\t\"nginx.ingress.kubernetes.io/session-cookie-name\":         \"route\",\n\t\t\"nginx.ingress.kubernetes.io/websocket-services\":          params.ServiceName,\n\t\t\"nginx.org/websocket-services\":                            params.ServiceName,\n\t}\n\n\tvar rules []networkingv1.IngressRule\n\tif params.UseFirstRule {\n\t\tannotations[\"nginx.ingress.kubernetes.io/rewrite-target\"] = \"/$2\"\n\t\trules = append(rules, networkingv1.IngressRule{\n\t\t\tHost: params.Host,\n\t\t\tIngressRuleValue: networkingv1.IngressRuleValue{\n\t\t\t\tHTTP: &networkingv1.HTTPIngressRuleValue{\n\t\t\t\t\tPaths: []networkingv1.HTTPIngressPath{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPath:     \"/\" + params.Path + \"(/|$)(.*)\",\n\t\t\t\t\t\t\tPathType: func() *networkingv1.PathType { p := networkingv1.PathTypeImplementationSpecific; return &p }(),\n\t\t\t\t\t\t\tBackend: networkingv1.IngressBackend{\n\t\t\t\t\t\t\t\tService: &networkingv1.IngressServiceBackend{\n\t\t\t\t\t\t\t\t\tName: params.ServiceName,\n\t\t\t\t\t\t\t\t\tPort: networkingv1.ServiceBackendPort{\n\t\t\t\t\t\t\t\t\t\tNumber: 8376,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t} else {\n\t\trules = append(rules, networkingv1.IngressRule{\n\t\t\tHost: params.Path + \".\" + params.Host,\n\t\t\tIngressRuleValue: networkingv1.IngressRuleValue{\n\t\t\t\tHTTP: &networkingv1.HTTPIngressRuleValue{\n\t\t\t\t\tPaths: []networkingv1.HTTPIngressPath{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPath:     \"/\",\n\t\t\t\t\t\t\tPathType: func() *networkingv1.PathType { p := networkingv1.PathTypePrefix; return &p }(),\n\t\t\t\t\t\t\tBackend: networkingv1.IngressBackend{\n\t\t\t\t\t\t\t\tService: &networkingv1.IngressServiceBackend{\n\t\t\t\t\t\t\t\t\tName: params.ServiceName,\n\t\t\t\t\t\t\t\t\tPort: networkingv1.ServiceBackendPort{\n\t\t\t\t\t\t\t\t\t\tNumber: 8443,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t}\n\n\tingress := &networkingv1.Ingress{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName: params.Name + (func() string {\n\t\t\t\tif params.UseFirstRule {\n\t\t\t\t\treturn \"-first-rule\"\n\t\t\t\t} else {\n\t\t\t\t\treturn \"-second-rule\"\n\t\t\t\t}\n\t\t\t})(),\n\t\t\tNamespace:   params.Namespace,\n\t\t\tAnnotations: annotations,\n\t\t\tLabels: map[string]string{\n\t\t\t\t\"kubelab.ch\":             params.Name,\n\t\t\t\t\"kubelab.ch/userId\":      params.UserRecord.GetString(\"id\"),\n\t\t\t\t\"kubelab.ch/username\":    params.UserRecord.GetString(\"username\"),\n\t\t\t\t\"kubelab.ch/displayName\": util.StringParser(params.UserRecord.GetString(\"name\")),\n\t\t\t},\n\t\t},\n\t\tSpec: networkingv1.IngressSpec{\n\t\t\tIngressClassName: func() *string { s := env.Config.IngressClass; return &s }(),\n\t\t\tTLS: []networkingv1.IngressTLS{\n\t\t\t\t{\n\t\t\t\t\tSecretName: env.Config.TlsSecretName,\n\t\t\t\t},\n\t\t\t},\n\t\t\tRules: rules,\n\t\t},\n\t}\n\n\treturn Clientset.NetworkingV1().Ingresses(params.Namespace).Create(Ctx, ingress, metav1.CreateOptions{})\n}\n\nfunc DeleteIngress(namespace string, name string) error {\n\t// Delete the ingress for the first rule\n\terr := Clientset.NetworkingV1().Ingresses(namespace).Delete(Ctx, name+\"-first-rule\", metav1.DeleteOptions{})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Delete the ingress for the second rule\n\terr = Clientset.NetworkingV1().Ingresses(namespace).Delete(Ctx, name+\"-second-rule\", metav1.DeleteOptions{})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "kubelab-backend/pkg/k8s/namespace.go",
    "content": "package k8s\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/natrontech/kubelab/pkg/util\"\n\t\"github.com/pocketbase/pocketbase/models\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nfunc GetTotalNamespaces() (string, error) {\n\n\tif Clientset != nil {\n\t\tnamespaces, err := Clientset.CoreV1().Namespaces().List(Ctx, metav1.ListOptions{})\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\ttotalNamespaces := len(namespaces.Items)\n\t\treturn strconv.Itoa(totalNamespaces), nil\n\t}\n\n\treturn \"unknown\", nil\n}\n\ntype NamespaceParams struct {\n\tName       string\n\tUserRecord *models.Record\n}\n\nfunc CreateNamespace(params NamespaceParams) error {\n\tns := &v1.Namespace{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName: params.Name,\n\t\t\tLabels: map[string]string{\n\t\t\t\t\"kubelab.ch\":             params.Name,\n\t\t\t\t\"kubelab.ch/userId\":      params.UserRecord.GetString(\"id\"),\n\t\t\t\t\"kubelab.ch/username\":    params.UserRecord.GetString(\"username\"),\n\t\t\t\t\"kubelab.ch/displayName\": util.StringParser(params.UserRecord.GetString(\"name\")),\n\t\t\t},\n\t\t},\n\t}\n\t_, err := Clientset.CoreV1().Namespaces().Create(Ctx, ns, metav1.CreateOptions{})\n\n\t// if err already exists, update\n\tif err != nil && strings.Contains(err.Error(), \"already exists\") {\n\t\t_, err = Clientset.CoreV1().Namespaces().Update(Ctx, ns, metav1.UpdateOptions{})\n\t}\n\n\treturn err\n}\n\nfunc DeleteNamespace(namespace string) error {\n\treturn Clientset.CoreV1().Namespaces().Delete(Ctx, namespace, metav1.DeleteOptions{})\n}\n\nfunc GetTotalNamespacesByPrefix(prefix string) (int, error) {\n\n\tif Clientset != nil {\n\t\tnamespaces, err := Clientset.CoreV1().Namespaces().List(Ctx, metav1.ListOptions{})\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\tvar totalNamespaces int\n\t\t// check if prefix of namespace is prefix\n\t\tfor _, namespace := range namespaces.Items {\n\t\t\t// get name of namespace\n\t\t\tname := namespace.GetName()\n\t\t\t// check if namespace contains prefix\n\t\t\tif strings.Contains(name, prefix) {\n\t\t\t\ttotalNamespaces++\n\t\t\t}\n\t\t}\n\t\treturn totalNamespaces, nil\n\t}\n\n\treturn 0, nil\n}\n"
  },
  {
    "path": "kubelab-backend/pkg/k8s/pod.go",
    "content": "package k8s\n\nimport (\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nfunc GetPodByName(namespace string, name string) (*v1.Pod, error) {\n\tif Clientset != nil {\n\t\tpod, err := Clientset.CoreV1().Pods(namespace).Get(Ctx, name, metav1.GetOptions{})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn pod, nil\n\t}\n\treturn nil, nil\n}\n"
  },
  {
    "path": "kubelab-backend/pkg/k8s/resourcequota.go",
    "content": "package k8s\n\nimport (\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/api/resource\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nfunc CreateResourceQuota(namespace string, name string, pods string, storage string) error {\n\tresourceQuota := &v1.ResourceQuota{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:      name,\n\t\t\tNamespace: namespace,\n\t\t},\n\t\tSpec: v1.ResourceQuotaSpec{\n\t\t\tHard: v1.ResourceList{\n\t\t\t\tv1.ResourcePods:            resource.MustParse(pods),\n\t\t\t\tv1.ResourceRequestsStorage: resource.MustParse(storage),\n\t\t\t},\n\t\t},\n\t}\n\n\t_, err := Clientset.CoreV1().ResourceQuotas(namespace).Create(Ctx, resourceQuota, metav1.CreateOptions{})\n\treturn err\n}\n"
  },
  {
    "path": "kubelab-backend/pkg/k8s/secret.go",
    "content": "package k8s\n\nimport (\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nfunc GetSecretByName(namespace string, name string) (*v1.Secret, error) {\n\tif Clientset != nil {\n\t\tsecret, err := Clientset.CoreV1().Secrets(namespace).Get(Ctx, name, metav1.GetOptions{})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn secret, nil\n\t}\n\treturn nil, nil\n}\n"
  },
  {
    "path": "kubelab-backend/pkg/k8s/service.go",
    "content": "package k8s\n\nimport (\n\t\"github.com/natrontech/kubelab/pkg/util\"\n\t\"github.com/pocketbase/pocketbase/models\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\ntype ServiceParams struct {\n\tNamespace  string\n\tName       string\n\tPort       int32\n\tUserRecord *models.Record\n}\n\nfunc CreateService(params ServiceParams) (*v1.Service, error) {\n\tservice := &v1.Service{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:      params.Name,\n\t\t\tNamespace: params.Namespace,\n\t\t\tLabels: map[string]string{\n\t\t\t\t\"kubelab.ch\":             params.Name,\n\t\t\t\t\"kubelab.ch/userId\":      params.UserRecord.GetString(\"id\"),\n\t\t\t\t\"kubelab.ch/username\":    params.UserRecord.GetString(\"username\"),\n\t\t\t\t\"kubelab.ch/displayName\": util.StringParser(params.UserRecord.GetString(\"name\")),\n\t\t\t},\n\t\t},\n\t\tSpec: v1.ServiceSpec{\n\t\t\tSelector: map[string]string{\n\t\t\t\t\"kubelab.ch\": params.Name,\n\t\t\t},\n\t\t\tPorts: []v1.ServicePort{\n\t\t\t\t{\n\t\t\t\t\tName: \"main-port\", // You can name this appropriately\n\t\t\t\t\tPort: params.Port,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName: \"code-server-port\", // Name for the code-server port\n\t\t\t\t\tPort: 8443,               // Assuming code-server runs on port 8080\n\t\t\t\t},\n\t\t\t},\n\t\t\tType: v1.ServiceTypeClusterIP,\n\t\t},\n\t}\n\n\treturn Clientset.CoreV1().Services(params.Namespace).Create(Ctx, service, metav1.CreateOptions{})\n}\n\nfunc DeleteService(namespace string, name string) error {\n\treturn Clientset.CoreV1().Services(namespace).Delete(Ctx, name, metav1.DeleteOptions{})\n}\n"
  },
  {
    "path": "kubelab-backend/pkg/util/helpers.go",
    "content": "package util\n\nimport \"strings\"\n\nvar (\n\tSpecialCharacters string = \"!@#$%^&*()_+-=[]{}|;':,./<>?`~\"\n)\n\nfunc StringParser(s string) string {\n\treturnString := strings.ToLower(s)\n\treturnString = strings.Replace(returnString, \" \", \"-\", -1)\n\t// remove special characters from returnString\n\tfor _, c := range SpecialCharacters {\n\t\treturnString = strings.Replace(returnString, string(c), \"\", -1)\n\t}\n\treturn returnString\n}\n"
  },
  {
    "path": "kubelab-backend/vcluster-values.yaml",
    "content": "sync:\n  persistentvolumes:\n    enabled: true\n  storageclasses:\n    enabled: false\n  ingresses:\n    enabled: true\n  hoststorageclasses:\n    enabled: true\n  events:\n    enabled: false\nvcluster:\n  image: rancher/k3s:v1.28.7-k3s1\nstorage:\n  persistence: false\nisolation:\n  enabled: true\n  podSecurityStandard: privileged\n  nodeProxyPermission:\n    enabled: false\n  resourceQuota:\n    enabled: false\n  limitRange:\n    enabled: false\n  networkPolicy:\n    enabled: false\n    outgoingConnections:\n      ipBlock:\n        cidr: 8.8.8.8/32\n        except: []\nsyncer:\n  kubeConfigContextName: \"kubelab-vcluster\"\n"
  },
  {
    "path": "kubelab-fill/example_users.csv",
    "content": "firstname,lastname,email,password\n"
  },
  {
    "path": "kubelab-fill/upload.py",
    "content": "import json\nimport os\nimport requests\n\n\nclass Lab:\n    def __init__(self, title, description, docs):\n        self.title = title\n        self.description = description\n        self.docs = docs\n\n\nclass Exercise:\n    def __init__(self, title, description, docs, hint, solution, check, bootstrap, lab):\n        self.title = title\n        self.description = description\n        self.docs = docs\n        self.hint = hint\n        self.solution = solution\n        self.check = check\n        self.bootstrap = bootstrap\n        self.lab = lab\n\n\ndef post_request(url, data):\n    try:\n        headers = {'Content-type': 'application/json'}\n        response = requests.post(url, data=json.dumps(data), headers=headers)\n        return response\n    except requests.exceptions.RequestException as e:\n        print(e)\n\n\ndef main():\n    # from .env import labs_upload_url, exercises_upload_url\n    labs_upload_url = os.environ['LABS_UPLOAD_URL']\n    exercises_upload_url = os.environ['EXERCISES_UPLOAD_URL']\n    prefix = os.environ['URL_PREFIX']\n    path = os.environ['WORSHOP_DIR_PATH']\n\n    labs = get_labs_from_dir(path)\n    exercises = get_exercises_from_dir(path)\n\n    # sort labs and exercises\n    labs.sort(key=lambda x: x.title)\n    exercises.sort(key=lambda x: x.title)\n\n    for lab in labs:\n        data = {\n            'title': lab.title,\n            'description': lab.description,\n            'docs': prefix + lab.docs\n        }\n        response = post_request(labs_upload_url, data)\n        if response.status_code == 200:\n            print(\"Lab {} uploaded successfully\".format(lab.title))\n            # convert response.content bytes to json\n            labId = json.loads(response.text)['id']\n            print(\"Lab {} id: {}\".format(lab.title, labId))\n            for exercise in exercises:\n                if exercise.lab == lab.title:\n                    data = {\n                        'title': exercise.title,\n                        'description': exercise.description,\n                        'docs': prefix + exercise.docs,\n                        'hint': prefix + exercise.hint,\n                        'solution': prefix + exercise.solution,\n                        'check': prefix + exercise.check,\n                        'bootstrap': prefix + exercise.bootstrap,\n                        'lab': labId\n                    }\n                    response = post_request(exercises_upload_url, data)\n                    if response.status_code == 200:\n                        print(\"Exercise {} uploaded successfully\".format(exercise.title))\n                    else:\n                        print(\"Exercise {} upload failed\".format(exercise.title))\n\n\n        else:\n            print(\"Lab {} upload failed\".format(lab.title))\n\n\n\ndef get_labs_from_dir(path):\n    labs = []\n    for name in os.listdir(path):\n        if os.path.isdir(os.path.join(path, name)):\n            prename = name\n            number = name.split(\"_\")[0]\n            name = name.split(\"_\")[1:]\n            name = \" \".join(name)\n            name = number+\" \"+name.title()\n            labs.append(\n                Lab(\n                    title=name,\n                    description=name,\n                    docs=prename+\"/docs.md\"\n                )\n            )\n    return labs\n\n\ndef get_exercises_from_dir(path, level=1, parent=None):\n    exercises = []\n    for name in os.listdir(path):\n        full_path = os.path.join(path, name)\n        if os.path.isdir(full_path):\n            if level == 2:\n                prename = name\n                number = name.split(\"_\")[0]\n                name = name.split(\"_\")[1:]\n                name = \" \".join(name)\n                name = number+\" \"+name.title()\n                exercises.append(\n                    Exercise(\n                        title=name,\n                        description=name,\n                        docs=parent+\"/\"+prename+\"/docs.md\",\n                        hint=parent+\"/\"+prename+\"/hint.md\",\n                        solution=parent+\"/\"+prename+\"/solution.md\",\n                        check=parent+\"/\"+prename+\"/check.sh\",\n                        bootstrap=parent+\"/\"+prename+\"/bootstrap.sh\",\n                        lab=parse_name(parent)\n                    )\n                )\n            exercises.extend(get_exercises_from_dir(full_path, level + 1, parent=name))\n    return exercises\n\ndef parse_name(name):\n    name = name.split(\"_\")\n    number = name[0]\n    name = name[1:]\n    name = \" \".join(name)\n    name = number+\" \"+name.title()\n    return name\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "kubelab-fill/users_import.py",
    "content": "\n\nimport csv\nimport json\nimport os\nimport requests\n\nclass User:\n    def __init__(self, email, password, passwordConfirm, name):\n        self.email = email\n        self.password = password\n        self.passwordConfirm = passwordConfirm\n        self.name = name\n\n\ndef post_request(url, data):\n    try:\n        headers = {'Content-type': 'application/json'}\n        response = requests.post(url, data=json.dumps(data), headers=headers)\n        return response\n    except requests.exceptions.RequestException as e:\n        print(e)\n\ndef main():\n    user_upload_url = os.environ['USER_UPLOAD_URL']\n    path = os.environ['CSV_PATH']\n\n    users = get_users_from_csv(path)\n\n    for user in users:\n        data = vars(user)\n        response = post_request(user_upload_url, data)\n        if response.status_code == 200:\n            print(\"User {} uploaded successfully\".format(user.name))\n            # convert response.content bytes to json\n            userId = json.loads(response.text)['id']\n            print(\"User {} id: {}\".format(user.name, userId))\n\n\ndef get_users_from_csv(path):\n    users = []\n    with open(path, newline='') as csvfile:\n        reader = csv.DictReader(csvfile)\n        for row in reader:\n            name = row['firstname'] + \" \" + row['lastname']\n            user = User(row['email'], row['password'], row['password'], name)\n            users.append(user)\n    return users\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "kubelab-score/score.csv",
    "content": "userId,username,email,exercise_session_id,start_time,end_time\r\n"
  },
  {
    "path": "kubelab-score/score_calculation.py",
    "content": "import csv\nimport json\nimport os\nimport requests\n\n\nclass User:\n    def __init__(self, id, name, email, username):\n        self.id = id\n        self.name = name\n        self.email = email\n        self.username = username\n\n\nclass ExerciseSession:\n    def __init__(self, id, userId, startTime, endTime):\n        self.id = id\n        self.userId = userId\n        self.startTime = startTime\n        self.endTime = endTime\n\n\ndef get_request(url):\n    try:\n        headers = {'Content-type': 'application/json'}\n        response = requests.get(url, headers=headers)\n        return response\n    except requests.exceptions.RequestException as e:\n        print(e)\n\n\ndef main():\n    exercises_sessions_url = os.environ['EXERCISE_SESSIONS_URL']\n    users_url = os.environ['USERS_URL']\n    csv_path = os.environ['CSV_PATH']\n\n    exercise_sessions = get_exercise_sessions_from_exercise_sessions_url(\n        exercises_sessions_url)\n    users = get_users_from_users_url(users_url)\n\n    # parse the data into a csv file\n    with open(csv_path, mode='w') as file:\n        writer = csv.writer(file)\n        writer.writerow([\"userId\", \"username\", \"email\",\n                        \"exercise_session_id\", \"start_time\", \"end_time\"])\n\n        for user in users:\n            for exercise_session in exercise_sessions:\n                if user.id == exercise_session.userId:\n                    writer.writerow([user.id, user.username, user.email, exercise_session.id,\n                                     exercise_session.startTime, exercise_session.endTime])\n\n\ndef get_users_from_users_url(users_url):\n    users = []\n    response = get_request(users_url)\n    if response.status_code == 200:\n        users_json = json.loads(response.text)['items']\n        print(users_json)\n        for user_json in users_json:\n            email = user_json.get('email')\n            username = user_json.get('username')\n            user = User(user_json['id'], user_json['name'], email, username)\n            users.append(user)\n    return users\n\n\n\ndef get_exercise_sessions_from_exercise_sessions_url(exercises_sessions_url):\n    exercise_sessions = []\n    response = get_request(exercises_sessions_url)\n    if response.status_code == 200:\n        exercise_sessions_json = json.loads(response.text)['items']\n        print(exercise_sessions_json)\n        for exercise_session_json in exercise_sessions_json:\n            exercise_session = ExerciseSession(\n                exercise_session_json['id'], exercise_session_json['user'], exercise_session_json['startTime'], exercise_session_json['endTime'])\n            exercise_sessions.append(exercise_session)\n    return exercise_sessions\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "kubelab-ui/.dockerignore",
    "content": ".git\n.svelte-kit\nbuild\nnode_modules\n"
  },
  {
    "path": "kubelab-ui/.env.example",
    "content": "export AGENT_INGRESS_CLASS=nginx-external\nexport ALLOWED_HOSTS=kubelab.ch\nexport LOCAL=true\n"
  },
  {
    "path": "kubelab-ui/.eslintignore",
    "content": ".DS_Store\nnode_modules\n/build\n/.svelte-kit\n/package\n.env\n.env.*\n!.env.example\n\n# Ignore files for PNPM, NPM and YARN\npnpm-lock.yaml\npackage-lock.json\nyarn.lock\n"
  },
  {
    "path": "kubelab-ui/.eslintrc.cjs",
    "content": "module.exports = {\n  root: true,\n  parser: \"@typescript-eslint/parser\",\n  extends: [\"eslint:recommended\", \"plugin:@typescript-eslint/recommended\", \"prettier\"],\n  plugins: [\"svelte3\", \"@typescript-eslint\"],\n  ignorePatterns: [\"*.cjs\"],\n  overrides: [{ files: [\"*.svelte\"], processor: \"svelte3/svelte3\" }],\n  settings: {\n    \"svelte3/typescript\": () => require(\"typescript\")\n  },\n  parserOptions: {\n    sourceType: \"module\",\n    ecmaVersion: 2020\n  },\n  env: {\n    browser: true,\n    es2017: true,\n    node: true\n  }\n};\n"
  },
  {
    "path": "kubelab-ui/.gitignore",
    "content": ".DS_Store\nnode_modules\n/build\n/.svelte-kit\n/package\n.env\n.env.*\n!.env.example\nvite.config.js.timestamp-*\nvite.config.ts.timestamp-*\n"
  },
  {
    "path": "kubelab-ui/.npmrc",
    "content": "engine-strict=true\n"
  },
  {
    "path": "kubelab-ui/.prettierignore",
    "content": ".DS_Store\nnode_modules\n/build\n/.svelte-kit\n/package\n.env\n.env.*\n!.env.example\n\n# Ignore files for PNPM, NPM and YARN\npnpm-lock.yaml\npackage-lock.json\nyarn.lock\n"
  },
  {
    "path": "kubelab-ui/.prettierrc",
    "content": "{\n  \"useTabs\": false,\n  \"singleQuote\": false,\n  \"trailingComma\": \"none\",\n  \"printWidth\": 100,\n  \"plugins\": [\"prettier-plugin-svelte\"],\n  \"pluginSearchDirs\": [\".\"],\n  \"overrides\": [{ \"files\": \"*.svelte\", \"options\": { \"parser\": \"svelte\" } }]\n}\n"
  },
  {
    "path": "kubelab-ui/.vscode/settings.json",
    "content": "{\n  \"tailwindCSS.classAttributes\": [\n    \"class\",\n    \"accent\",\n    \"active\",\n    \"background\",\n    \"badge\",\n    \"border\",\n    \"borderColor\",\n    \"borderWidth\",\n    \"button\",\n    \"buttonBack\",\n    \"buttonClasses\",\n    \"buttonComplete\",\n    \"buttonNext\",\n    \"buttonTextNext\",\n    \"buttonTextPrevious\",\n    \"chips\",\n    \"color\",\n    \"cursor\",\n    \"display\",\n    \"element\",\n    \"fill\",\n    \"flex\",\n    \"gap\",\n    \"gridColumns\",\n    \"height\",\n    \"hover\",\n    \"invalid\",\n    \"justify\",\n    \"meter\",\n    \"padding\",\n    \"regionBody\",\n    \"regionCaption\",\n    \"regionCaret\",\n    \"regionCone\",\n    \"regionContent\",\n    \"regionControl\",\n    \"regionDefault\",\n    \"regionFoot\",\n    \"regionHead\",\n    \"regionHeader\",\n    \"regionIcon\",\n    \"regionInterface\",\n    \"regionInterfaceText\",\n    \"regionLabel\",\n    \"regionLead\",\n    \"regionLegend\",\n    \"regionList\",\n    \"regionNavigation\",\n    \"regionPage\",\n    \"regionPanel\",\n    \"regionRowHeadline\",\n    \"regionRowMain\",\n    \"regionTrail\",\n    \"rounded\",\n    \"select\",\n    \"shadow\",\n    \"slotDefault\",\n    \"slotFooter\",\n    \"slotHeader\",\n    \"slotLead\",\n    \"slotMessage\",\n    \"slotMeta\",\n    \"slotPageContent\",\n    \"slotPageFooter\",\n    \"slotPageHeader\",\n    \"slotSidebarLeft\",\n    \"slotSidebarRight\",\n    \"slotTrail\",\n    \"space\",\n    \"spacing\",\n    \"text\",\n    \"track\",\n    \"width\"\n  ]\n}\n"
  },
  {
    "path": "kubelab-ui/Dockerfile",
    "content": "FROM node:lts-slim as build\n\nWORKDIR /app\n\nCOPY package*.json ./\nRUN rm -rf node_modules\nRUN rm -rf build\nCOPY . .\nRUN npm install\nRUN npm run build\n\nFROM node:lts-slim as run\n\nWORKDIR /app\nCOPY --from=build /app/package.json ./package.json\nCOPY --from=build /app/build ./build\nRUN npm install --production\n\nEXPOSE 3000\nENTRYPOINT [ \"npm\", \"run\", \"start\" ]\n"
  },
  {
    "path": "kubelab-ui/README.md",
    "content": "# KubeLab UI\n\n## Setup\n\n```bash\nnpm install # install dependencies\nnpm run build # compile frontend\n```\n\nThe above produces `build` output directory which is then used by PocketBase to serve the frontend of your app.\n\n## Live Development\n\n```bash\n# start the backend, if not already running ...\nsource .env\nnpm run dev:backend\n# and then start the frontend ...\nnpm run dev\n```\n\nNow visit http://localhost:5173 (ui) or http://localhost:8090 (pb)\n\n## Generated Types\n\nThe file `generated-types.ts` contains TypeScript definitions of `Record` types mirroring the fields in your database collections. But it needs to be regenerated every time you modify the schema. This can be done by simply running the `typegen` script in the frontend's `package.json`. So remember to run `npm run typegen` after every schema change.\n\n## Building\n\nTo create a production version of your app (static HTML/JS app):\n\n_NOTE_: The build below will fail unless the backend has at least 1\npost created. So please create a \"posts\" record using the app UI or\nthe admin UI before running build below.\n\n```bash\n# compile frontend\nnpm run build\n# and then serve it with pocketbase\nnpm run backend\n```\n\nThe above generates output in the `build` folder. Now you can serve production compiled version of the frontend using the backend (with `--publicDir ../frontend/build`), any static file web server, or `npm preview`.\n"
  },
  {
    "path": "kubelab-ui/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/basic-features/typescript for more information.\n"
  },
  {
    "path": "kubelab-ui/package.json",
    "content": "{\n  \"name\": \"ui\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite dev\",\n    \"dev:backend\": \"cd ../kubelab-backend && modd\",\n    \"backend\": \"cd ../kubelab-backend && ./pocketbase serve --publicDir=../kubelab-ui/build\",\n    \"build\": \"vite build\",\n    \"build:backend\": \"cd ../kubelab-backend && go build\",\n    \"typegen\": \"pocketbase-typegen --db ../kubelab-backend/pb_data/data.db --out ./src/lib/pocketbase/generated-types.ts\",\n    \"preview\": \"vite preview\",\n    \"check\": \"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json\",\n    \"check:watch\": \"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch\",\n    \"lint\": \"prettier --plugin-search-dir . --check . && eslint .\",\n    \"format\": \"prettier --plugin-search-dir . --write .\"\n  },\n  \"devDependencies\": {\n    \"@sveltejs/adapter-auto\": \"^2.1.1\",\n    \"@sveltejs/kit\": \"^1.25.2\",\n    \"@tailwindcss/forms\": \"^0.5.3\",\n    \"@tailwindcss/line-clamp\": \"^0.4.4\",\n    \"@tailwindcss/typography\": \"^0.5.13\",\n    \"@types/marked\": \"^5.0.0\",\n    \"@types/node\": \"^20.14.10\",\n    \"@types/prismjs\": \"^1.26.4\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.62.0\",\n    \"@typescript-eslint/parser\": \"^5.62.0\",\n    \"autoprefixer\": \"^10.4.13\",\n    \"daisyui\": \"^4.10.2\",\n    \"eslint\": \"^9.8.0\",\n    \"eslint-config-prettier\": \"^9.1.0\",\n    \"eslint-plugin-svelte\": \"^2.39.3\",\n    \"eslint-plugin-svelte3\": \"^4.0.0\",\n    \"flowbite\": \"^2.3.0\",\n    \"flowbite-svelte\": \"^0.46.0\",\n    \"flowbite-svelte-icons\": \"^0.4.4\",\n    \"mdsvex\": \"^0.11.0\",\n    \"pocketbase\": \"^0.21.2\",\n    \"postcss\": \"^8.4.38\",\n    \"prettier\": \"^3.3.1\",\n    \"prettier-plugin-svelte\": \"^3.2.6\",\n    \"svelte\": \"^4.2.18\",\n    \"svelte-check\": \"^3.6.9\",\n    \"svelte-preprocess\": \"^6.0.2\",\n    \"svelte-splitpanes\": \"^0.7.14\",\n    \"tailwind-scrollbar\": \"^3.0.4\",\n    \"tailwindcss\": \"^3.4.3\",\n    \"tslib\": \"^2.6.3\",\n    \"typescript\": \"^5.4.5\",\n    \"vite\": \"^4.0.0\"\n  },\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@floating-ui/dom\": \"^1.2.8\",\n    \"@picocss/pico\": \"^2.0.6\",\n    \"@sveltejs/adapter-node\": \"^1.2.1\",\n    \"@sveltejs/adapter-static\": \"^2.0.3\",\n    \"@webcontainer/api\": \"^1.1.9\",\n    \"chart.js\": \"^4.3.0\",\n    \"lucide-svelte\": \"^0.265.0\",\n    \"marked\": \"^5.0.4\",\n    \"pocketbase-typegen\": \"^1.2.1\",\n    \"prismjs\": \"^1.29.0\",\n    \"svelte-confetti\": \"^1.4.0\",\n    \"svelte-french-toast\": \"^1.2.0\",\n    \"svelte-icons-pack\": \"^2.1.0\",\n    \"svelte-local-storage-store\": \"^0.6.4\",\n    \"svelte-markdown\": \"^0.4.1\",\n    \"xterm\": \"^5.3.0\",\n    \"xterm-addon-fit\": \"^0.7.0\"\n  }\n}\n"
  },
  {
    "path": "kubelab-ui/playwright.config.ts",
    "content": "import type { PlaywrightTestConfig } from \"@playwright/test\";\n\nconst config: PlaywrightTestConfig = {\n    webServer: {\n        command: \"npm run build && npm run preview\",\n        port: 4173\n    },\n    testDir: \"tests\"\n};\n\nexport default config;\n"
  },
  {
    "path": "kubelab-ui/postcss.config.cjs",
    "content": "module.exports = {\n  plugins: [require(\"tailwindcss\"), require(\"autoprefixer\")]\n};\n"
  },
  {
    "path": "kubelab-ui/src/app.css",
    "content": "a {\n    text-decoration: none !important;\n}\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nh1 {\n    font-size: 2rem;\n    font-weight: 700;\n    line-height: 1.2;\n    margin-bottom: 0.5em;\n}\n\nh2 {\n    font-size: 1.5rem;\n    font-weight: 600;\n    line-height: 1.25;\n    margin-bottom: 0.5em;\n}\n\nh3 {\n    font-size: 1.25rem;\n    font-weight: 600;\n    line-height: 1.25;\n    margin-bottom: 0.5em;\n}\n\n\n.btn {\n    @apply border-2;\n}\n\n.splitpanes__splitter {\n\tbackground-color: darkgray !important;\n\tposition: relative;\n    opacity: 0;\n}\n\n\n.splitpanes--horizontal > .splitpanes__splitter {\n    border-top: none !important;\n}\n\n.splitpanes--vertical > .splitpanes__splitter {\n    border-left: none !important;\n}\n"
  },
  {
    "path": "kubelab-ui/src/app.d.ts",
    "content": "// See https://kit.svelte.dev/docs/types#app\n// for information about these interfaces\n// and what to do when importing types\ndeclare namespace App {\n    // interface Locals {}\n    // interface PageData {}\n    // interface Error {}\n    // interface Platform {}\n}\n"
  },
  {
    "path": "kubelab-ui/src/app.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" >\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%sveltekit.assets%/favicon.png\" />\n    <meta name=\"viewport\" content=\"width=device-width\" />\n    %sveltekit.head%\n  </head>\n  <body>\n    <div style=\"display: contents\" class=\"h-full\">%sveltekit.body%</div>\n  </body>\n</html>\n"
  },
  {
    "path": "kubelab-ui/src/app.postcss",
    "content": "/*place global styles here */\n/* html,\nbody {\n  @apply h-full overflow-hidden;\n} */\nhtml,\nbody {\n  @apply h-full scrollbar-none;\n}\n\n\n@font-face {\n  /* Reference name */\n  font-family: \"Inter\";\n  /* For multiple files use commas, ex: url(), url(), ... */\n  src: url(\"/fonts/Inter-VariableFont_slnt,wght.ttf\");\n}\n"
  },
  {
    "path": "kubelab-ui/src/hooks.client.ts",
    "content": "import { client, currentUser } from \"$lib/pocketbase\";\n\nclient.authStore.loadFromCookie(document.cookie);\nclient.authStore.onChange(() => {\n    currentUser.set(client.authStore.model);\n    document.cookie = client.authStore.exportToCookie({ httpOnly: false });\n});\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/Console.svelte",
    "content": "<script lang=\"ts\">\n  import { client } from \"$lib/pocketbase\";\n  import { layout_store } from \"$lib/stores/layout_store\";\n  import { terminal_size } from \"$lib/stores/terminal\";\n  import darkTheme from \"$lib/stores/theme\";\n  import { onDestroy, onMount } from \"svelte\";\n\n  import { Terminal } from \"xterm\";\n  import { FitAddon } from \"xterm-addon-fit\";\n\n  export const terminal = new Terminal({\n    convertEol: true,\n    disableStdin: false,\n    cursorBlink: true,\n    fontFamily: \"monospace\",\n    fontSize: 14,\n\n    theme: $darkTheme\n      ? {\n          foreground: \"#d2d2d2\",\n          background: \"#2B3441\",\n          cursor: \"#adadad\",\n          black: \"#000000\",\n          red: \"#d81e00\",\n          green: \"#5ea702\",\n          yellow: \"#cfae00\",\n          blue: \"#427ab3\",\n          magenta: \"#89658e\",\n          cyan: \"#00a7aa\",\n          white: \"#dbded8\",\n          brightBlack: \"#686a66\",\n          brightRed: \"#f54235\",\n          brightGreen: \"#99e343\",\n          brightYellow: \"#fdeb61\",\n          brightBlue: \"#84b0d8\",\n          brightMagenta: \"#bc94b7\",\n          brightCyan: \"#37e6e8\",\n          brightWhite: \"#f1f1f0\"\n        }\n      : {\n          foreground: \"#d2d2d2\",\n          background: \"#2B3441\",\n          cursor: \"#adadad\",\n          black: \"#000000\",\n          red: \"#d81e00\",\n          green: \"#5ea702\",\n          yellow: \"#cfae00\",\n          blue: \"#427ab3\",\n          magenta: \"#89658e\",\n          cyan: \"#00a7aa\",\n          white: \"#dbded8\",\n          brightBlack: \"#686a66\",\n          brightRed: \"#f54235\",\n          brightGreen: \"#99e343\",\n          brightYellow: \"#fdeb61\",\n          brightBlue: \"#84b0d8\",\n          brightMagenta: \"#bc94b7\",\n          brightCyan: \"#37e6e8\",\n          brightWhite: \"#f1f1f0\"\n        },\n    scrollOnUserInput: true\n  });\n\n  let socket: WebSocket;\n  let agentUrl: string;\n\n  const initializeWebSocket = () => {\n    const lab_session_id = window.location.pathname.split(\"/\")[2];\n    const exercise_id = window.location.pathname.split(\"/\")[3];\n    // if dev mode the agentHost is kubelab.ch\n    const agentHost =\n      window.location.host === \"localhost:5173\" ? \"kubelab.ch\" : window.location.host;\n\n    agentUrl =\n      agentHost +\n      \"/kubelab-\" +\n      lab_session_id +\n      \"-\" +\n      exercise_id +\n      \"-\" +\n      client.authStore.model?.id;\n\n    // Close the socket only if it is opened\n    if (socket && socket.readyState === WebSocket.OPEN) {\n      socket.close();\n    }\n    terminal.reset();\n    socket = new WebSocket(\"wss://\" + agentUrl + \"/xterm.js\");\n\n    socket.binaryType = \"arraybuffer\";\n\n    socket.onerror = (error) => {\n      terminal.write(`\\r\\nWebSocket error: ${error}\\r\\n`);\n    };\n\n    socket.onopen = () => {\n      // Check that the socket is opened before interacting with it\n      if (socket.readyState === WebSocket.OPEN) {\n        terminal.onData((data) => {\n          socket.send(new TextEncoder().encode(\"\\x00\" + data));\n        });\n\n        terminal.onTitleChange((title) => {\n          document.title = title;\n        });\n      }\n    };\n\n    socket.onclose = () => {\n      terminal.write(\"\\r\\nConnection closed. Try to refresh the tab.\\r\\n\");\n    };\n\n    socket.onmessage = (message) => {\n      if (message.data instanceof ArrayBuffer) {\n        terminal.write(ab2str(message.data));\n      }\n    };\n  };\n\n  function ab2str(buf: any) {\n    // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n    // @ts-ignore\n    return String.fromCharCode.apply(null, new Uint8Array(buf));\n  }\n\n  export const fitAddon = new FitAddon();\n  terminal.loadAddon(fitAddon);\n  terminal.focus();\n\n  terminal.onResize((size) => {\n    const terminal_size = {\n      cols: size.cols,\n      rows: size.rows,\n      y: size.rows,\n      x: size.cols\n    };\n    // Check that the socket is opened before interacting with it\n    if (socket.readyState === WebSocket.OPEN) {\n      socket.send(new TextEncoder().encode(\"\\x01\" + JSON.stringify(terminal_size)));\n    }\n  });\n\n  let div: HTMLDivElement;\n\n  onMount(() => {\n    initializeWebSocket();\n    terminal.open(div);\n    setTimeout(() => {\n      update_height();\n    }, 300);\n  });\n\n  export const update_height = () => {\n    fitAddon.fit();\n  };\n\n  $: {\n    $terminal_size;\n    $layout_store.terminal;\n    setTimeout(() => {\n      update_height();\n    }, 300);\n  }\n\n  onDestroy(() => {\n    socket?.close();\n    terminal.dispose();\n  });\n</script>\n\n<div bind:this={div} />\n\n<style>\n  div {\n    height: 100%;\n    width: 100%;\n  }\n  div :global(.xterm) {\n    height: 100%;\n    padding: 5px;\n  }\n\n  /* disable scrollbar */\n  div :global(.xterm-viewport) {\n    overflow-y: hidden !important;\n  }\n</style>\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/base/BackButton.svelte",
    "content": "<script lang=\"ts\">\n  import Icon from \"svelte-icons-pack\";\n  import { ArrowBigLeft } from \"lucide-svelte\";\n\n  function onClick() {\n    window.history.back();\n  }\n</script>\n\n<button\n  on:click={onClick}\n  class=\"hover:text-gray-700 hover:bg-gray-300 text-gray-500 bg-gray-200 p-2\"\n>\n  <ArrowBigLeft class=\"inline\" />\n  Back\n</button>\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/base/Badges.svelte",
    "content": "<script lang=\"ts\">\n  import { Color } from \"$lib/utils/enums\";\n\n  export let color: Color = Color.Gray;\n  export let size: \"xs\" | \"sm\" | \"md\" | \"lg\" = \"md\";\n  export let rounded: boolean = true;\n  export let text: string = \"Badge\";\n\n  let bgColorClass: string = \"bg-\" + color + \"-100\";\n  let textColorClass: string = \"text-\" + color + \"-800\";\n  let roundedClass: string = rounded ? \"rounded-full\" : \"\";\n  let sizeClass: string = \"text-\" + size;\n\n  let cssClasses: string =\n    \"px-2.5 py-0.5 inline-flex items-center \" +\n    sizeClass +\n    \" font-medium \" +\n    bgColorClass +\n    \" \" +\n    textColorClass +\n    \" \" +\n    roundedClass;\n</script>\n\n<div>\n  <span class={cssClasses}>{text}</span>\n</div>\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/base/Card.svelte",
    "content": "<div class=\"card h-64\">\n  <div class=\"card-header\" />\n  <div class=\"card-footer\" />\n</div>\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/base/Desktop.svelte",
    "content": "<script lang=\"ts\">\n  import darkTheme from \"$lib/stores/theme\";\n  import type { ComponentType, SvelteComponentTyped } from \"svelte\";\n  // eslint-disable-next-line @typescript-eslint/naming-convention\n  export let Console: ComponentType<SvelteComponentTyped>;\n</script>\n\n{#key $darkTheme}\n  <div class=\"h-full overflow-y-auto\">\n    <svelte:component this={Console} />\n  </div>\n{/key}\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/base/Nav.svelte",
    "content": "<script lang=\"ts\">\n  import { navigating } from \"$app/stores\";\n  import { client, logout } from \"$lib/pocketbase\";\n    import { avatarUrl } from \"$lib/stores/data\";\n  import darkTheme from \"$lib/stores/theme\";\n  import type { NavRoute } from \"$lib/types\";\n  import {\n    TerminalSquare,\n    Github,\n    Presentation,\n    Sun,\n    Moon,\n    BarChart2,\n    Building2\n  } from \"lucide-svelte\";\n\n  $: if (client.authStore) {\n    $avatarUrl =\n      \"/api/files/\" +\n      client.authStore.model?.collectionId +\n      \"/\" +\n      client.authStore.model?.id +\n      \"/\" +\n      client.authStore.model?.avatar;\n  }\n\n  let routes: NavRoute[] = [\n    {\n      id: \"1\",\n      name: \"Dashboard\",\n      href: \"/app/\",\n      icon: BarChart2\n    },\n    {\n      id: \"2\",\n      name: \"Labs\",\n      href: \"/labs/\",\n      icon: TerminalSquare\n    },\n    {\n      id: \"3\",\n      name: \"Material\",\n      href: \"/material/\",\n      icon: Presentation\n    },\n    {\n      id: \"4\",\n      name: \"About\",\n      href: \"https://github.com/natrontech/kubelab\",\n      icon: Github\n    }\n  ];\n\n  if (client.authStore.model?.role != \"admin\") {\n    routes = [\n      {\n        id: \"1\",\n        name: \"Dashboard\",\n        href: \"/app/\",\n        icon: BarChart2\n      },\n      {\n        id: \"2\",\n        name: \"Labs\",\n        href: \"/labs/\",\n        icon: TerminalSquare\n      },\n      {\n        id: \"3\",\n        name: \"Material\",\n        href: \"/material/\",\n        icon: Presentation\n      },\n      {\n        id: \"4\",\n        name: \"About\",\n        href: \"https://github.com/natrontech/kubelab\",\n        icon: Github\n      }\n    ];\n  } else {\n    routes = [\n      {\n        id: \"1\",\n        name: \"Companies\",\n        href: \"/app/\",\n        icon: Building2\n      }\n    ];\n  }\n</script>\n\n<div class=\"navbar  h-16 pt-4\">\n  <div class=\"navbar-start z-10\">\n    <div class=\"dropdown\">\n      <!-- svelte-ignore a11y-label-has-associated-control -->\n      <label tabindex=\"0\" class=\"btn btn-ghost lg:hidden -mt-2\">\n        <img src=\"/images/kubelab-logo.png\" alt=\"logo\" class=\"w-8 h-8\" />\n      </label>\n      <ul class=\"menu menu-sm dropdown-content mt-3 p-2 shadow bg-base-200 rounded-box w-52 \">\n        {#each routes as route}\n          <li>\n            <a href={route.href}>\n              <svelte:component this={route.icon} class=\"w-5 h-5\" />{@html \"&nbsp;\"}\n              {route.name}\n            </a>\n          </li>\n        {/each}\n      </ul>\n    </div>\n    <a class=\"btn btn-ghost normal-case text-xl hidden lg:flex -mt-2\" href=\"/app\">\n      <img src=\"/images/kubelab-logo.png\" alt=\"logo\" class=\"w-8 h-8 mr-2\" /> KubeLab</a\n    >\n  </div>\n  <div class=\"navbar-center hidden lg:flex\">\n    <ul class=\"menu menu-horizontal px-1\">\n      {#each routes as route}\n        <li>\n          <a href={route.href}>\n            <svelte:component this={route.icon} class=\"w-5 h-5\" />{@html \"&nbsp;\"}\n            {route.name}\n          </a>\n        </li>\n      {/each}\n    </ul>\n  </div>\n\n  <div class=\"navbar-end sm:-mt-2 \">\n    <button class=\"btn bg-transparent border-none\" on:click={() => darkTheme.set(!$darkTheme)}>\n      {#if $darkTheme === true}\n        <Sun />\n      {:else}\n        <Moon />\n      {/if}\n    </button>\n\n    <a href=\"https://natron.io\" target=\"_blank\" class=\"-mt-2 mx-2\">\n      <span class=\"text-xs font-semibold leading-6 text-gray-900 dark:text-white\">Powered by</span>\n      {#if $darkTheme === true}\n        <img class=\"h-4 w-auto\" src={\"/images/natron-dark.png\"} alt=\"Switzerland\" />\n      {:else}\n        <img class=\"h-4 w-auto\" src={\"/images/natron.png\"} alt=\"Switzerland\" />\n      {/if}\n    </a>\n    <div class=\"dropdown dropdown-end z-10\">\n      <!-- svelte-ignore a11y-no-noninteractive-tabindex -->\n      <!-- svelte-ignore a11y-label-has-associated-control -->\n      <label tabindex=\"0\" class=\"btn btn-ghost btn-circle avatar\">\n        <div class=\"w-10 rounded-full\">\n          <img src={$avatarUrl} />\n        </div>\n      </label>\n      <!-- svelte-ignore a11y-no-noninteractive-tabindex -->\n      <ul\n        tabindex=\"0\"\n        class=\"menu menu-sm dropdown-content mt-3 p-2 shadow bg-base-200 rounded-box w-52 \"\n      >\n        <li>\n          <a href=\"/app/profile\"> Profile </a>\n        </li>\n        <li><button on:click={() => logout()}>Logout</button></li>\n      </ul>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/base/PlaceholderComponent.svelte",
    "content": "<div />\n\n<style>\n  div {\n    background-color: var(--sk-back-1);\n    width: 100%;\n    height: 100%;\n  }\n</style>\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/base/SideOver.svelte",
    "content": "<script lang=\"ts\">\n  import type {\n    ExerciseSessionsRecord,\n    ExerciseSessionsResponse,\n    LabSessionsRecord,\n    LabSessionsResponse\n  } from \"$lib/pocketbase/generated-types\";\n  import { exercise_sessions, lab_sessions, updateDataStores } from \"$lib/stores/data\";\n  import { AlertTriangle, Pause, Play, TestTube2, X } from \"lucide-svelte\";\n  import SvelteMarkdown from \"svelte-markdown\";\n  import CodeSpanComponent from \"$lib/components/markdown/CodeSpanComponent.svelte\";\n  import CodeComponent from \"$lib/components/markdown/CodeComponent.svelte\";\n  import LinkComponent from \"$lib/components/markdown/LinkComponent.svelte\";\n  import toast from \"svelte-french-toast\";\n  import { loadingExercises, loadingLabs } from \"$lib/stores/loading\";\n  import { client } from \"$lib/pocketbase\";\n  import { sidebar_exercise_sessions, sidebar_lab, sidebar_lab_session } from \"$lib/stores/sidebar\";\n  import Exercise from \"../labs/Exercise.svelte\";\n  import { Button, Modal } from \"flowbite-svelte\";\n\n  let docs: string;\n  export let drawerHidden = true;\n  let confirmation = false;\n\n  async function getMarkdown() {\n    fetch($sidebar_lab.docs)\n      .then((response) => response.text())\n      .then((text) => {\n        docs = text;\n      })\n      .catch((error) => {\n        console.error(error);\n      });\n  }\n\n  async function startLab(lab_session_id: string) {\n    // if there is more than one lab_session clusterRunning = true, fail\n    if ($lab_sessions.filter((lab_session) => lab_session.clusterRunning).length > 1) {\n      toast.error(\"There are already two lab running\");\n      return;\n    }\n\n    if ($loadingLabs.size > 1) {\n      toast.error(\"There are already 2 labs starting/stopping\");\n      return;\n    }\n\n    loadingLabs.update((labs) => {\n      labs.add(lab_session_id);\n      return new Set(labs); // Required for Svelte's reactivity\n    });\n\n    const data: LabSessionsRecord = {\n      clusterRunning: true,\n      // @ts-ignore\n      user: client.authStore.model?.id,\n      lab: $sidebar_lab.id,\n      startTime: new Date().toISOString()\n    };\n\n    await client\n      .collection(\"lab_sessions\")\n      .update(lab_session_id, data)\n      // @ts-ignore\n      .then((record: LabSessionsResponse) => {\n        toast.success(\"Lab started\");\n\n        $sidebar_lab_session = record;\n\n        lab_sessions.update((lab_sessions) => {\n          return lab_sessions.map((lab_session) => {\n            if (lab_session.id === record.id) {\n              return record;\n            }\n            return lab_session;\n          });\n        });\n      })\n      .catch((error) => {\n        console.error(error);\n        toast.error(\"Lab failed to start\");\n      })\n      .finally(() => {\n        updateDataStores().catch((error) => {\n          toast.error(error);\n        });\n        loadingLabs.update((labs) => {\n          labs.delete(lab_session_id);\n          return new Set(labs); // Required for Svelte's reactivity\n        });\n      });\n  }\n\n  // TODO: add modal to confirm stop lab\n  async function stopLab(lab_session_id: string) {\n    loadingLabs.update((labs) => {\n      labs.add(lab_session_id);\n      return new Set(labs); // Required for Svelte's reactivity\n    });\n\n    const data: LabSessionsRecord = {\n      clusterRunning: false,\n      // @ts-ignore\n      user: client.authStore.model?.id,\n      lab: $sidebar_lab.id,\n      endTime: new Date().toISOString()\n    };\n\n    // update each exercise session to stop agentRunning = false\n    $sidebar_exercise_sessions.forEach(async (sidebar_exercise_session) => {\n      const exercise_session_data: ExerciseSessionsRecord = {\n        agentRunning: false,\n        // @ts-ignore\n        user: client.authStore.model?.id,\n        exercise: sidebar_exercise_session.exercise\n      };\n\n      loadingExercises.update((exercises) => {\n        exercises.add(sidebar_exercise_session.id);\n        return new Set(exercises); // Required for Svelte's reactivity\n      });\n\n      await client\n        .collection(\"exercise_sessions\")\n        .update(sidebar_exercise_session.id, exercise_session_data)\n        // @ts-ignore\n        .then((record: ExerciseSessionsResponse) => {\n          sidebar_exercise_session = record;\n          sidebar_exercise_sessions.update((sidebar_exercise_sessions) => {\n            return sidebar_exercise_sessions.map((sidebar_exercise_session) => {\n              if (sidebar_exercise_session.id === record.id) {\n                return record;\n              }\n              return sidebar_exercise_session;\n            });\n          });\n          exercise_sessions.update((exercise_sessions) => {\n            return exercise_sessions.map((exercise_session) => {\n              if (exercise_session.id === record.id) {\n                return record;\n              }\n              return exercise_session;\n            });\n          });\n        })\n        .catch((error) => {\n          console.error(error);\n        })\n        .finally(() => {\n          updateDataStores().catch((error) => {\n            toast.error(error);\n          });\n          loadingExercises.update((exercises) => {\n            exercises.delete(sidebar_exercise_session.id);\n            return new Set(exercises); // Required for Svelte's reactivity\n          });\n          confirmation = false;\n        });\n    });\n\n    await client\n      .collection(\"lab_sessions\")\n      .update($sidebar_lab_session.id, data)\n      // @ts-ignore\n      .then((record: LabSessionsResponse) => {\n        toast.success(\"Lab stopped\");\n\n        $sidebar_lab_session = record;\n\n        lab_sessions.update((lab_sessions) => {\n          return lab_sessions.map((lab_session) => {\n            if (lab_session.id === record.id) {\n              return record;\n            }\n            return lab_session;\n          });\n        });\n      })\n      .catch((error) => {\n        console.error(error);\n        toast.error(\"Lab failed to stop\");\n      })\n      .finally(() => {\n        updateDataStores().catch((error) => {\n          toast.error(error);\n        });\n        loadingLabs.update((labs) => {\n          labs.delete(lab_session_id);\n          return new Set(labs); // Required for Svelte's reactivity\n        });\n      });\n  }\n</script>\n\n<aside class=\"\">\n  {#key $sidebar_lab_session}\n    {#if !drawerHidden}\n      <div\n        class=\"{$sidebar_lab_session.clusterRunning\n          ? ''\n          : ''} px-4 py-6 sm:px-6 absolute w-full z-10\"\n      >\n        <div class=\"flex items-center justify-between\">\n          <h2 class=\"text-base font-semibold leading-6 text-primary\" id=\"slide-over-title\">\n            <TestTube2 class=\"w-5 h-5 mr-2 inline-block\" />\n            {$sidebar_lab.title}\n          </h2>\n        </div>\n        <div\n          class=\"badge badge-outline {$sidebar_lab_session.clusterRunning ? 'badge-success' : ''}\"\n        >\n          {#if $sidebar_lab_session.clusterRunning}\n            <Play class=\"w-4 h-4 mr-1 inline-block\" />\n          {:else}\n            <Pause class=\"w-4 h-4 mr-1 inline-block\" />\n          {/if}\n          {$sidebar_lab_session.clusterRunning ? \"Running\" : \"Stopped\"}\n        </div>\n        <div class=\"grid grid-cols-1 gap-2 mt-4\">\n          {#if !$sidebar_lab_session.clusterRunning}\n            <button\n              class=\"btn btn-outline btn-success\"\n              on:click={() => startLab($sidebar_lab_session.id)}\n            >\n              {#if $loadingLabs.has($sidebar_lab_session.id)}\n                <span class=\"loading loading-dots loading-md\" />\n              {:else}\n                <Play class=\"w-5 h-5 mr-2 inline-block\" />\n                Start lab\n              {/if}\n            </button>\n          {:else}\n            {#if $loadingLabs.has($sidebar_lab_session.id)}\n              <button class=\"btn btn-outline btn-disabled\">\n                <span class=\"loading loading-dots loading-md\" />\n                Stopping lab\n              </button>\n            {:else if confirmation}\n              <button\n                class=\"btn btn-outline btn-warning\"\n                on:click={() => stopLab($sidebar_lab_session.id)}\n              >\n                <AlertTriangle class=\"w-5 h-5 mr-2 inline-block\" />\n                Are you sure?\n              </button>\n            {:else}\n              <button class=\"btn btn-outline btn-error\" on:click={() => (confirmation = true)}>\n                <Pause class=\"w-5 h-5 mr-2 inline-block\" />\n                Stop lab\n              </button>\n            {/if}\n          {/if}\n        </div>\n        <!-- svelte-ignore a11y-no-noninteractive-tabindex -->\n        <div\n          tabindex=\"0\"\n          class=\"collapse collapse-arrow bg-white dark:bg-neutral border-primary border-2 my-4  rounded-lg shadow-md\"\n        >\n          <!-- svelte-ignore a11y-click-events-have-key-events -->\n          <div\n            class=\"collapse-title text-md font-medium\"\n            on:click={() => {\n              getMarkdown();\n            }}\n          >\n            About the lab\n          </div>\n          <div class=\"collapse-content\">\n            <SvelteMarkdown\n              source={docs}\n              renderers={{\n                codespan: CodeSpanComponent,\n                code: CodeComponent,\n                link: LinkComponent\n              }}\n            />\n          </div>\n        </div>\n        <div class=\"absolute top-5 right-6\">\n          <div class=\"tooltip tooltip-left\" data-tip=\"close\">\n            <button\n              type=\"button\"\n              on:click={() => {\n                drawerHidden = !drawerHidden;\n              }}\n              class=\"btn btn-outline border-none btn-sm btn-square\"\n            >\n              <X />\n            </button>\n          </div>\n        </div>\n      </div>\n      {#key $sidebar_exercise_sessions}\n        <div\n          class=\" space-y-2 px-6 absolute w-full top-64 bottom-0 overflow-y-scroll scrollbar-none pb-4\"\n        >\n          {#each $sidebar_exercise_sessions as exercise_session, idx}\n            <Exercise this_exercise_session={exercise_session} index={idx} />\n          {/each}\n        </div>\n      {/key}\n    {/if}\n  {/key}\n</aside>\n\n<style>\n  aside {\n    right: -100%;\n    transition: right 0.3s ease-in-out;\n  }\n</style>\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/base/ToggleConfetti.svelte",
    "content": "<script>\n  import { onMount, tick } from \"svelte\";\n\n  export let toggleOnce = false;\n  export let relative = true;\n\n  let active = false;\n\n  async function click() {\n    if (toggleOnce) {\n      active = !active;\n      return;\n    }\n\n    active = false;\n    await tick();\n    active = true;\n  }\n\n  onMount(() => {\n    if (toggleOnce) {\n      return;\n    }\n\n    click();\n  });\n\n</script>\n\n<span on:click={click} class:relative>\n  <slot name=\"label\" />\n  {#if active}\n    <div class=\"confetti\">\n      <slot />\n    </div>\n  {/if}\n</span>\n\n<style>\n  .relative {\n    position: relative;\n  }\n\n  .relative .confetti {\n    position: absolute;\n    top: 50%;\n    left: 50%;\n  }\n\n  .confetti {\n    pointer-events: none;\n  }\n</style>\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/dashboard/Chart.svelte",
    "content": "<script>\n  import { Line } from \"svelte-chartjs\";\n\n  import {\n    Chart as ChartJS,\n    Title,\n    Tooltip,\n    Legend,\n    LineElement,\n    LinearScale,\n    PointElement,\n    CategoryScale\n  } from \"chart.js\";\n  import { data } from \"./data\";\n\n  ChartJS.register(Title, Tooltip, LineElement, LinearScale, PointElement, CategoryScale);\n</script>\n\n<Line {data} options={{ responsive: true }} />\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/dashboard/RunningExercises.svelte",
    "content": "<script lang=\"ts\">\n  import { goto } from \"$app/navigation\";\n  import { client } from \"$lib/pocketbase\";\n  import type {\n    ExerciseSessionsRecord,\n    ExerciseSessionsResponse\n  } from \"$lib/pocketbase/generated-types\";\n  import {\n    exercise,\n    exercise_sessions,\n    exercise_session,\n    getExerciseSessionByExercise,\n    exercises,\n    lab\n  } from \"$lib/stores/data\";\n  import { loadingExercises } from \"$lib/stores/loading\";\n  import { Card } from \"flowbite-svelte\";\n  import { Pause, Terminal } from \"lucide-svelte\";\n  import toast from \"svelte-french-toast\";\n\n\n  let running_exercises: ExerciseSessionsResponse[] = $exercise_sessions.filter(\n    (exercise_session) => exercise_session.agentRunning\n  );\n\n  function openExercise(local_exercise_session: ExerciseSessionsResponse) {\n    // TODO: fix roadmap\n    let exercises_by_lab = $exercises.filter(\n      (exercise) => exercise.lab === local_exercise_session.expand.exercise.lab\n    );\n    exercise.set(local_exercise_session.expand.exercise);\n    exercises.set(exercises_by_lab);\n    new Promise((resolve) => setTimeout(resolve, 100)).then(() =>\n      goto(\n        `/labs/${local_exercise_session.expand.exercise.lab}/${local_exercise_session.expand.exercise.id}`\n      )\n    );\n  }\n\n  async function stopExercise(exercise_id: string) {\n    const data: ExerciseSessionsRecord = {\n      // @ts-ignore\n      user: client.authStore.model?.id,\n      exercise: exercise_id,\n      startTime: new Date().toISOString(),\n      endTime: \"\",\n      agentRunning: false\n    };\n\n    $loadingExercises = $loadingExercises.concat(exercise_id);\n\n    const exercise_session_id = getExerciseSessionByExercise(exercise_id)?.id;\n\n    if (exercise_session_id) {\n      await client\n        .collection(\"exercise_sessions\")\n        .update(exercise_session_id, data)\n        // @ts-ignore\n        .then((response: any) => {\n          toast.success(\"Exercise stopped\");\n\n          // @ts-ignore\n          // update running_exercises\n          running_exercises = running_exercises.filter(\n            (running_exercise) => running_exercise.id !== exercise_session_id\n          );\n\n          $exercise_sessions = $exercise_sessions.map((exercise_session) => {\n            if (exercise_session.id === exercise_session_id) {\n              return response;\n            }\n            return exercise_session;\n          });\n        })\n        .catch((error) => {\n          toast.error(error.message);\n        })\n        .finally(() => {\n          $loadingExercises = $loadingExercises.filter((id) => id !== exercise_id);\n        });\n    }\n  }\n</script>\n\n<Card\n  padding=\"xl\"\n  class=\"bg-white dark:bg-base-100 rounded-xl mt-4 shadow hover:shadow-md transition-all duration-150 ease-in-out overflow-x-hidden\"\n>\n  <div class=\"flex justify-between items-center mb-4\">\n    <h5 class=\"text-xl font-bold leading-none \">Running Exercises</h5>\n    <a href=\"/labs\" class=\"text-sm font-medium \"> View all </a>\n  </div>\n  {#if running_exercises.length === 0}\n    <div class=\"text-center text-sm font-medium leading-6 \">No running exercises</div>\n  {/if}\n  <div class=\"overflow-y-auto max-h-96 space-y-2\">\n    {#each running_exercises as running_exercise}\n      <div class=\"flex items-center\">\n        <div class=\"text-sm font-medium leading-6 \">{running_exercise.expand.exercise.title}</div>\n        <div class=\"relative ml-auto gap-2\">\n          <button class=\"btn-sm btn btn-outline\" on:click={() => openExercise(running_exercise)}>\n            <Terminal class=\"w-4 h-4 mr-1 inline-block\" />\n            Shell</button\n          >\n          <button\n            class=\"btn-sm btn btn-outline btn-error\"\n            on:click={() => stopExercise(running_exercise.expand.exercise.id)}\n          >\n            <Pause class=\"w-4 h-4 mr-1 inline-block\" />\n            Stop Exercise</button\n          >\n        </div>\n      </div>\n    {/each}\n  </div>\n</Card>\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/dashboard/data.ts",
    "content": "export const data: any = {\n    labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],\n    datasets: [\n      {\n        label: 'My First dataset',\n        fill: true,\n        lineTension: 0.3,\n        backgroundColor: 'rgba(225, 204,230, .3)',\n        borderColor: 'rgb(205, 130, 158)',\n        borderCapStyle: 'butt',\n        borderDash: [],\n        borderDashOffset: 0.0,\n        borderJoinStyle: 'miter',\n        pointBorderColor: 'rgb(205, 130,1 58)',\n        pointBackgroundColor: 'rgb(255, 255, 255)',\n        pointBorderWidth: 10,\n        pointHoverRadius: 5,\n        pointHoverBackgroundColor: 'rgb(0, 0, 0)',\n        pointHoverBorderColor: 'rgba(220, 220, 220,1)',\n        pointHoverBorderWidth: 2,\n        pointRadius: 1,\n        pointHitRadius: 10,\n        data: [65, 59, 80, 81, 56, 55, 40],\n      },\n      {\n        label: 'My Second dataset',\n        fill: true,\n        lineTension: 0.3,\n        backgroundColor: 'rgba(184, 185, 210, .3)',\n        borderColor: 'rgb(35, 26, 136)',\n        borderCapStyle: 'butt',\n        borderDash: [],\n        borderDashOffset: 0.0,\n        borderJoinStyle: 'miter',\n        pointBorderColor: 'rgb(35, 26, 136)',\n        pointBackgroundColor: 'rgb(255, 255, 255)',\n        pointBorderWidth: 10,\n        pointHoverRadius: 5,\n        pointHoverBackgroundColor: 'rgb(0, 0, 0)',\n        pointHoverBorderColor: 'rgba(220, 220, 220, 1)',\n        pointHoverBorderWidth: 2,\n        pointRadius: 1,\n        pointHitRadius: 10,\n        data: [28, 48, 40, 19, 86, 27, 90],\n      },\n    ],\n  };\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/labs/Exercise.svelte",
    "content": "<script lang=\"ts\">\n  import { goto } from \"$app/navigation\";\n  import { client } from \"$lib/pocketbase\";\n  import {\n    ExerciseSessionLogsTypeOptions,\n    type ExerciseSessionLogsRecord,\n    type ExerciseSessionsRecord,\n    type ExerciseSessionsResponse,\n    type ExercisesResponse\n  } from \"$lib/pocketbase/generated-types\";\n  import {\n    exercise,\n    exercise_sessions,\n    exercises,\n    getExerciseSessionByExercise\n  } from \"$lib/stores/data\";\n  import { loadingExercises } from \"$lib/stores/loading\";\n  import {\n    sidebarOpen,\n    sidebar_exercise_sessions,\n    sidebar_exercises,\n    sidebar_lab,\n    sidebar_lab_session\n  } from \"$lib/stores/sidebar\";\n  import { getDeltaTime } from \"$lib/utils/time\";\n  import { AlertTriangle, CheckCircle, Info, MoreHorizontal, Pause, Play, Terminal } from \"lucide-svelte\";\n  import { onMount } from \"svelte\";\n  import toast from \"svelte-french-toast\";\n  export let this_exercise_session: ExerciseSessionsResponse;\n  let this_exercise: ExercisesResponse;\n  let confirmation = false;\n\n  onMount(() => {\n    let exercise = $sidebar_exercises.find(\n      (exercise) => exercise.id === this_exercise_session.exercise\n    );\n    if (exercise) {\n      this_exercise = exercise;\n    }\n  });\n\n  async function stopExercise(exercise_id: string) {\n    const data: ExerciseSessionsRecord = {\n      // @ts-ignore\n      user: client.authStore.model?.id,\n      exercise: exercise_id,\n      startTime: new Date().toISOString(),\n      endTime: \"\",\n      agentRunning: false\n    };\n\n    loadingExercises.update((exercises) => {\n      exercises.add(exercise_id);\n      return new Set(exercises); // Required for Svelte's reactivity\n    });\n\n    // const labId = window.location.pathname.split(\"/\")[2];\n\n    const exercise_session_id = getExerciseSessionByExercise(exercise_id)?.id;\n\n    if (exercise_session_id) {\n      await client\n        .collection(\"exercise_sessions\")\n        .update(exercise_session_id, data)\n        // @ts-ignore\n        .then((response: any) => {\n          // goto(`/labs/${labId}/${exercise_id}`);\n          toast.success(\"Exercise stopped\");\n          this_exercise_session = response;\n          $sidebar_exercise_sessions = $sidebar_exercise_sessions.map((exercise_session) => {\n            if (exercise_session.id === exercise_session_id) {\n              return response;\n            }\n            return exercise_session;\n          });\n\n          exercise_sessions.update((exercise_sessions) => {\n            return exercise_sessions.map((exercise_session) => {\n              if (exercise_session.id === exercise_session_id) {\n                return response;\n              }\n              return exercise_session;\n            });\n          });\n        })\n        .catch((error) => {\n          toast.error(error.message);\n        })\n        .finally(() => {\n          loadingExercises.update((exercises) => {\n            exercises.delete(exercise_id);\n            return new Set(exercises); // Required for Svelte's reactivity\n          });\n          confirmation = false;\n        });\n    }\n  }\n\n  async function startExercise(exercise_id: string) {\n    const data: ExerciseSessionsRecord = {\n      // @ts-ignore\n      user: client.authStore.model?.id,\n      exercise: exercise_id,\n      startTime: new Date().toISOString(),\n      endTime: \"\",\n      agentRunning: true\n    };\n\n    loadingExercises.update((exercises) => {\n      exercises.add(exercise_id);\n      return new Set(exercises); // Required for Svelte's reactivity\n    });\n\n    // const labId = window.location.pathname.split(\"/\")[2];\n\n    const exercise_session_id = getExerciseSessionByExercise(exercise_id)?.id;\n    if (exercise_session_id) {\n      await client\n        .collection(\"exercise_sessions\")\n        .update(exercise_session_id, data)\n        // @ts-ignore\n        .then((response: any) => {\n          toast.success(\"Exercise started\");\n          this_exercise_session = response;\n          $sidebar_exercise_sessions = $sidebar_exercise_sessions.map((exercise_session) => {\n            if (exercise_session.id === exercise_session_id) {\n              return response;\n            }\n            return exercise_session;\n          });\n\n          exercise_sessions.update((exercise_sessions) => {\n            return exercise_sessions.map((exercise_session) => {\n              if (exercise_session.id === exercise_session_id) {\n                return response;\n              }\n              return exercise_session;\n            });\n          });\n\n          // make an entry in the exercise_session_logs collection\n\n          const exercise_session_log_data: ExerciseSessionLogsRecord = {\n            // @ts-ignore\n            user: client.authStore.model?.id,\n            exercise_session: exercise_session_id,\n            type: ExerciseSessionLogsTypeOptions.start,\n            timestamp: new Date().toISOString()\n          };\n\n          client\n            .collection(\"exercise_session_logs\")\n            .create(exercise_session_log_data)\n            .then((response) => {})\n            .catch((error) => {\n              console.log(error);\n            });\n        })\n        .catch((error) => {\n          toast.error(error.message);\n        })\n        .finally(() => {\n          loadingExercises.update((exercises) => {\n            exercises.delete(exercise_id);\n            return new Set(exercises); // Required for Svelte's reactivity\n          });\n        });\n    }\n  }\n</script>\n\n{#if this_exercise}\n  <div class=\"overflow-hidden rounded-xl border-2 h-auto\">\n    <div class=\"flex items-center gap-x-4 border-b-2 p-6\">\n      <div class=\"text-sm font-medium leading-6 \">\n        {this_exercise.title}\n      </div>\n      {#if $sidebar_lab_session.clusterRunning}\n        <div class=\"relative ml-auto dropdown dropdown-end dropdown-bottom\">\n          <button class=\"btn btn-neutral flex justify-center items-center  relative\">\n            {#if $loadingExercises.has(this_exercise.id)}\n              <span class=\"capitalize\">Actions</span>\n              <span class=\"loading loading-dots loading-sm inline-block p-2\" />\n              <ul class=\"dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52 gap-2\">\n                <li>\n                  <!-- exercise is starting... -->\n                  <button class=\"border-2 \">\n                    <Info class=\"w-4 h-4 mr-1 inline-block\" />\n                    Starting Exercise</button\n                  >\n                </li>\n              </ul>\n            {:else}\n              <button class=\"-m-3 block p-2.5\">\n                Actions <MoreHorizontal class=\"w-6 h-6 inline-block\" strokeWidth={3} />\n              </button>\n              <ul class=\"dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52 gap-2\">\n                <li>\n                  <button\n                    on:click={() => {\n                      sidebarOpen.set(false);\n                      exercise.set(this_exercise);\n                      exercises.set($sidebar_exercises);\n                      new Promise((resolve) => setTimeout(resolve, 100)).then(() =>\n                        goto(`/labs/${$sidebar_lab.id}/${this_exercise.id}`)\n                      );\n                    }}\n                    class=\"border-2 text-primary\"\n                  >\n                    <Terminal class=\"w-4 h-4 mr-1 inline-block\" />\n                    Shell</button\n                  >\n                </li>\n                {#if this_exercise_session.agentRunning}\n                  <li>\n                    {#if confirmation}\n                    <button\n                    class=\"border-2 text-warning border-warning hover:bg-warning hover:text-primary\"\n                      on:click={() => stopExercise(this_exercise.id)}\n                    >\n                      <AlertTriangle class=\"w-5 h-5 mr-2 inline-block\" />\n                      Are you sure?\n                    </button>\n                    {:else}\n                    <div\n                      class=\"border-2 text-error border-error hover:bg-error hover:text-primary\"\n                      on:click={() => (confirmation = true)}\n                    >\n                      <Pause class=\"w-4 h-4 mr-1 inline-block\" />\n                      Stop Exercise</div\n                    >\n                    {/if}\n                  </li>\n                {:else}\n                  <li>\n                    <button\n                      class=\"border-2 text-success border-success hover:bg-success hover:text-primary\"\n                      on:click={() => startExercise(this_exercise.id)}\n                    >\n                      <Play class=\"w-4 h-4 mr-1 inline-block\" />\n                      Start Exercise</button\n                    >\n                  </li>\n                {/if}\n              </ul>\n            {/if}\n            {#if this_exercise_session.agentRunning}\n              <span class=\"absolute flex h-4 w-4 -top-2 -right-2\">\n                <span\n                  class=\"animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-75\"\n                />\n                <span class=\"relative inline-flex rounded-full h-4 w-4 bg-success\" />\n              </span>\n            {/if}\n          </button>\n        </div>\n      {:else}\n        <p class=\"text-error text-sm relative ml-auto\">Lab not running</p>\n      {/if}\n    </div>\n    <dl class=\"-my-3 divide-y divide-gray-100 px-6 py-4 text-sm leading-6\">\n      <div class=\"flex justify-between gap-x-4 py-3\">\n        <dt class=\"\">Status</dt>\n        <dd class=\"badge badge-outline {this_exercise_session.agentRunning ? 'badge-success' : ''}\">\n          {#if this_exercise_session.agentRunning}\n            <Play class=\"w-4 h-4 mr-1 inline-block\" />\n          {:else}\n            <Pause class=\"w-4 h-4 mr-1 inline-block\" />\n          {/if}\n          {this_exercise_session.agentRunning ? \"Running\" : \"Stopped\"}\n        </dd>\n      </div>\n      <div class=\"flex justify-between gap-x-4 py-3\">\n        <dt class=\"\">Done</dt>\n        <dd class={this_exercise_session.agentRunning ? \"text-success\" : \"text-gray-400\"}>\n          <span class=\"ml-1 text-xs\">\n            {#if this_exercise_session.endTime && !this_exercise_session.agentRunning}\n              <span class=\"text-success\">\n                <CheckCircle class=\"w-5 h-5 inline-block\" />\n                {getDeltaTime(this_exercise_session.startTime, this_exercise_session.endTime)}\n              </span>\n            {:else}\n              <span class=\"text-gray-400\">not yet</span>\n            {/if}\n          </span>\n        </dd>\n      </div>\n    </dl>\n  </div>\n{/if}\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/labs/Lab.svelte",
    "content": "<script lang=\"ts\">\n  import type {\n    ExerciseSessionsRecord,\n    ExerciseSessionsResponse,\n    ExercisesResponse,\n    LabSessionsResponse,\n    LabsResponse\n  } from \"$lib/pocketbase/generated-types\";\n  import { getExerciseSessionByExercise } from \"$lib/stores/data\";\n  import { loadingExercises } from \"$lib/stores/loading\";\n  import {\n    sidebar_exercise_sessions,\n    sidebar_exercises,\n    sidebar_lab,\n    sidebar_lab_session\n  } from \"$lib/stores/sidebar\";\n  import { getTimeAgo } from \"$lib/utils/time\";\n  import { CheckCircle, Inspect, Pause, Play, XCircle } from \"lucide-svelte\";\n\n  export let this_lab: LabsResponse = $sidebar_lab || {};\n  export let this_lab_session: LabSessionsResponse = $sidebar_lab_session || {};\n  export let this_exercises: ExercisesResponse[] = $sidebar_exercises || [];\n  export let this_exercise_sessions: ExerciseSessionsResponse[] = $sidebar_exercise_sessions || [];\n\n  $: console.log(this_lab_session);\n\n  function getDoneExercises() {\n    let done_exercises: ExercisesResponse[] = [];\n    // each exercise_session which has an endTimestamp is done\n    this_exercise_sessions.forEach((exercise_session) => {\n      if (exercise_session.endTime) {\n        let exercise = this_exercises.find((exercise) => exercise.id === exercise_session.exercise);\n        if (exercise) {\n          done_exercises.push(exercise);\n        }\n      }\n    });\n    return done_exercises;\n  }\n\n  function handleSideBar() {\n    drawerHidden = !drawerHidden;\n    if (!drawerHidden) {\n      sidebar_lab.set(this_lab);\n      sidebar_lab_session.set(this_lab_session);\n      sidebar_exercises.set(this_exercises);\n      sidebar_exercise_sessions.set(this_exercise_sessions);\n      return;\n    }\n    sidebar_lab.set(this_lab);\n    sidebar_lab_session.set(this_lab_session);\n    sidebar_exercises.set(this_exercises);\n    sidebar_exercise_sessions.set(this_exercise_sessions);\n  }\n\n  export let drawerHidden = true;\n</script>\n\n{#if this_lab_session}\n  {#key this_lab_session}\n    <div class=\"rounded-xl relative bg-white dark:bg-transparent dark:border-2 \">\n      <div class=\"flex items-center gap-x-4 border-b-2 p-6\">\n        <div class=\"text-lg font-bold leading-6\">{this_lab.title}</div>\n        <div class=\"relative ml-auto\">\n          <button class=\"btn btn-outline\" on:click={() => handleSideBar()}>\n            <Inspect />\n          </button>\n\n          {#if this_lab_session.clusterRunning}\n            <span class=\"absolute flex h-4 w-4 -top-1 -right-1\">\n              <span\n                class=\"animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-75\"\n              />\n              <span class=\"relative inline-flex rounded-full h-4 w-4 bg-success\" />\n            </span>\n          {/if}\n        </div>\n      </div>\n      <dl class=\"-my-3 divide-y divide-gray-100 px-6 py-4 text-sm leading-6\">\n        <div class=\"flex justify-between gap-x-4 py-3\">\n          <dt class=\"\">Status</dt>\n          <dd class=\"badge badge-outline {this_lab_session.clusterRunning ? 'badge-success' : ''}\">\n            {#if this_lab_session.clusterRunning}\n              <Play class=\"w-4 h-4 mr-1 inline-block\" />\n            {:else}\n              <Pause class=\"w-4 h-4 mr-1 inline-block\" />\n            {/if}\n            {this_lab_session.clusterRunning ? \"Running\" : \"Stopped\"}\n          </dd>\n        </div>\n        <div class=\"flex justify-between gap-x-4 py-3\">\n          <dt class=\"\">Time</dt>\n          <dd class={this_lab_session.clusterRunning ? \"text-success\" : \"text-gray-400\"}>\n            <span class=\"ml-1 text-xs text-primary\">\n              {#if this_lab_session.clusterRunning && this_lab_session.startTime}\n                since\n                {getTimeAgo(this_lab_session.startTime)}\n              {:else if this_lab_session.endTime && !this_lab_session.clusterRunning}\n                {getTimeAgo(this_lab_session.endTime)}\n                ago\n              {/if}\n            </span>\n          </dd>\n        </div>\n        <div class=\"flex justify-between gap-x-4 py-3\">\n          <dt class=\"\">Done Exercises</dt>\n          <dd class=\"flex items-start gap-x-2\">\n            {#if getDoneExercises().length == this_exercises.length}\n              <div class=\"text-success\">\n                <CheckCircle />\n              </div>\n            {:else}\n              <div class=\"text-red-500\">\n                <XCircle />\n              </div>\n            {/if}\n            <div\n              class=\"font-medium\n            {getDoneExercises().length == this_exercises.length ? 'text-success' : 'text-red-500'}\n          \"\n            >\n              {getDoneExercises().length} / {this_exercises.length}\n            </div>\n          </dd>\n        </div>\n      </dl>\n    </div>\n  {/key}\n{/if}\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/landingpage/Companies.svelte",
    "content": "<script lang=\"ts\">\n  import type { CompaniesResponse } from \"$lib/pocketbase/generated-types\";\n\n  export let companies: CompaniesResponse[] = [];\n\n  function imagePath(record: CompaniesResponse) {\n    return `/api/files/${record.collectionId}/${record.id}/${record.logo}`;\n  }\n</script>\n\n<div class=\"mx-auto max-w-7xl px-6 lg:px-8\">\n  <div\n    class=\"mx-auto grid max-w-lg grid-cols-4 items-center gap-x-8 gap-y-12 sm:max-w-xl sm:grid-cols-6 sm:gap-x-10 sm:gap-y-14 lg:mx-0 lg:max-w-none lg:grid-cols-5\"\n  >\n    {#if companies.length > 0}\n      {#each companies as company}\n        <img\n          class=\"col-span-2 max-h-14 w-full object-contain lg:col-span-1\"\n          src={imagePath(company)}\n          alt={company.name}\n          width=\"158\"\n          height=\"48\"\n        />\n      {/each}\n    {/if}\n  </div>\n  <div class=\"mt-16 flex justify-center\">\n    <p\n      class=\"relative rounded-full px-4 py-1.5 text-sm leading-6  ring-1 ring-inset ring-gray-900/10 dark:ring-gray-100/10\"\n    >\n      <span class=\"hidden md:inline\"\n        >Swisscom trains their employees on Kubernetes by working with us.</span\n      >\n      <a\n        href=\"https://natron.io/blog/kubernetes-mastery-swisscom-kubelab-workshop\"\n        class=\"font-semibold\"\n        ><span class=\"absolute inset-0\" aria-hidden=\"true\" /> Read our blog\n        <span aria-hidden=\"true\">&rarr;</span></a\n      >\n    </p>\n  </div>\n</div>\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/landingpage/Cta.svelte",
    "content": "<div class=\"relative -z-10 mt-32 px-6 lg:px-8\">\n  <div\n    class=\"absolute inset-x-0 top-1/2 -z-10 flex -translate-y-1/2 transform-gpu justify-center overflow-hidden blur-3xl sm:bottom-0 sm:right-[calc(50%-6rem)] sm:top-auto sm:translate-y-0 sm:transform-gpu sm:justify-end\"\n    aria-hidden=\"true\"\n  >\n    <div\n      class=\"aspect-[1108/632] w-[69.25rem] flex-none bg-gradient-to-r from-black to-white opacity-25\"\n      style=\"clip-path: polygon(73.6% 48.6%, 91.7% 88.5%, 100% 53.9%, 97.4% 18.1%, 92.5% 15.4%, 75.7% 36.3%, 55.3% 52.8%, 46.5% 50.9%, 45% 37.4%, 50.3% 13.1%, 21.3% 36.2%, 0.1% 0.1%, 5.4% 49.1%, 21.4% 36.4%, 58.9% 100%, 73.6% 48.6%)\"\n    />\n  </div>\n  <div class=\"mx-auto max-w-2xl text-center\">\n    <h2 class=\"text-3xl font-bold tracking-tight  sm:text-4xl\">\n      Ready to Dive into Kubernetes Mastery?\n    </h2>\n    <p class=\"mx-auto mt-6 max-w-xl text-lg leading-8 \">\n      Join KubeLab today and elevate your learning experience with hands-on, real-world Kubernetes\n      labs and workshops.\n    </p>\n    <div class=\"mt-10 flex items-center justify-center gap-x-6\">\n      <a href=\"/signup/\" class=\"btn btn-neutral \">Get started</a>\n      <a\n        href=\"https://github.com/natrontech/kubelab\"\n        class=\"text-sm font-semibold leading-6 \"\n        >Learn more <span aria-hidden=\"true\">→</span></a\n      >\n    </div>\n  </div>\n  <div\n    class=\"absolute left-1/2 right-0 top-full -z-10 hidden -translate-y-1/2 transform-gpu overflow-hidden blur-3xl sm:block\"\n    aria-hidden=\"true\"\n  >\n    <div\n      class=\"aspect-[1155/678] w-[72.1875rem] bg-gradient-to-tr from-black to-white opacity-30\"\n      style=\"clip-path: polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)\"\n    />\n  </div>\n</div>\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/landingpage/Faq.svelte",
    "content": "<script lang=\"ts\">\n  import type { FaqsResponse } from \"$lib/pocketbase/generated-types\";\n\n  export let faqs: FaqsResponse[] = [];\n</script>\n\n<div\n  class=\"mx-auto max-w-2xl divide-y divide-gray-900/10 px-6 pb-8 sm:pb-24 sm:pt-12 lg:max-w-7xl lg:px-8 lg:pb-32\"\n>\n  <h2 class=\"text-2xl font-bold leading-10 tracking-tight \">\n    Frequently asked questions\n  </h2>\n  {#each faqs as faq}\n    <dl class=\"mt-10 space-y-8 divide-y divide-gray-900/10\">\n      <div class=\"pt-8 lg:grid lg:grid-cols-12 lg:gap-8\">\n        <dt class=\"text-base font-semibold leading-7 lg:col-span-5\">\n          {faq.question}\n        </dt>\n        <dd class=\"mt-4 lg:col-span-7 lg:mt-0\">\n          <p class=\"text-base leading-7\">{faq.answer}</p>\n        </dd>\n      </div>\n    </dl>\n  {/each}\n</div>\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/landingpage/Features.svelte",
    "content": "<script>\n  import { PocketKnife, RefreshCcw, ShipWheel, Terminal } from \"lucide-svelte\";\n</script>\n\n<div class=\"mx-auto mt-32 max-w-7xl px-6  lg:px-8\">\n  <div class=\"mx-auto max-w-2xl lg:text-center\">\n    <h2 class=\"text-base font-semibold leading-7\">Learn faster</h2>\n    <p class=\"mt-2 text-3xl font-bold tracking-tight  sm:text-4xl\">\n      Discover KubeLab's Cutting-Edge Features\n    </p>\n    <p class=\"mt-6 text-lg leading-8 \">\n      Delve deep into KubeLab's suite of tools and functionalities tailored to enhance your\n      Kubernetes learning journey.\n    </p>\n  </div>\n  <div class=\"mx-auto mt-16 max-w-2xl sm:mt-20 lg:mt-24 lg:max-w-4xl\">\n    <dl class=\"grid max-w-xl grid-cols-1 gap-x-8 gap-y-10 lg:max-w-none lg:grid-cols-2 lg:gap-y-16\">\n      <div class=\"relative pl-16\">\n        <dt class=\"text-base font-semibold leading-7 \">\n          <div\n            class=\"absolute left-0 top-0 flex h-10 w-10 items-center justify-center rounded-lg bg-neutral\"\n          >\n            <Terminal class=\"h-6 w-6 text-white\" />\n          </div>\n          Web Terminal\n        </dt>\n        <dd class=\"mt-2 text-base leading-7\">\n          KubeLab offers a state-of-the-art in-browser terminal, enabling users to execute commands\n          and interact directly with Kubernetes clusters. This seamless experience means there's no\n          need for any additional software or setup - just dive right in!\n        </dd>\n      </div>\n      <div class=\"relative pl-16\">\n        <dt class=\"text-base font-semibold leading-7\">\n          <div\n            class=\"absolute left-0 top-0 flex h-10 w-10 items-center justify-center rounded-lg bg-neutral\"\n          >\n            <ShipWheel class=\"h-6 w-6 text-white\" />\n          </div>\n          Dedicated Cluster Per Session\n        </dt>\n        <dd class=\"mt-2 text-base leading-7 \">\n          For each learning session on KubeLab, users receive their own isolated Kubernetes cluster.\n          This ensures a private, secure, and uninterrupted learning space where you can explore and\n          experiment with Kubernetes functionalities without affecting or being affected by other\n          learners.\n        </dd>\n      </div>\n      <div class=\"relative pl-16\">\n        <dt class=\"text-base font-semibold leading-7 \">\n          <div\n            class=\"absolute left-0 top-0 flex h-10 w-10 items-center justify-center rounded-lg bg-neutral\"\n          >\n            <RefreshCcw class=\"h-6 w-6 text-white\" />\n          </div>\n          Regularly Updated Lab Exercises\n        </dt>\n        <dd class=\"mt-2 text-base leading-7 \">\n          Staying updated is crucial in the fast-evolving tech world. At KubeLab, our labs are not\n          just comprehensive but are also updated regularly. As Kubernetes continues to evolve, so\n          do our exercises, ensuring you always have the latest knowledge and best practices at\n          hand.\n        </dd>\n      </div>\n      <div class=\"relative pl-16\">\n        <dt class=\"text-base font-semibold leading-7 \">\n          <div\n            class=\"absolute left-0 top-0 flex h-10 w-10 items-center justify-center rounded-lg bg-neutral\"\n          >\n            <PocketKnife class=\"h-6 w-6 text-white\" />\n          </div>\n          Swiss Hosted\n        </dt>\n        <dd class=\"mt-2 text-base leading-7 \">\n          Security and data protection are our top priorities. KubeLab is hosted in Switzerland at <a\n            class=\" font-bold\"\n            href=\"https://natron.io\">Natron Tech</a\n          >'s Cloud, known for its stringent data protection laws and top-tier infrastructure. This\n          guarantees both high performance and peace of mind for our users, knowing that their data\n          and learning environment are hosted in one of the world's most secure locations.\n        </dd>\n      </div>\n    </dl>\n  </div>\n</div>\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/landingpage/Footer.svelte",
    "content": "<div class=\"mx-auto mt-32 max-w-7xl px-6 lg:px-8\">\n  <footer\n    aria-labelledby=\"footer-heading\"\n    class=\"relative border-t border-gray-900/10 py-24 sm:mt-56 sm:py-32\"\n  >\n    <h2 id=\"footer-heading\" class=\"sr-only\">Footer</h2>\n    <div class=\"xl:grid xl:grid-cols-3 xl:gap-8\">\n      <img\n        class=\"h-7\"\n        src=\"https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600\"\n        alt=\"Company name\"\n      />\n      <div class=\"mt-16 grid grid-cols-2 gap-8 xl:col-span-2 xl:mt-0\">\n        <div class=\"md:grid md:grid-cols-2 md:gap-8\">\n          <div>\n            <h3 class=\"text-sm font-semibold leading-6 text-gray-900\">Solutions</h3>\n            <ul role=\"list\" class=\"mt-6 space-y-4\">\n              <li>\n                <a href=\"#\" class=\"text-sm leading-6 text-gray-600 hover:text-gray-900\">Hosting</a>\n              </li>\n              <li>\n                <a href=\"#\" class=\"text-sm leading-6 text-gray-600 hover:text-gray-900\"\n                  >Data Services</a\n                >\n              </li>\n              <li>\n                <a href=\"#\" class=\"text-sm leading-6 text-gray-600 hover:text-gray-900\"\n                  >Uptime Monitoring</a\n                >\n              </li>\n              <li>\n                <a href=\"#\" class=\"text-sm leading-6 text-gray-600 hover:text-gray-900\"\n                  >Enterprise Services</a\n                >\n              </li>\n            </ul>\n          </div>\n          <div class=\"mt-10 md:mt-0\">\n            <h3 class=\"text-sm font-semibold leading-6 text-gray-900\">Support</h3>\n            <ul role=\"list\" class=\"mt-6 space-y-4\">\n              <li>\n                <a href=\"#\" class=\"text-sm leading-6 text-gray-600 hover:text-gray-900\">Pricing</a>\n              </li>\n              <li>\n                <a href=\"#\" class=\"text-sm leading-6 text-gray-600 hover:text-gray-900\"\n                  >Documentation</a\n                >\n              </li>\n              <li>\n                <a href=\"#\" class=\"text-sm leading-6 text-gray-600 hover:text-gray-900\">Guides</a>\n              </li>\n              <li>\n                <a href=\"#\" class=\"text-sm leading-6 text-gray-600 hover:text-gray-900\"\n                  >API Reference</a\n                >\n              </li>\n            </ul>\n          </div>\n        </div>\n        <div class=\"md:grid md:grid-cols-2 md:gap-8\">\n          <div>\n            <h3 class=\"text-sm font-semibold leading-6 text-gray-900\">Company</h3>\n            <ul role=\"list\" class=\"mt-6 space-y-4\">\n              <li>\n                <a href=\"#\" class=\"text-sm leading-6 text-gray-600 hover:text-gray-900\">About</a>\n              </li>\n              <li>\n                <a href=\"#\" class=\"text-sm leading-6 text-gray-600 hover:text-gray-900\">Blog</a>\n              </li>\n              <li>\n                <a href=\"#\" class=\"text-sm leading-6 text-gray-600 hover:text-gray-900\">Jobs</a>\n              </li>\n              <li>\n                <a href=\"#\" class=\"text-sm leading-6 text-gray-600 hover:text-gray-900\">Press</a>\n              </li>\n              <li>\n                <a href=\"#\" class=\"text-sm leading-6 text-gray-600 hover:text-gray-900\">Partners</a>\n              </li>\n            </ul>\n          </div>\n          <div class=\"mt-10 md:mt-0\">\n            <h3 class=\"text-sm font-semibold leading-6 text-gray-900\">Legal</h3>\n            <ul role=\"list\" class=\"mt-6 space-y-4\">\n              <li>\n                <a href=\"#\" class=\"text-sm leading-6 text-gray-600 hover:text-gray-900\">Claim</a>\n              </li>\n              <li>\n                <a href=\"#\" class=\"text-sm leading-6 text-gray-600 hover:text-gray-900\">Privacy</a>\n              </li>\n              <li>\n                <a href=\"#\" class=\"text-sm leading-6 text-gray-600 hover:text-gray-900\">Terms</a>\n              </li>\n            </ul>\n          </div>\n        </div>\n      </div>\n    </div>\n  </footer>\n</div>\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/landingpage/Header.svelte",
    "content": "<script lang=\"ts\">\n  import darkTheme from \"$lib/stores/theme\";\n  import { Moon, Sun } from \"lucide-svelte\";\n</script>\n\n<header class=\"absolute inset-x-0 top-0 z-50\">\n  <nav class=\"flex items-center justify-between p-6 lg:px-8\" aria-label=\"Global\">\n    <div class=\"flex lg:flex-1\">\n      <a href=\"https://kubelab.ch\" class=\"-m-1.5 p-1.5\">\n        <span class=\"sr-only \">KubeLab</span>\n        <img class=\"h-8 w-auto\" src={\"/images/kubelab-logo.png\"} alt=\"\" />\n      </a>\n      <p class=\"text-lg font-bold pt-1 pl-2 \">KubeLab</p>\n    </div>\n    <div class=\"flex items-center gap-x-6\">\n      <!-- powered by label -->\n      <a href=\"https://natron.io\" target=\"_blank\">\n        <span class=\"text-sm font-semibold leading-6\">Powered by</span>\n        {#if $darkTheme === true}\n          <img class=\"h-8 w-auto\" src={\"/images/natron-dark.png\"} alt=\"Switzerland\" />\n        {:else}\n          <img class=\"h-8 w-auto\" src={\"/images/natron.png\"} alt=\"Switzerland\" />\n        {/if}\n      </a>\n      <button class=\"btn bg-transparent border-none\" on:click={() => darkTheme.set(!$darkTheme)}>\n        {#if $darkTheme === true}\n          <Sun />\n        {:else}\n          <Moon />\n        {/if}\n      </button>\n    </div>\n  </nav>\n</header>\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/landingpage/Hero.svelte",
    "content": "<script>\n  import darkTheme from \"$lib/stores/theme\";\n</script>\n\n<div class=\"relative pt-14\">\n  <div\n    class=\"absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-80\"\n    aria-hidden=\"true\"\n  >\n    <div\n      class=\"relative left-[calc(50%-11rem)] aspect-[1155/678] w-[36.125rem] -translate-x-1/2 rotate-[30deg] bg-gradient-to-tr from-black to-white opacity-30 sm:left-[calc(50%-30rem)] sm:w-[72.1875rem]\"\n      style=\"clip-path: polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)\"\n    />\n  </div>\n  <div class=\"py-24 sm:py-32\">\n    <div class=\"mx-auto max-w-7xl px-6 lg:px-8\">\n      <div class=\"mx-auto max-w-2xl text-center\">\n        <h1 class=\"text-4xl font-bold tracking-tight sm:text-6xl\">\n          The Ultimate Kubernetes Learning Platform\n        </h1>\n        <p class=\"mt-6 text-lg leading-8\">\n          Embark on your Kubernetes Journey through Hands-on Practice.\n        </p>\n        <div class=\"mt-10 flex items-center justify-center gap-x-6\">\n          <a href=\"/login/\" class=\"btn btn-neutral \">Get started</a>\n          <a href=\"https://github.com/natrontech/kubelab\" class=\"text-sm font-semibold leading-6 \"\n            >Learn more <span aria-hidden=\"true\">→</span></a\n          >\n        </div>\n      </div>\n      <div class=\"mt-16 flow-root sm:mt-24\">\n        <div\n          class=\"-m-2 rounded-xl bg-gray-900/5 p-2 ring-1 ring-inset ring-gray-900/10 lg:-m-4 lg:rounded-2xl lg:p-4\"\n        >\n          {#if $darkTheme === true}\n            <img\n              src=\"/assets/screenshot-dark.png\"\n              alt=\"App screenshot\"\n              width=\"2432\"\n              height=\"1442\"\n              class=\"rounded-md shadow-2xl ring-1 ring-gray-900/10\"\n            />\n          {:else}\n            <img\n              src=\"/assets/screenshot.png\"\n              alt=\"App screenshot\"\n              width=\"2432\"\n              height=\"1442\"\n              class=\"rounded-md shadow-2xl ring-1 ring-gray-900/10\"\n            />\n          {/if}\n        </div>\n      </div>\n    </div>\n  </div>\n  <div\n    class=\"absolute inset-x-0 top-[calc(100%-13rem)] -z-10 transform-gpu overflow-hidden blur-3xl sm:top-[calc(100%-30rem)]\"\n    aria-hidden=\"true\"\n  >\n    <div\n      class=\"relative left-[calc(50%+3rem)] aspect-[1155/678] w-[36.125rem] -translate-x-1/2 bg-gradient-to-tr from-white to-black opacity-30 sm:left-[calc(50%+36rem)] sm:w-[72.1875rem]\"\n      style=\"clip-path: polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)\"\n    />\n  </div>\n</div>\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/markdown/CodeComponent.svelte",
    "content": "<script lang=\"ts\">\n  import { Copy } from \"lucide-svelte\";\n  import Prism from \"prismjs\";\n  import toast from \"svelte-french-toast\";\n  export let text: string;\n\n  let language = \"javascript\";\n\n  function copyCode() {\n    navigator.clipboard.writeText(text);\n    toast.success(\"Code copied to clipboard\");\n  }\n</script>\n\n<div class=\"code bg-base-200 p-2 my-2 rounded-md relative\">\n  {@html Prism.highlight(text, Prism.languages[language], language)}\n  <button class=\"absolute top-2 right-2\" on:click={copyCode}>\n    <Copy class=\"\" size={16} />\n  </button>\n</div>\n\n<style>\n  .code {\n    white-space: pre-wrap;\n  }\n</style>\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/markdown/CodeSpanComponent.svelte",
    "content": "<script lang=\"ts\">\n  import { Copy } from \"lucide-svelte\";\n    import toast from \"svelte-french-toast\";\n\n  export let raw: string;\n\n  function copyCode() {\n    navigator.clipboard.writeText(raw.replace(/`/g, \"\"));\n    toast.success(\"Code copied to clipboard\");\n  }\n</script>\n\n<button\n  class=\"btn btn-sm btn-neutral dark:btn-primary dark:text-neutral\"\n  on:click={copyCode}\n>\n  <code>{raw.replace(/`/g, \"\")}</code>\n  <Copy class=\"inline ml-2\" size={16} />\n</button>\n\n<style>\n  .btn {\n    /* disable caps lock */\n    text-transform: none;\n  }\n\n</style>\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/markdown/LinkComponent.svelte",
    "content": "<script lang=\"ts\">\n    import { ExternalLink } from \"lucide-svelte\";\n\n  export let href: string = \"\";\n  export let title: string = \"\";\n</script>\n\n<a {href} {title} target=\"_blank\">\n  <div class=\"btn btn-sm btn-info\">\n    <slot />\n    <ExternalLink class=\"inline ml-2\" size={16} />\n  </div>\n</a>\n\n<style>\n  .btn {\n    /* disable caps lock */\n    text-transform: none;\n  }\n</style>\n"
  },
  {
    "path": "kubelab-ui/src/lib/components/markdown/ListComponent.svelte",
    "content": "<script lang=\"ts\">\n</script>\n\n<slot />\n\n<style>\n</style>\n"
  },
  {
    "path": "kubelab-ui/src/lib/config.ts",
    "content": "export const site = {\n    name: \"KubeLab\",\n    description:\n        \"KubeLab is a state-of-the-art web platform offering an immersive collection of hands-on labs designed exclusively for Kubernetes workshops. Our aim is to transform your Kubernetes learning journey into an interactive, engaging, and practical experience, helping you understand and apply complex concepts in real-world scenarios.\"\n};\n"
  },
  {
    "path": "kubelab-ui/src/lib/mock-data.ts",
    "content": "import { DeploymentStatus, type Deployment } from \"./types\";\n\nexport const mockDeployments: Deployment[] = [\n    {\n        id: \"1\",\n        name: \"nginx-deployment\",\n        status: DeploymentStatus.Running,\n        namespace: \"default\"\n    },\n    {\n        id: \"2\",\n        name: \"nginx-deployment\",\n        status: DeploymentStatus.Failed,\n        namespace: \"kube-system\"\n    },\n    {\n        id: \"3\",\n        name: \"nginx-deployment\",\n        status: DeploymentStatus.Pending,\n        namespace: \"kube-system\"\n    },\n    {\n        id: \"4\",\n        name: \"nginx-deployment\",\n        status: DeploymentStatus.Unknown,\n        namespace: \"kube-system\"\n    }\n];\n\nexport const mockDeploymentData = {\n    id: \"1\",\n    name: \"deployment1-test\",\n    base: {\n        _values: {\n            replicas: 3,\n            image: {\n                registry: \"docker.io\",\n                repository: \"bitnami/nginx\",\n                tag: \"1.24.0-debian-11-r6\"\n            },\n            ingress: {\n                enabled: true\n            }\n        },\n        _secrets: {},\n        test: {\n            values: {\n                ingress: {\n                    hostname: \"test.example.com\",\n                    path: \"/\"\n                }\n            },\n            _secrets: {}\n        }\n    }\n};\n"
  },
  {
    "path": "kubelab-ui/src/lib/pocketbase/generated-types.ts",
    "content": "/**\n* This file was @generated using pocketbase-typegen\n*/\n\nexport enum Collections {\n\tCompanies = \"companies\",\n\tExerciseSessionLogs = \"exercise_session_logs\",\n\tExerciseSessions = \"exercise_sessions\",\n\tExercises = \"exercises\",\n\tFaqs = \"faqs\",\n\tFeatures = \"features\",\n\tHooks = \"hooks\",\n\tLabSessions = \"lab_sessions\",\n\tLabs = \"labs\",\n\tNotifications = \"notifications\",\n\tPlans = \"plans\",\n\tUsers = \"users\",\n}\n\n// Alias types for improved usability\nexport type IsoDateString = string\nexport type RecordIdString = string\nexport type HTMLString = string\n\n// System fields\nexport type BaseSystemFields<T = never> = {\n\tid: RecordIdString\n\tcreated: IsoDateString\n\tupdated: IsoDateString\n\tcollectionId: string\n\tcollectionName: Collections\n\texpand?: T\n}\n\nexport type AuthSystemFields<T = never> = {\n\temail: string\n\temailVisibility: boolean\n\tusername: string\n\tverified: boolean\n} & BaseSystemFields<T>\n\n// Record types for each collection\n\nexport type CompaniesRecord = {\n\tname: string\n\tlogo: string\n}\n\nexport enum ExerciseSessionLogsTypeOptions {\n\t\"start\" = \"start\",\n\t\"end\" = \"end\",\n}\nexport type ExerciseSessionLogsRecord = {\n\tuser: RecordIdString\n\ttimestamp: IsoDateString\n\texercise_session: RecordIdString\n\ttype: ExerciseSessionLogsTypeOptions\n}\n\nexport type ExerciseSessionsRecord = {\n\tagentRunning?: boolean\n\tuser: RecordIdString\n\tstartTime?: IsoDateString\n\tendTime?: IsoDateString\n\texercise: RecordIdString\n}\n\nexport type ExercisesRecord = {\n\ttitle?: string\n\tdescription?: string\n\tdocs?: string\n\thint?: string\n\tsolution?: string\n\tcheck?: string\n\tbootstrap?: string\n\tlab: RecordIdString\n}\n\nexport type FaqsRecord = {\n\tquestion?: string\n\tanswer?: string\n}\n\nexport type FeaturesRecord = {\n\tfeature?: string\n}\n\nexport enum HooksEventOptions {\n\t\"insert\" = \"insert\",\n\t\"update\" = \"update\",\n\t\"delete\" = \"delete\",\n}\n\nexport enum HooksActionTypeOptions {\n\t\"command\" = \"command\",\n\t\"post\" = \"post\",\n}\nexport type HooksRecord = {\n\tcollection: string\n\tevent: HooksEventOptions\n\taction_type: HooksActionTypeOptions\n\taction: string\n\taction_params?: string\n\texpands?: string\n\tdisabled?: boolean\n}\n\nexport type LabSessionsRecord = {\n\tuser: RecordIdString\n\tstartTime?: IsoDateString\n\tendTime?: IsoDateString\n\tlab: RecordIdString\n\tclusterRunning?: boolean\n}\n\nexport type LabsRecord = {\n\ttitle?: string\n\tdescription?: string\n\tdocs?: string\n}\n\nexport enum NotificationsTypeOptions {\n\t\"help\" = \"help\",\n}\nexport type NotificationsRecord = {\n\ttype: NotificationsTypeOptions\n\tuser: RecordIdString\n\tdone?: boolean\n\texercise?: RecordIdString\n}\n\nexport type PlansRecord = {\n\tname: string\n\tdescription: string\n\tprice?: number\n\tfeatures: RecordIdString[]\n\toptionalFeatures?: RecordIdString[]\n}\n\nexport enum UsersRoleOptions {\n\t\"user\" = \"user\",\n\t\"admin\" = \"admin\",\n}\nexport type UsersRecord = {\n\tname?: string\n\tavatar?: string\n\tplan?: RecordIdString\n\trole: UsersRoleOptions\n\tcompany?: RecordIdString\n\tworkshop?: boolean\n}\n\n// Response types include system fields and match responses from the PocketBase API\nexport type CompaniesResponse = Required<CompaniesRecord> & BaseSystemFields\nexport type ExerciseSessionLogsResponse<Texpand = unknown> = Required<ExerciseSessionLogsRecord> & BaseSystemFields<Texpand>\nexport type ExerciseSessionsResponse<Texpand = unknown> = Required<ExerciseSessionsRecord> & BaseSystemFields<Texpand>\nexport type ExercisesResponse<Texpand = unknown> = Required<ExercisesRecord> & BaseSystemFields<Texpand>\nexport type FaqsResponse = Required<FaqsRecord> & BaseSystemFields\nexport type FeaturesResponse = Required<FeaturesRecord> & BaseSystemFields\nexport type HooksResponse = Required<HooksRecord> & BaseSystemFields\nexport type LabSessionsResponse<Texpand = unknown> = Required<LabSessionsRecord> & BaseSystemFields<Texpand>\nexport type LabsResponse = Required<LabsRecord> & BaseSystemFields\nexport type NotificationsResponse<Texpand = unknown> = Required<NotificationsRecord> & BaseSystemFields<Texpand>\nexport type PlansResponse<Texpand = unknown> = Required<PlansRecord> & BaseSystemFields<Texpand>\nexport type UsersResponse<Texpand = unknown> = Required<UsersRecord> & AuthSystemFields<Texpand>\n\n// Types containing all Records and Responses, useful for creating typing helper functions\n\nexport type CollectionRecords = {\n\tcompanies: CompaniesRecord\n\texercise_session_logs: ExerciseSessionLogsRecord\n\texercise_sessions: ExerciseSessionsRecord\n\texercises: ExercisesRecord\n\tfaqs: FaqsRecord\n\tfeatures: FeaturesRecord\n\thooks: HooksRecord\n\tlab_sessions: LabSessionsRecord\n\tlabs: LabsRecord\n\tnotifications: NotificationsRecord\n\tplans: PlansRecord\n\tusers: UsersRecord\n}\n\nexport type CollectionResponses = {\n\tcompanies: CompaniesResponse\n\texercise_session_logs: ExerciseSessionLogsResponse\n\texercise_sessions: ExerciseSessionsResponse\n\texercises: ExercisesResponse\n\tfaqs: FaqsResponse\n\tfeatures: FeaturesResponse\n\thooks: HooksResponse\n\tlab_sessions: LabSessionsResponse\n\tlabs: LabsResponse\n\tnotifications: NotificationsResponse\n\tplans: PlansResponse\n\tusers: UsersResponse\n}"
  },
  {
    "path": "kubelab-ui/src/lib/pocketbase/index.ts",
    "content": "import PocketBase from \"pocketbase\";\nimport { writable } from \"svelte/store\";\nimport toast from \"svelte-french-toast\";\nimport { goto } from \"$app/navigation\";\n\nexport const client = new PocketBase();\n\nexport const currentUser = writable(client.authStore.model);\n\nexport async function login(\n    email: string,\n    password: string,\n    register = false,\n    rest: { [key: string]: any } = {}\n) {\n    if (register) {\n        const user = { ...rest, email, password, confirmPassword: password };\n        await client.collection(\"users\").create(user);\n    }\n    await client.collection(\"users\").authWithPassword(email, password);\n}\n\nexport function logout() {\n    client.authStore.clear();\n    currentUser.set(null);\n    goto(\"/login/\");\n    toast.success(\"Successfully logged out.\");\n}\n"
  },
  {
    "path": "kubelab-ui/src/lib/pocketbase/ui.ts",
    "content": "import toast from \"svelte-french-toast\";\n\n// wrapper to execute a pocketbase client request and generate alerts on failure\nexport async function alertOnFailure(request: () => void) {\n    try {\n        await request();\n    } catch (e: any) {\n        const {\n            message,\n            data: { data = {} }\n        } = e;\n        if (message) toast.error(message);\n        for (const key in data) {\n            const { message } = data[key];\n            if (message) toast.error(`${key}: ${message}`);\n        }\n    }\n}\n"
  },
  {
    "path": "kubelab-ui/src/lib/stores/codeView.ts",
    "content": "import { writable } from \"svelte/store\";\n\nconst defaultValue = false;\nconst initialCodeViewValue =\n    localStorage.getItem(\"codeView\") === null\n        ? defaultValue\n        : localStorage.getItem(\"codeView\") === \"true\";\n\nconst codeView = writable<boolean>(initialCodeViewValue);\n\ncodeView.subscribe((value) => {\n    localStorage.setItem(\"codeView\", value.toString());\n});\n\nexport default codeView;\n"
  },
  {
    "path": "kubelab-ui/src/lib/stores/data.ts",
    "content": "import { client } from \"$lib/pocketbase\";\nimport type {\n    CompaniesResponse,\n    ExerciseSessionLogsResponse,\n    ExerciseSessionsResponse,\n    ExercisesResponse,\n    LabSessionsResponse,\n    LabsResponse\n} from \"$lib/pocketbase/generated-types\";\nimport { get, writable, type Writable } from \"svelte/store\";\n\nexport const loadingCodeEditor: Writable<boolean> = writable<boolean>(false);\nexport const avatarUrl: Writable<string> = writable<string>();\nexport const lab: Writable<LabsResponse> = writable<LabsResponse>();\nexport const labs: Writable<LabsResponse[]> = writable<LabsResponse[]>();\nexport const lab_session: Writable<LabSessionsResponse> = writable<LabSessionsResponse>();\nexport const lab_sessions: Writable<LabSessionsResponse[]> = writable<LabSessionsResponse[]>();\nexport const exercise: Writable<ExercisesResponse> = writable<ExercisesResponse>();\nexport const exercises: Writable<ExercisesResponse[]> = writable<ExercisesResponse[]>();\nexport const exercise_session: Writable<ExerciseSessionsResponse> =\n    writable<ExerciseSessionsResponse>();\nexport const exercise_sessions: Writable<ExerciseSessionsResponse[]> =\n    writable<ExerciseSessionsResponse[]>();\nexport const exercise_session_logs: Writable<ExerciseSessionLogsResponse[]> =\n    writable<ExerciseSessionLogsResponse[]>();\nexport const companies: Writable<CompaniesResponse[]> = writable<CompaniesResponse[]>();\n\nexport async function getLabSession(labId: string) {\n    return get(labs).find((lab) => lab.id === labId);\n}\n\nexport async function getLabSessionsByLab(labId: string) {\n    return get(lab_sessions).filter((lab_session) => lab_session.lab === labId);\n}\n\nexport function filterExerciseSessionsByLab(labId: string) {\n    const filtered_exercises = get(exercises).filter((exercise) => exercise.lab === labId);\n    // exercise_sessions.exercise is a reference to the exercise id, by filtered exercises\n    const filtered_exercise_sessions = get(exercise_sessions).filter((exercise_session) =>\n        filtered_exercises.find((exercise) => exercise.id == exercise_session.exercise)\n    );\n\n    exercise_sessions.set(filtered_exercise_sessions);\n}\n\nexport function filterExercisesByLab(labId: string) {\n    const filtered_exercises = get(exercises).filter((exercise) => exercise.lab === labId);\n    exercises.set(filtered_exercises);\n}\n\nexport function getExerciseSessionByExercise(exerciseId: string) {\n    return get(exercise_sessions).find(\n        (exercise_session) => exercise_session.exercise === exerciseId\n    );\n}\n\nexport function checkIfExerciseIsDone(exercise_id: string) {\n    const temp_exercise_session = getExerciseSessionByExercise(exercise_id);\n    if (temp_exercise_session) {\n        return temp_exercise_session.endTime;\n    }\n    return false;\n}\n\nexport async function setExerciseByExerciseSession(exerciseSessionId: string) {\n    if (exercises === undefined) {\n        return;\n    }\n    // set exercise by exercise_session\n    const filtered_exercise = get(exercises).find(\n        (exercise_session) => exercise_session.id === exerciseSessionId\n    );\n\n    if (filtered_exercise === undefined) {\n        return;\n    }\n\n    exercise.set(filtered_exercise as ExercisesResponse);\n}\n\nexport async function setExerciseSessionByExercise(exerciseId: string) {\n    if (exercise_sessions === undefined) {\n        return;\n    }\n    // set exercise_session by exercise\n    const filtered_exercise_session = get(exercise_sessions).find(\n        (exercise_session) => exercise_session.exercise === exerciseId\n    );\n\n    if (filtered_exercise_session === undefined) {\n        return;\n    }\n\n    exercise_session.set(filtered_exercise_session as ExerciseSessionsResponse);\n}\n\nexport enum UpdateFilterEnum {\n    ALL = \"all\",\n    ExercisesByLab = \"exercises_by_lab\"\n}\n\nexport interface UpdateFilter {\n    filter: UpdateFilterEnum;\n    labId?: string;\n}\n\nexport async function updateDataStores(filter: UpdateFilter = { filter: UpdateFilterEnum.ALL }) {\n    // get labs and set labs store\n    await client\n        .collection(\"labs\")\n        .getFullList({\n            sort: \"title\"\n        })\n        .then((response: unknown) => {\n            labs.set(response as LabsResponse[]);\n            return response;\n        })\n        .catch((error: unknown) => {\n            console.log(error);\n        });\n\n    // get lab_sessions and set lab_sessions store\n    await client\n        .collection(\"lab_sessions\")\n        .getFullList(200, {\n            expand: \"lab\"\n        })\n        .then((response: unknown) => {\n            lab_sessions.set(response as LabSessionsResponse[]);\n            return response;\n        })\n        .catch((error: unknown) => {\n            console.log(error);\n        });\n\n    // get exercises and set exercises store\n    await client\n        .collection(\"exercises\")\n        .getFullList()\n        .then((response: unknown) => {\n            if (filter.filter === UpdateFilterEnum.ExercisesByLab) {\n                const filtered_exercises = (response as ExercisesResponse[]).filter(\n                    (exercise) => exercise.lab === filter.labId\n                );\n                exercises.set(filtered_exercises);\n            }\n            if (filter.filter === UpdateFilterEnum.ALL) {\n                exercises.set(response as ExercisesResponse[]);\n            }\n            return response;\n        })\n        .catch((error: unknown) => {\n            console.log(error);\n        });\n\n    // get exercise_sessions and set exercise_sessions store\n    await client\n        .collection(\"exercise_sessions\")\n        .getFullList(200, {\n            expand: \"exercise,exercise.lab\"\n        })\n        .then((response: unknown) => {\n            exercise_sessions.set(response as ExerciseSessionsResponse[]);\n            return response;\n        })\n        .catch((error: unknown) => {\n            console.log(error);\n        });\n}\n\nexport async function setLabStartTime(labSessionId: string) {\n    const data = {\n        startTime: new Date().toISOString()\n    };\n    await client\n        .collection(\"lab_sessions\")\n        .update(labSessionId, data)\n        .then((response: unknown) => {\n            return response;\n        })\n        .catch((error: unknown) => {\n            console.log(error);\n        });\n\n    updateDataStores();\n}\n\nexport async function setLabEndTime(labSessionId: string) {\n    const data = {\n        endTime: new Date().toISOString()\n    };\n    await client\n        .collection(\"lab_sessions\")\n        .update(labSessionId, data)\n        .then((response: unknown) => {\n            return response;\n        })\n        .catch((error: unknown) => {\n            console.log(error);\n        });\n\n    updateDataStores();\n}\n\nexport async function setExerciseStartTime(exerciseSessionId: string) {\n    const data = {\n        startTime: new Date().toISOString()\n    };\n    await client\n        .collection(\"exercise_sessions\")\n        .update(exerciseSessionId, data)\n        .then((response: unknown) => {\n            return response;\n        })\n        .catch((error: unknown) => {\n            console.log(error);\n        });\n\n    updateDataStores();\n}\n\nexport async function setExerciseEndTime(exerciseSessionId: string) {\n    const data = {\n        endTime: new Date().toISOString()\n    };\n    await client\n        .collection(\"exercise_sessions\")\n        .update(exerciseSessionId, data)\n        .then((response: unknown) => {\n            return response;\n        })\n        .catch((error: unknown) => {\n            console.log(error);\n        });\n\n    updateDataStores();\n}\n"
  },
  {
    "path": "kubelab-ui/src/lib/stores/layout_store.ts",
    "content": "import { persisted } from \"svelte-local-storage-store\";\n\ninterface LayoutStore {\n    file_tree: number;\n    terminal: number;\n    show_config: boolean;\n    folders_first: boolean;\n}\n\nconst { subscribe, update, set } = persisted<LayoutStore>(\"layout_preferences\", {\n    file_tree: 30,\n    terminal: 30,\n    show_config: true,\n    folders_first: true\n});\n\nfunction toggle_state(key: keyof LayoutStore) {\n    update((state: any) => ({\n        ...state,\n        [key]: !state[key]\n    }));\n}\n\ntype LayoutKeysByType<T> = keyof {\n    [Key in keyof LayoutStore as LayoutStore[Key] extends T ? Key : never]: LayoutStore[Key];\n};\n\nfunction toggle_number(key: LayoutKeysByType<number>) {\n    update((state: any) => ({\n        ...state,\n        [key]: state[key] === 0 ? 30 : 0\n    }));\n}\n\nconst toggle_file_tree = () => toggle_number(\"file_tree\");\nconst toggle_terminal = () => toggle_number(\"terminal\");\nconst toggle_config = () => toggle_state(\"show_config\");\nconst toggle_sort = () => toggle_state(\"folders_first\");\n\nexport const layout_store = {\n    subscribe,\n    set, // used by <Pane bind:size> inside Desktop.svelte\n    toggle_file_tree,\n    toggle_terminal,\n    toggle_config,\n    toggle_sort\n};\n"
  },
  {
    "path": "kubelab-ui/src/lib/stores/loading.ts",
    "content": "import { writable, type Writable } from \"svelte/store\";\n\nexport const loadingLabs: Writable<Set<string>> = writable<Set<string>>(new Set());\nexport const loadingExercises: Writable<Set<string>> = writable<Set<string>>(new Set());\n"
  },
  {
    "path": "kubelab-ui/src/lib/stores/metadata.ts",
    "content": "import { writable } from \"svelte/store\";\n\nexport interface Metadata {\n    title?: string;\n    description?: string;\n}\n\nexport const metadata = writable<Metadata>({});\n"
  },
  {
    "path": "kubelab-ui/src/lib/stores/sidebar.ts",
    "content": "import type { ExerciseSessionsResponse, ExercisesResponse, LabSessionsResponse, LabsResponse } from \"$lib/pocketbase/generated-types\";\nimport { writable, type Writable } from \"svelte/store\";\n\nexport const sidebarOpen: Writable<boolean> = writable<boolean>(false);\nexport const sidebar_lab: Writable<LabsResponse> = writable<LabsResponse>();\nexport const sidebar_lab_session: Writable<LabSessionsResponse> = writable<LabSessionsResponse>();\nexport const sidebar_exercises: Writable<ExercisesResponse[]> = writable<ExercisesResponse[]>([]);\nexport const sidebar_exercise_sessions: Writable<ExerciseSessionsResponse[]> = writable<ExerciseSessionsResponse[]>([]);\n"
  },
  {
    "path": "kubelab-ui/src/lib/stores/tableView.ts",
    "content": "import { writable } from \"svelte/store\";\n\nconst defaultValue = false;\nconst initialHorizontalViewValue =\n    localStorage.getItem(\"horizontalView\") === null\n        ? defaultValue\n        : localStorage.getItem(\"horizontalView\") === \"true\";\n\nconst horizontalView = writable<boolean>(initialHorizontalViewValue);\n\nhorizontalView.subscribe((value) => {\n    localStorage.setItem(\"horizontalView\", value.toString());\n});\n\nexport default horizontalView;\n"
  },
  {
    "path": "kubelab-ui/src/lib/stores/terminal.ts",
    "content": "import { writable } from \"svelte/store\";\n\nexport const terminal_size = writable({ height: 65 });\n\n// get pathnames from url\nexport const pathname = writable(window.location.pathname);\n"
  },
  {
    "path": "kubelab-ui/src/lib/stores/theme.ts",
    "content": "import { writable } from \"svelte/store\";\n\nconst defaultValue = false;\nconst initialDarkThemeValue =\n    localStorage.getItem(\"darkTheme\") === null\n        ? defaultValue\n        : localStorage.getItem(\"darkTheme\") === \"true\";\n\nconst darkTheme = writable<boolean>(initialDarkThemeValue);\n\ndarkTheme.subscribe((value) => {\n    localStorage.setItem(\"darkTheme\", value.toString());\n});\n\nexport default darkTheme;\n"
  },
  {
    "path": "kubelab-ui/src/lib/terminal.ts",
    "content": ""
  },
  {
    "path": "kubelab-ui/src/lib/types.ts",
    "content": "export enum DeploymentStatus {\n    Running = \"Running\",\n    Pending = \"Pending\",\n    Failed = \"Failed\",\n    Unknown = \"Unknown\"\n}\n\nexport interface Deployment {\n    id: string;\n    name: string;\n    status: DeploymentStatus;\n    namespace: string;\n}\n\nexport interface NavRoute {\n    id: string;\n    name: string;\n    href: string;\n    icon: any;\n}\n"
  },
  {
    "path": "kubelab-ui/src/lib/utils/clickOutside.ts",
    "content": "export function clickOutside(node: any) {\n    const handleClick = (event: any) => {\n        if (node && !node.contains(event.target) && !event.defaultPrevented) {\n            node.dispatchEvent(new CustomEvent(\"click_outside\", node));\n        }\n    };\n\n    document.addEventListener(\"click\", handleClick, true);\n\n    return {\n        destroy() {\n            document.removeEventListener(\"click\", handleClick, true);\n        }\n    };\n}\n"
  },
  {
    "path": "kubelab-ui/src/lib/utils/enums.ts",
    "content": "export enum Color {\n    Red = \"red\",\n    Yellow = \"yellow\",\n    Green = \"green\",\n    Blue = \"blue\",\n    Indigo = \"indigo\",\n    Purple = \"purple\",\n    Pink = \"pink\",\n    Gray = \"gray\"\n}\n"
  },
  {
    "path": "kubelab-ui/src/lib/utils/interfaces.ts",
    "content": ""
  },
  {
    "path": "kubelab-ui/src/lib/utils/time.ts",
    "content": "export function getTimeAgo(isoString: string) {\n    const now = new Date(); // Current time\n    const pastTime = new Date(isoString); // Time from ISO string\n\n    const timeDiffMilliseconds = now.getTime() - pastTime.getTime();\n    const timeDiffSeconds = Math.floor(timeDiffMilliseconds / 1000);\n\n    // Calculate the time difference in seconds, minutes, hours, days, months, or years\n    if (timeDiffSeconds < 60) {\n        return timeDiffSeconds + \" seconds\";\n    } else if (timeDiffSeconds < 3600) {\n        const minutes = Math.floor(timeDiffSeconds / 60);\n        return minutes + (minutes === 1 ? \" minute\" : \" minutes\");\n    } else if (timeDiffSeconds < 86400) {\n        const hours = Math.floor(timeDiffSeconds / 3600);\n        return hours + (hours === 1 ? \" hour\" : \" hours\");\n    } else if (timeDiffSeconds < 2592000) {\n        const days = Math.floor(timeDiffSeconds / 86400);\n        return days + (days === 1 ? \" day\" : \" days\");\n    } else if (timeDiffSeconds < 31536000) {\n        const months = Math.floor(timeDiffSeconds / 2592000);\n        return months + (months === 1 ? \" month\" : \" months\");\n    } else {\n        const years = Math.floor(timeDiffSeconds / 31536000);\n        return years + (years === 1 ? \" year\" : \" years\");\n    }\n}\n\nexport function getDeltaTime(startIsoString: string, endIsoString: string) {\n    // calculate the time difference\n    const startTime = new Date(startIsoString);\n    const endTime = new Date(endIsoString);\n    const timeDiffMilliseconds = endTime.getTime() - startTime.getTime();\n    const timeDiffSeconds = Math.floor(timeDiffMilliseconds / 1000);\n\n    // Calculate the time difference in seconds, minutes, hours, days, months, or years\n    if (timeDiffSeconds < 60) {\n        return timeDiffSeconds + \" seconds\";\n    } else if (timeDiffSeconds < 3600) {\n        const minutes = Math.floor(timeDiffSeconds / 60);\n        return minutes + (minutes === 1 ? \" minute\" : \" minutes\");\n    } else if (timeDiffSeconds < 86400) {\n        const hours = Math.floor(timeDiffSeconds / 3600);\n        return hours + (hours === 1 ? \" hour\" : \" hours\");\n    } else if (timeDiffSeconds < 2592000) {\n        const days = Math.floor(timeDiffSeconds / 86400);\n        return days + (days === 1 ? \" day\" : \" days\");\n    } else if (timeDiffSeconds < 31536000) {\n        const months = Math.floor(timeDiffSeconds / 2592000);\n        return months + (months === 1 ? \" month\" : \" months\");\n    } else {\n        const years = Math.floor(timeDiffSeconds / 31536000);\n        return years + (years === 1 ? \" year\" : \" years\");\n    }\n}\n"
  },
  {
    "path": "kubelab-ui/src/lib/xterm.css",
    "content": "/**\n * Copyright (c) 2014 The xterm.js authors. All rights reserved.\n * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)\n * https://github.com/chjj/term.js\n * @license MIT\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n *\n * Originally forked from (with the author's permission):\n *   Fabrice Bellard's javascript vt100 for jslinux:\n *   http://bellard.org/jslinux/\n *   Copyright (c) 2011 Fabrice Bellard\n *   The original design remains. The terminal itself\n *   has been extended to include xterm CSI codes, among\n *   other features.\n */\n\n/**\n *  Default styles for xterm.js\n */\n\n.xterm {\n    position: relative;\n    user-select: none;\n    -ms-user-select: none;\n    -webkit-user-select: none;\n    padding: 15px;\n}\n\n.xterm.focus,\n.xterm:focus {\n    outline: none;\n}\n\n.xterm .xterm-helpers {\n    position: absolute;\n    top: 0;\n    /**\n     * The z-index of the helpers must be higher than the canvases in order for\n     * IMEs to appear on top.\n     */\n    z-index: 5;\n}\n\n.xterm .xterm-helper-textarea {\n    padding: 0;\n    border: 0;\n    margin: 0;\n    /* Move textarea out of the screen to the far left, so that the cursor is not visible */\n    position: absolute;\n    opacity: 0;\n    left: -9999em;\n    top: 0;\n    width: 0;\n    height: 0;\n    z-index: -5;\n    /** Prevent wrapping so the IME appears against the textarea at the correct position */\n    white-space: nowrap;\n    overflow: hidden;\n    resize: none;\n}\n\n.xterm .composition-view {\n    /* TODO: Composition position got messed up somewhere */\n    background: #000;\n    color: #fff;\n    display: none;\n    position: absolute;\n    white-space: nowrap;\n    z-index: 1;\n}\n\n.xterm .composition-view.active {\n    display: block;\n}\n\n.xterm .xterm-viewport {\n    /* On OS X this is required in order for the scroll bar to appear fully opaque */\n    background-color: #000;\n    overflow-y: scroll;\n    cursor: default;\n    position: absolute;\n    right: 0;\n    left: 0;\n    top: 0;\n    bottom: 0;\n}\n\n.xterm .xterm-screen {\n    position: relative;\n}\n\n.xterm .xterm-screen canvas {\n    position: absolute;\n    left: 0;\n    top: 0;\n}\n\n.xterm .xterm-scroll-area {\n    visibility: hidden;\n}\n\n.xterm-char-measure-element {\n    display: inline-block;\n    visibility: hidden;\n    position: absolute;\n    top: 0;\n    left: -9999em;\n    line-height: normal;\n}\n\n.xterm {\n    cursor: text;\n}\n\n.xterm.enable-mouse-events {\n    /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */\n    cursor: default;\n}\n\n.xterm.xterm-cursor-pointer,\n.xterm .xterm-cursor-pointer {\n    cursor: pointer;\n}\n\n.xterm.column-select.focus {\n    /* Column selection mode */\n    cursor: crosshair;\n}\n\n.xterm .xterm-accessibility,\n.xterm .xterm-message {\n    position: absolute;\n    left: 0;\n    top: 0;\n    bottom: 0;\n    right: 0;\n    z-index: 10;\n    color: transparent;\n}\n\n.xterm .live-region {\n    position: absolute;\n    left: -9999px;\n    width: 1px;\n    height: 1px;\n    overflow: hidden;\n}\n\n.xterm-dim {\n    opacity: 0.5;\n}\n\n.xterm-underline {\n    text-decoration: underline;\n}\n\n.xterm-strikethrough {\n    text-decoration: line-through;\n}\n\n.xterm-screen .xterm-decoration-container .xterm-decoration {\n    z-index: 6;\n    position: absolute;\n}\n"
  },
  {
    "path": "kubelab-ui/src/routes/+layout.svelte",
    "content": "<script lang=\"ts\">\n  import \"../app.postcss\";\n  import \"../app.css\";\n  import \"../styles/xterm.css\";\n  import \"../styles/prism.css\";\n  import { metadata } from \"$lib/stores/metadata\";\n  import { site } from \"$lib/config\";\n  import { beforeNavigate } from \"$app/navigation\";\n  import Nav from \"$lib/components/base/Nav.svelte\";\n  import { page } from \"$app/stores\";\n  import { Toaster } from \"svelte-french-toast\";\n  import darkTheme from \"$lib/stores/theme\";\n\n  // export let data: any;\n\n  $: title = $metadata.title ? $metadata.title + \" | \" + site.name : site.name;\n  $: description = $metadata.description ?? site.description;\n\n  // reset metadata on navigation so that the new page inherits nothing from the old page\n  beforeNavigate(() => {\n    $metadata = {};\n  });\n\n  // add  class=\"dark\" data-theme=\"dark\" to <html> if dark mode is enabled\n  $: if ($darkTheme) {\n    document.documentElement.classList.add(\"dark\");\n    document.documentElement.setAttribute(\"data-theme\", \"dark\");\n  } else {\n    document.documentElement.classList.remove(\"dark\");\n    document.documentElement.setAttribute(\"data-theme\", \"light\");\n  }\n</script>\n\n<svelte:head>\n  <title>{title}</title>\n  <meta name=\"description\" content={description} />\n</svelte:head>\n\n<div>\n  <Toaster position=\"bottom-center\" />\n  <!-- only display nav when not on /login -->\n  {#if $page.route.id !== \"/login\" && $page.route.id !== \"/\" && $page.route.id !== \"/signup\" && $page.route.id !== \"/admin/%5Bid%5D\"}\n    <!-- also not display nav when any subpath of /admin with regex /admin\\/.*$/ -->\n    {#if !$page.route.id.match(/\\/admin\\/.*$/)}\n      <Nav />\n    {/if}\n  {/if}\n  <slot />\n</div>\n"
  },
  {
    "path": "kubelab-ui/src/routes/+layout.ts",
    "content": "import { browser } from \"$app/environment\";\nimport { client } from \"$lib/pocketbase\";\nimport { redirect } from \"@sveltejs/kit\";\n\n// turn off SSR - we're JAMstack here\nexport const ssr = false;\n// Prerendering turned off. Turn it on if you know what you're doing.\nexport const prerender = false;\n// trailing slashes make relative paths much easier\nexport const trailingSlash = \"always\";\n\nexport const load = ({ url }) => {\n    const { pathname } = url;\n\n    if (browser) {\n        if (client.authStore.model) {\n            if (pathname === \"/login/\" || pathname === \"/signup/\") {\n                throw redirect(307, \"/app\");\n            }\n        }\n\n        if (client.authStore.isValid) {\n            if (pathname === \"/login/\" || pathname === \"/signup/\") {\n                throw redirect(307, \"/app\");\n            }\n        }\n    }\n\n    return {\n        pathname\n    };\n};\n"
  },
  {
    "path": "kubelab-ui/src/routes/+page.svelte",
    "content": "<script lang=\"ts\">\n  import Companies from \"$lib/components/landingpage/Companies.svelte\";\n  import Faq from \"$lib/components/landingpage/Faq.svelte\";\n  import Features from \"$lib/components/landingpage/Features.svelte\";\n  import Header from \"$lib/components/landingpage/Header.svelte\";\n  import Hero from \"$lib/components/landingpage/Hero.svelte\";\n  import type {\n    CompaniesResponse,\n    FaqsResponse\n  } from \"$lib/pocketbase/generated-types\";\n\n  export let data: any;\n\n  let faqs: FaqsResponse[] = data?.faqs ?? [];\n  let companies: CompaniesResponse[] = data?.companies ?? [];\n</script>\n\n<div>\n  <Header />\n  <Hero />\n  <Companies {companies} />\n  <Features />\n  <Faq {faqs} />\n  <!-- <Cta /> -->\n  <!-- <Footer /> -->\n</div>\n"
  },
  {
    "path": "kubelab-ui/src/routes/+page.ts",
    "content": "import { client } from \"$lib/pocketbase\";\nimport type {\n    CompaniesResponse,\n    FaqsResponse,\n    PlansResponse\n} from \"$lib/pocketbase/generated-types.js\";\n\nexport const load = async () => {\n    const faqs: FaqsResponse[] = await client.collection(\"faqs\").getFullList(200, {});\n\n    const companies: CompaniesResponse[] = await client\n        .collection(\"companies\")\n        .getFullList(200, {});\n\n    return {\n        faqs,\n        companies\n    };\n};\n"
  },
  {
    "path": "kubelab-ui/src/routes/admin/[id]/+page.svelte",
    "content": "<script lang=\"ts\">\n  import { client } from \"$lib/pocketbase\";\n  import {\n    NotificationsTypeOptions,\n    type ExerciseSessionLogsResponse,\n    type NotificationsResponse,\n    type ExerciseSessionsResponse,\n    type CompaniesResponse,\n    type ExercisesResponse\n  } from \"$lib/pocketbase/generated-types\";\n  import { exercise_session_logs } from \"$lib/stores/data\";\n  import {\n    Table,\n    TableBody,\n    TableBodyCell,\n    TableBodyRow,\n    TableHead,\n    TableHeadCell\n  } from \"flowbite-svelte\";\n  import { CheckCircle, HelpCircle, Play } from \"lucide-svelte\";\n  import { onDestroy, onMount } from \"svelte\";\n  import toast from \"svelte-french-toast\";\n\n  interface Activity {\n    exercise_title: string;\n    lab_title: string;\n    timestamp: string;\n    end_time: string;\n    user_name: string;\n    start_time: string;\n    avatarUrl: string;\n    type: \"start\" | \"end\";\n  }\n\n  let activities: Activity[] = [];\n  let notifications: NotificationsResponse[] = [];\n  export let data: any;\n  let company_id = data.props.id || \"\";\n  let all_exercise_sessions: ExerciseSessionsResponse[] = [];\n  let all_exercises: ExercisesResponse[] = [];\n\n  interface Company {\n    id: string;\n    name: string;\n    logo: string;\n  }\n\n  let company: Company = {\n    id: \"\",\n    name: \"\",\n    logo: \"\"\n  };\n\n  async function getCompany() {\n    let company_response: CompaniesResponse = await client\n      .collection(\"companies\")\n      .getOne(company_id, {\n        expand: \"avatar\"\n      });\n\n    company = {\n      id: company_response.id,\n      name: company_response.name,\n      logo:\n        \"/api/files/\" +\n        company_response?.collectionId +\n        \"/\" +\n        company_response?.id +\n        \"/\" +\n        company_response?.logo\n    };\n  }\n\n  async function getAllExerciseSessions() {\n    let exercise_sessions_response: ExerciseSessionsResponse[] = await client\n      .collection(\"exercise_sessions\")\n      .getFullList(100, {\n        expand: \"exercise,user\",\n        sort: \"-user\"\n      });\n\n    // filter exercise_sessions by company and only show the exercise_sessions where ther user exercise_sessions.expand.user.company.id == company_id\n    exercise_sessions_response = exercise_sessions_response.filter(\n      //@ts-ignore\n      (exercise_session) => exercise_session.expand.user.company == company_id\n    );\n    let exercises_response: ExercisesResponse[] = await client\n      .collection(\"exercises\")\n      .getFullList(100, {\n        expand: \"lab\",\n        sort: \"-lab\"\n      });\n\n    all_exercises = exercises_response;\n\n    // set all_exercise_sessions to the filtered exercise_sessions\n    all_exercise_sessions = exercise_sessions_response;\n    getRanking();\n  }\n\n  interface Ranking {\n    user_name: string;\n    avatarUrl: string;\n    average_time: number;\n    solved_exercises_percentage: number;\n  }\n\n  let all_ranking: Ranking[] = [];\n\n  function getRanking() {\n    // in exercise_sessions, we have all the exercise_sessions of the company. We need to group them by user\n\n    let users: any = {};\n\n\n    all_exercise_sessions.forEach((exercise_session: any) => {\n      if (users[exercise_session.expand.user.id]) {\n        users[exercise_session.expand.user.id].push(exercise_session);\n      } else {\n        users[exercise_session.expand.user.id] = [exercise_session];\n      }\n    });\n\n    // if users exercise_sessions have endTime = \"\", remove the exercise_session from the array\n\n    Object.keys(users).forEach((user_id) => {\n      let user_exercise_sessions = users[user_id];\n      user_exercise_sessions = user_exercise_sessions.filter(\n        (exercise_session: any) => exercise_session.endTime !== \"\"\n      );\n      users[user_id] = user_exercise_sessions;\n    });\n\n    // if the user has no exercise_sessions, remove it from the users object\n\n    Object.keys(users).forEach((user_id) => {\n      let user_exercise_sessions = users[user_id];\n      if (user_exercise_sessions.length === 0) {\n        delete users[user_id];\n      }\n    });\n\n    // now we have an object with all the exercise_sessions grouped by user\n    // we need to calculate the average time of each user, if there is no endTime, we don't count it\n\n    let ranking: Ranking[] = [];\n\n    Object.keys(users).forEach((user_id) => {\n      let user_exercise_sessions = users[user_id];\n      let total_time = 0;\n      let total_exercise_sessions = 0;\n\n      user_exercise_sessions.forEach((exercise_session: any) => {\n        if (exercise_session.endTime) {\n          total_time +=\n            new Date(exercise_session.endTime).getTime() -\n            new Date(exercise_session.startTime).getTime();\n          total_exercise_sessions++;\n        }\n      });\n\n      let average_time = total_time / total_exercise_sessions;\n\n      ranking.push({\n        user_name: user_exercise_sessions[0].expand.user.name,\n        avatarUrl:\n          \"/api/files/\" +\n          user_exercise_sessions[0].expand.user?.collectionId +\n          \"/\" +\n          user_exercise_sessions[0].expand.user?.id +\n          \"/\" +\n          user_exercise_sessions[0].expand.user.avatar,\n        average_time: average_time,\n        solved_exercises_percentage: Math.round(\n          (user_exercise_sessions.length / all_exercises.length) * 100\n        )\n      });\n    });\n\n    // first sort by the solved_exercises_percentage and then by the average_time\n    ranking.sort((a, b) => {\n      if (a.solved_exercises_percentage > b.solved_exercises_percentage) {\n        return -1;\n      } else if (a.solved_exercises_percentage < b.solved_exercises_percentage) {\n        return 1;\n      } else {\n        if (a.average_time < b.average_time) {\n          return -1;\n        } else if (a.average_time > b.average_time) {\n          return 1;\n        } else {\n          return 0;\n        }\n      }\n    });\n\n    // parse the average time to a human readable format -> minutes and seconds\n\n    ranking.forEach((user: any) => {\n      let minutes = Math.floor(user.average_time / 60000);\n      let seconds = Math.floor((user.average_time % 60000) / 1000);\n\n      user.average_time = minutes + \"m \" + seconds + \"s\";\n    });\n\n    ranking = ranking.slice(0, 3);\n\n    all_ranking = ranking;\n  }\n\n  function getRelativeTime(timestamp: string) {\n    let date = new Date(timestamp);\n    let now = new Date();\n    let difference = (now.getTime() - date.getTime()) / 1000;\n\n    if (difference < 60) {\n      return `${Math.floor(difference)}s`;\n    } else if (difference < 3600) {\n      return `${Math.floor(difference / 60)}m`;\n    } else if (difference < 86400) {\n      return `${Math.floor(difference / 3600)}h`;\n    } else {\n      return `${Math.floor(difference / 86400)}d`;\n    }\n  }\n\n  function getRelativeTimeDuration(start_time: string, end_time: string) {\n    let start_date = new Date(start_time);\n    let end_date = new Date(end_time);\n    let difference = (end_date.getTime() - start_date.getTime()) / 1000;\n\n    if (difference < 60) {\n      return `${Math.floor(difference)}s`;\n    } else if (difference < 3600) {\n      return `${Math.floor(difference / 60)}m`;\n    } else if (difference < 86400) {\n      return `${Math.floor(difference / 3600)}h`;\n    } else {\n      return `${Math.floor(difference / 86400)}d`;\n    }\n  }\n\n  async function getExerciseSessionLogs() {\n    let exercise_session_logs_response: ExerciseSessionLogsResponse[] = await client\n      .collection(\"exercise_session_logs\")\n      .getFullList(10, {\n        expand:\n          \"user,user.company,exercise_session,exercise_session.exercise,exercise_session.exercise.lab\",\n        sort: \"-timestamp\"\n      });\n\n    // filter exercise_session_logs by company and only show the exercise_session_logs where ther user exercise_session_logs.expand.user.company.id == company_id\n    exercise_session_logs_response = exercise_session_logs_response.filter(\n      //@ts-ignore\n      (log) => log.expand.user.company == company_id\n    );\n\n    // filter only the last 10 exercise_session_logs\n    exercise_session_logs_response = exercise_session_logs_response.slice(0, 10);\n\n    exercise_session_logs.set(exercise_session_logs_response as ExerciseSessionLogsResponse[]);\n    activities = parseLogsToActivities(exercise_session_logs_response);\n  }\n\n  async function getNotifications() {\n    let notifications_response: NotificationsResponse[] = await client\n      .collection(\"notifications\")\n      .getFullList(3, {\n        expand: \"user,user.company,exercise\",\n        sort: \"-created\"\n      });\n    // filter notifications by company and only show the notifications where ther user notifications_response.expand.user.company.id == company_id\n    // filter only the notifications last 3 notifications\n    notifications_response = notifications_response.filter(\n      //@ts-ignore\n      (notification) => notification.expand.user.company == company_id\n    );\n    notifications_response = notifications_response.slice(0, 10);\n\n    notifications = notifications_response;\n  }\n\n  onMount(async () => {\n    await getExerciseSessionLogs();\n    await getNotifications();\n    await getAllExerciseSessions();\n    await getCompany();\n\n    // set an interval to get the exercise_session_logs every 5 seconds\n    setInterval(async () => {\n      await getExerciseSessionLogs();\n      await getNotifications();\n      await getAllExerciseSessions();\n    }, 10000);\n\n    // watch for new notifications and post them to the notifications array\n    client.collection(\"notifications\").subscribe(\"*\", function (e) {\n      if (e.action === \"create\") {\n        toast(\"New notification!\", {\n          icon: \"👋\",\n          position: \"top-right\",\n          duration: 10000\n        });\n      }\n    });\n  });\n\n  onDestroy(() => {\n    // clear the interval when the component is destroyed\n    setInterval(() => {});\n  });\n\n  function parseLogToActivity(log: any) {\n    let activity: Activity = {\n      exercise_title: log.expand.exercise_session.expand.exercise.title,\n      lab_title: log.expand.exercise_session.expand.exercise.expand.lab.title,\n      timestamp: log.timestamp,\n      start_time: log.expand.exercise_session.startTime,\n      end_time: log.expand.exercise_session.endTime,\n      user_name: log.expand.user.name,\n      avatarUrl:\n        \"/api/files/\" +\n        log.expand.user?.collectionId +\n        \"/\" +\n        log.expand.user?.id +\n        \"/\" +\n        log.expand.user.avatar,\n      type: log.type\n    };\n\n    return activity;\n  }\n\n  async function setDone(notification: NotificationsResponse) {\n    await client.collection(\"notifications\").update(notification.id, {\n      done: true\n    });\n    await getNotifications();\n  }\n\n  function parseLogsToActivities(logs: any) {\n    let activities: Activity[] = [];\n\n    logs.forEach((log: any) => {\n      activities.push(parseLogToActivity(log));\n    });\n\n    return activities;\n  }\n</script>\n\n<div\n  class=\"absolute top-0 bottom-0 left-0 right-0 p-4 bg-no-repeat bg-cover bg-center justify-center\n  bg-gradient-to-r from-blue-500 to-purple-500 dark:from-base-100 dark:to-base-100\n  \"\n  style=\"\"\n>\n  <div>\n    <h1 class=\"text-center text-4xl font-bold mb-8 text-white\">\n      Workshop Dashboard\n      {company.name}\n    </h1>\n  </div>\n  <div\n    class=\"justify-center items-center bg-white p-5 rounded-lg absolute top-20 bottom-10 overflow-y-scroll scrollbar-thin left-36 right-36 grid grid-cols-2 gap-4\"\n  >\n    <img class=\"mx-auto mb-8 absolute top-4 left-4 w-36\" src={company.logo} alt={company.name} />\n    <div class=\"flow-root p-2 rounded-lg col-span-2\">\n      <!-- a centralized title with \"User Activities\" -->\n      <div class=\"text-center mb-5\">\n        <h3 class=\"text-lg leading-6 font-medium text-gray-900\">Ranking</h3>\n        <p class=\"mt-1 text-sm text-gray-500\">Average time to solution</p>\n      </div>\n\n      <Table class=\"bg-gray-200 rounded-lg shadow-lg\">\n        <TableHead class=\"text-left\">\n          <TableHeadCell>Rank</TableHeadCell>\n          <TableHeadCell>User</TableHeadCell>\n          <TableHeadCell>Average Time</TableHeadCell>\n          <TableHeadCell>Solved Exercises</TableHeadCell>\n        </TableHead>\n        <TableBody tableBodyClass=\"divide-y\">\n          {#each all_ranking as item, idx}\n            <TableBodyRow>\n              <TableBodyCell>{idx + 1}</TableBodyCell>\n              <TableBodyCell>\n                <img\n                  class=\"h-10 w-10 items-center justify-center inline-block rounded-full bg-gray-400 ring-8 ring-white\"\n                  src={item.avatarUrl}\n                  alt=\"\"\n                />\n                {\" \"}\n                {item.user_name}</TableBodyCell\n              >\n              <TableBodyCell>{item.average_time}</TableBodyCell>\n              <TableBodyCell>{item.solved_exercises_percentage}%</TableBodyCell>\n            </TableBodyRow>\n          {/each}\n        </TableBody>\n      </Table>\n    </div>\n\n    <div class=\"flow-root col-span-1 p-2 rounded-lg min-h-full\">\n      <!-- a centralized title with \"User Activities\" -->\n      <div class=\"text-center mb-5\">\n        <h3 class=\"text-lg leading-6 font-medium text-gray-900\">Notifications</h3>\n        <p class=\"mt-1 text-sm text-gray-500\">Last 10 notifications</p>\n      </div>\n      <ul class=\"-mb-8\">\n        {#if notifications.length > 0}\n          {#each notifications as notification, idx}\n            <li>\n              <div class=\"relative pb-8\">\n                <div class=\"relative flex space-x-3\">\n                  <div class=\"relative\">\n                    <HelpCircle class=\"h-8 w-8 text-gray-400justify-center\" />\n                    {#if notification.done == false}\n                      <span class=\"absolute -top-1.5 left-0\">\n                        <span\n                          class=\"animate-ping absolute inline-flex h-4 w-4 rounded-full top-1 -left-0.5 bg-red-400 opacity-75\"\n                        />\n                        <span class=\"relative inline-flex rounded-full h-3 w-3 bg-red-500\" />\n                      </span>\n                    {:else}\n                      <span class=\"absolute -top-1.5 left-0\">\n                        <span class=\"relative inline-flex rounded-full h-3 w-3 bg-green-500\" />\n                      </span>\n                    {/if}\n                  </div>\n                  <div class=\"flex min-w-0 flex-1 justify-between space-x-4 pt-1.5\">\n                    <div>\n                      <span class=\"text-sm text-gray-500\">\n                        {#if notification.type == NotificationsTypeOptions.help}\n                          <span class=\"font-medium text-gray-900\"\n                            >\n                            {notification.expand?.user.name}</span\n                          >{\" \"}\n                          requested help\n                        {/if}\n                        {#if notification.exercise}\n                          {\" \"} for the exercise{\" \"}\n                          <span class=\"font-medium text-gray-900\"\n                            >{notification.expand?.exercise.title}</span\n                          >\n                          {\" \"}\n                        {/if}\n                      </span>\n                    </div>\n\n                    <div class=\"whitespace-nowrap\">\n                      <span class=\"text-right text-sm text-gray-500\">\n                        <time datetime={notification.created}>\n                          {getRelativeTime(notification.created)} ago\n                        </time>\n                      </span>\n                      {#if notification.done == false}\n                        <button class=\"btn btn-sm\" on:click={() => setDone(notification)}>\n                          Mark as done\n                        </button>\n                      {:else}\n                        <button class=\"btn btn-sm btn-success\">Done</button>\n                      {/if}\n                    </div>\n                  </div>\n                </div>\n              </div>\n            </li>\n          {/each}\n        {/if}\n      </ul>\n    </div>\n\n    <div class=\"flow-root p-2 rounded-lg col-span-1\">\n      <!-- a centralized title with \"User Activities\" -->\n      <div class=\"text-center mb-5\">\n        <h3 class=\"text-lg leading-6 font-medium text-gray-900\">User Activities</h3>\n        <p class=\"mt-1 text-sm text-gray-500\">Last 10 activities</p>\n      </div>\n\n      <ul class=\"-mb-8\">\n        {#if activities.length > 0}\n          {#each activities as activity, idx}\n            <li>\n              <div class=\"relative pb-8\">\n                {#if idx !== activities.length - 1}\n                  <span\n                    class=\"absolute left-5 top-5 -ml-px h-full w-0.5 bg-gray-200\"\n                    aria-hidden=\"true\"\n                  />\n                {/if}\n                <div class=\"relative flex items-start space-x-3\">\n                  <div class=\"relative\">\n                    <img\n                      class=\"flex h-10 w-10 items-center justify-center rounded-full bg-gray-400 ring-8 ring-white\"\n                      src={activity.avatarUrl}\n                      alt=\"\"\n                    />\n\n                    <span class=\"absolute -bottom-0.5 -right-1 rounded-tl bg-white px-0.5 py-px\">\n                      {#if activity.type === \"start\"}\n                        <Play class=\"h-5 w-5 text-gray-400\" strokeWidth={2} />\n                      {:else}\n                        <CheckCircle class=\"h-5 w-5 text-green-400\" />\n                      {/if}\n                    </span>\n                  </div>\n                  <div class=\"min-w-0 flex-1\">\n                    <div>\n                      <div class=\"text-sm\">\n                        <span class=\"font-bold\">{activity.user_name}</span>\n                      </div>\n                      <p class=\"mt-0.5 text-sm text-gray-500\">\n                        <time datetime={activity.timestamp}>\n                          {getRelativeTime(activity.timestamp)} ago\n                        </time>\n                      </p>\n                    </div>\n                    <div class=\"mt-2 text-sm \">\n                      <p>\n                        {activity.type === \"start\" ? \"Started\" : \"Finished\"}\n                        {#if activity.type === \"end\"}\n                          {\" \"}\n                          after{\" \"}\n                          {getRelativeTimeDuration(activity.start_time, activity.end_time)}\n                        {/if}\n                        {\" \"}\n                        <strong>{activity.exercise_title}</strong>\n                        in{\" \"}\n                        <strong>{activity.lab_title}</strong>\n                        <br />\n                      </p>\n                    </div>\n                  </div>\n                </div>\n              </div>\n            </li>\n          {/each}\n        {/if}\n      </ul>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "kubelab-ui/src/routes/admin/[id]/+page.ts",
    "content": "import type { PageLoad } from \"./$types\";\n\nexport const load: PageLoad = async ({ params }: any) => {\n    const { id } = params;\n\n    return {\n        props: {\n            id\n        }\n    };\n};\n"
  },
  {
    "path": "kubelab-ui/src/routes/app/+layout.svelte",
    "content": "<div\n  class=\"absolute top-16 bottom-0 left-0 right-0 p-4 bg-no-repeat bg-cover bg-center justify-center\nbg-gradient-to-r from-blue-500 to-purple-500 dark:from-base-100 dark:to-base-100\"\n>\n  <slot />\n</div>\n"
  },
  {
    "path": "kubelab-ui/src/routes/app/+page.svelte",
    "content": "<script lang=\"ts\">\n  import RunningExercises from \"$lib/components/dashboard/RunningExercises.svelte\";\n  import { client } from \"$lib/pocketbase\";\n  import { exercise_sessions, exercises, labs } from \"$lib/stores/data.js\";\n  import { TerminalSquare } from \"lucide-svelte\";\n  import { onMount } from \"svelte\";\n  import {\n    Table,\n    TableBody,\n    TableBodyCell,\n    TableBodyRow,\n    TableHead,\n    TableHeadCell\n  } from \"flowbite-svelte\";\n  import type { CompaniesResponse } from \"$lib/pocketbase/generated-types\";\n\n  let companies: CompaniesResponse[] = [];\n\n  async function getCompanies() {\n    let response: CompaniesResponse[] = await client.collection(\"companies\").getFullList();\n\n    companies = response;\n  }\n\n  onMount(async () => {\n    if (client.authStore.model?.role == \"admin\") {\n      await getCompanies();\n    }\n  });\n\n  function getDoneExercisesNumber() {\n    let done_exercises = $exercise_sessions.filter((exercise_session) => exercise_session.endTime);\n    return done_exercises.length;\n  }\n\n  function getAverageTimeToResolve() {\n    let done_exercises = $exercise_sessions.filter((exercise_session) => exercise_session.endTime);\n    let total_time = 0;\n    done_exercises.forEach((exercise_session) => {\n      // convert starTime and endTime to Date objects\n      let startTime = new Date(exercise_session.startTime);\n      let endTime = new Date(exercise_session.endTime);\n      // get the difference in seconds\n      let difference = (endTime.getTime() - startTime.getTime()) / 1000;\n      total_time += difference;\n    });\n\n    let average_time = total_time / done_exercises.length;\n\n    if (isNaN(average_time)) {\n      return \"N/A\";\n    }\n\n    // return a string with the average time in minutes and seconds\n    return `${Math.floor(average_time / 60)}m ${Math.floor(average_time % 60)}s`;\n  }\n</script>\n\n{#if client.authStore.model?.role != \"admin\"}\n  <h1 class=\"text-center text-4xl text-white font-bold mb-8\">Dashboard</h1>\n  <div class=\"grid grid-cols-3 gap-4 justify-center items-center\">\n    <div />\n    <div\n      class=\"stats w-full col-span-3 sm:col-span-1 shadow  hover:shadow-md transition-all duration-150 ease-in-out\"\n    >\n      <div class=\"stat\">\n        <div class=\"stat-title\">Your <strong>average</strong> time to resolution</div>\n        <div class=\"stat-value\">{getAverageTimeToResolve()}</div>\n      </div>\n    </div>\n\n    <div\n      class=\"stats col-span-3 mt-4 shadow w-full hover:shadow-md transition-all duration-150 ease-in-out\"\n    >\n      <a href=\"/labs\">\n        <div class=\"stat\">\n          <div class=\"stat-figure text-blue-500\">\n            <TerminalSquare class=\"w-8 h-8\" />\n          </div>\n          <div class=\"stat-title\">Total Labs</div>\n          <div class=\"stat-value text-blue-500\">{$labs.length}</div>\n          <div class=\"stat-desc\">With <strong>{$exercises.length}</strong> exercises</div>\n        </div>\n      </a>\n\n      <div class=\"stat\">\n        <div class=\"stat-figure text-accent\">\n          <svg\n            xmlns=\"http://www.w3.org/2000/svg\"\n            fill=\"none\"\n            viewBox=\"0 0 24 24\"\n            class=\"inline-block w-8 h-8 stroke-current\"\n            ><path\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n              stroke-width=\"2\"\n              d=\"M13 10V3L4 14h7v7l9-11h-7z\"\n            /></svg\n          >\n        </div>\n        <div class=\"stat-title\">Total Exercises</div>\n        <div class=\"stat-value text-accent\">{$exercises.length}</div>\n        <div class=\"stat-desc\">Within <strong>{$labs.length}</strong> labs</div>\n      </div>\n\n      <div class=\"stat\">\n        <div class=\"stat-figure text-secondary\">\n          <progress\n            class=\"progress w-56\"\n            value={$exercises.length !== 0\n              ? Math.round((getDoneExercisesNumber() / $exercises.length) * 100)\n              : 0}\n            max=\"100\"\n          />\n        </div>\n        <div class=\"stat-value\">\n          {Math.round((getDoneExercisesNumber() / $exercises.length) * 100)}%\n        </div>\n        <div class=\"stat-title\">Exercises done</div>\n        <div class=\"stat-desc text-blue-500\">\n          {$exercises?.length - getDoneExercisesNumber()} exercise(s) remaining\n        </div>\n      </div>\n    </div>\n    <div class=\"col-span-3\">\n      <RunningExercises />\n    </div>\n  </div>\n{:else}\n  <h1 class=\"text-center text-4xl text-white font-bold mb-8\">Admin Dashboard</h1>\n  <Table class=\"bg-gray-200 rounded-lg shadow-lg\">\n    <TableHead class=\"text-left\">\n      <TableHeadCell>Company</TableHeadCell>\n      <TableHeadCell>Dashboard URL</TableHeadCell>\n    </TableHead>\n    <TableBody tableBodyClass=\"divide-y\">\n      {#if companies.length == 0}\n        <TableBodyRow>\n          <TableBodyCell colspan=\"2\" class=\"text-center\">No companies yet</TableBodyCell>\n        </TableBodyRow>\n      {:else}\n        {#each companies as company}\n          <TableBodyRow>\n            <TableBodyCell>\n              <img\n                class=\"h-10 w-10 items-center justify-center inline-block rounded-full bg-gray-400 ring-8 ring-white\"\n                src={\"/api/files/\" +\n                  company?.collectionId +\n                  \"/\" +\n                  company?.id +\n                  \"/\" +\n                  company?.logo}\n                alt=\"\"\n              />\n              {\" \"}\n              {company.name}</TableBodyCell\n            >\n            <TableBodyCell>\n              <a href={`https://kubelab.ch/admin/${company.id}`}>kubelab.ch/admin/{company.id}</a>\n            </TableBodyCell>\n          </TableBodyRow>\n        {/each}\n      {/if}\n    </TableBody>\n  </Table>\n{/if}\n"
  },
  {
    "path": "kubelab-ui/src/routes/app/+page.ts",
    "content": "import { updateDataStores } from \"$lib/stores/data\";\nimport { browser } from \"$app/environment\";\nimport { client } from \"$lib/pocketbase\";\nimport type { PageLoad } from \"./$types\";\nimport { goto } from \"$app/navigation\";\n\nexport const load: PageLoad = async () => {\n\n    if (browser) {\n\n        if (client.authStore.model) {\n            await updateDataStores().catch((error) => {\n                console.error(error);\n            });\n        } else {\n            goto(\"/login/\");\n        }\n    }\n};\n"
  },
  {
    "path": "kubelab-ui/src/routes/app/profile/+page.svelte",
    "content": "<script lang=\"ts\">\n    import { client } from \"$lib/pocketbase\";\n    import { avatarUrl } from \"$lib/stores/data\";\n  import { Fileupload, Button, Label } from \"flowbite-svelte\";\n    import toast from \"svelte-french-toast\";\n\n  let fileuploadprops = {\n    id: \"user_avatar\",\n    name: \"user_avatar\",\n    accept: \"image/*\",\n    multiple: false,\n    placeholder: \"Choose file\",\n    class: \"w-64 dark:text-white border-none\",\n    style: \"width: 100%;\"\n  };\n\n  async function updateProfile() {\n    let fileupload: HTMLInputElement | null = document.getElementById(\"user_avatar\") as HTMLInputElement;\n\n    if (!fileupload.files?.length) {\n      toast.error(\"No file selected\");\n      return;\n    }\n\n    if (fileupload && fileupload.files && fileupload.files.length > 0) {\n      let file: File = fileupload.files[0];\n\n      // Create FormData object and append the selected file to it\n      const formData = new FormData();\n      formData.append('avatar', file);\n\n      // Perform the Pocketbase API call\n      try {\n        if (!client.authStore.model) {\n          throw new Error(\"No user found\");\n        }\n        await client.collection('users').update(client.authStore.model?.id, formData);\n\n        // update authStore with the new avatar\n        await client.collection(\"users\").authRefresh();\n\n        toast.success(\"Successfully updated profile\")\n\n        // reset the fileupload input\n        fileupload.value = \"\";\n\n        // update the avatarUrl\n        $avatarUrl = \"/api/files/\" + client.authStore.model?.collectionId + \"/\" + client.authStore.model?.id + \"/\" + client.authStore.model?.avatar;\n\n      } catch (error: any) {\n        toast.error(error.message);\n      }\n    }\n  }\n</script>\n\n<h1 class=\"text-center text-4xl text-white font-bold mb-8\">Profile</h1>\n<div\n  class=\"bg-white dark:bg-base-100 shadow-md rounded-md flex flex-row justify-between items-center h-16 px-4\"\n>\n  <div class=\"flex flex-row items-center\">\n    <img\n      src={$avatarUrl}\n      alt=\"avatar\"\n      class=\"rounded-full w-12 h-12 mr-4\"\n    />\n    <div class=\"flex flex-col\">\n      <span class=\"font-bold text-lg\">\n        {client.authStore.model?.name}\n      </span>\n      <span class=\" text-sm\">\n        {client.authStore.model?.email}\n      </span>\n    </div>\n  </div>\n  <div class=\"flex flex-row items-center\">\n    <Label for=\"user_avatar\" class=\"mr-4\">Max 5MB</Label>\n    <Fileupload {...fileuploadprops} />\n    <Button class=\"btn btn-neutral\" on:click={updateProfile}>Upload</Button>\n  </div>\n</div>\n"
  },
  {
    "path": "kubelab-ui/src/routes/labs/+layout.svelte",
    "content": "<div\n  class=\"absolute top-16 bottom-0 right-0 left-0 overflow-hidden bg-gradient-to-r from-blue-500 to-purple-500 dark:from-base-100 dark:to-base-100\"\n>\n  <slot />\n</div>\n"
  },
  {
    "path": "kubelab-ui/src/routes/labs/+page.svelte",
    "content": "<script lang=\"ts\">\n  import SideOver from \"$lib/components/base/SideOver.svelte\";\n  import Lab from \"$lib/components/labs/Lab.svelte\";\n  import type {\n    ExerciseSessionsResponse,\n    ExercisesResponse,\n    LabSessionsResponse\n  } from \"$lib/pocketbase/generated-types\";\n  import { exercise_sessions, exercises, lab_sessions, labs } from \"$lib/stores/data\";\n  import { metadata } from \"$lib/stores/metadata\";\n\n  import { Drawer } from \"flowbite-svelte\";\n  import { sineIn } from \"svelte/easing\";\n\n  $metadata.title = \"Labs\";\n\n  function getLabSessions(lab_id: string): LabSessionsResponse {\n    let lab_session = $lab_sessions.find((lab_session) => lab_session.lab === lab_id);\n    // only return the lab session if it exists\n    // @ts-ignore\n    return lab_session;\n  }\n\n  function getExercises(lab_id: string): ExercisesResponse[] {\n    let lab_exercises = $exercises.filter((exercise) => exercise.lab === lab_id);\n    // only return the lab session if it exists\n    // @ts-ignore\n    return lab_exercises;\n  }\n\n  function getExercisesSessions(lab_id: string): ExerciseSessionsResponse[] {\n    let lab_exercises_sessions: ExerciseSessionsResponse[] = [];\n    let lab_exercises = getExercises(lab_id);\n    lab_exercises.forEach((exercise) => {\n      let exercise_session = $exercise_sessions.find(\n        (exercise_session) => exercise_session.exercise === exercise.id\n      );\n      if (exercise_session) {\n        lab_exercises_sessions.push(exercise_session);\n      }\n    });\n    return lab_exercises_sessions;\n  }\n\n  let drawerHidden = true;\n  let transitionParamsRight = {\n    x: 320,\n    duration: 200,\n    easing: sineIn\n  };\n</script>\n\n<Drawer\n  backdrop={true}\n  placement=\"right\"\n  transitionType=\"fly\"\n  transitionParams={transitionParamsRight}\n  bind:hidden={drawerHidden}\n  width=\"w-full sm:w-2/5 \"\n  class=\"absolute h-full overflow-y-scroll scrollbar-none shadow-lg p-0\n    bg-base-100\n    dark:bg-base-900\n    dark:text-white\n    text-base-900\n    z-50\n  \"\n>\n  <SideOver bind:drawerHidden />\n</Drawer>\n\n<div\n  class=\" top-0 bottom-0 right-0 left-0 bg-black z-40 {drawerHidden\n    ? ' bg-opacity-0'\n    : 'absolute bg-opacity-40 '} transition-all duration-200 ease-in-out  \"\n/>\n{#if $lab_sessions.length > 0}\n  <h1 class=\"text-center text-4xl font-bold my-4 text-white\">Labs</h1>\n  {#key $lab_sessions}\n    <p class=\"text-center text-xl my-4 text-white\">\n      <!-- display number of running labs x/1 -->\n      Running labs:\n      <span class=\"font-bold\"\n        >{$lab_sessions.filter((lab_session) => lab_session.clusterRunning).length}</span\n      > / 2\n    </p>\n  {/key}\n  <!-- <SideOver /> -->\n  <div class=\"grid grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-4 p-2 overflow-y-scroll scrollbar-none absolute w-full top-36 bottom-0\">\n    {#each $labs as this_lab}\n      <Lab\n        {this_lab}\n        this_lab_session={getLabSessions(this_lab.id)}\n        this_exercises={getExercises(this_lab.id)}\n        this_exercise_sessions={getExercisesSessions(this_lab.id)}\n        bind:drawerHidden\n      />\n    {/each}\n  </div>\n{/if}\n"
  },
  {
    "path": "kubelab-ui/src/routes/labs/+page.ts",
    "content": "import { updateDataStores } from \"$lib/stores/data\";\nimport toast from \"svelte-french-toast\";\nimport type { PageLoad } from \"./$types\";\n\nexport const load: PageLoad = async () => {\n    await updateDataStores().catch((error) => {\n        toast.error(error);\n    });\n};\n"
  },
  {
    "path": "kubelab-ui/src/routes/labs/[id]/+page.svelte",
    "content": "<script lang=\"ts\">\n  import { metadata } from \"$lib/stores/metadata\";\n  import { ArrowLeft, Play, TerminalSquare } from \"lucide-svelte\";\n  import {\n    exercise_sessions,\n    exercises,\n    getExerciseSessionByExercise,\n    lab\n  } from \"$lib/stores/data\";\n  import { ExerciseSessionLogsTypeOptions, type ExerciseSessionLogsRecord, type ExerciseSessionsRecord } from \"$lib/pocketbase/generated-types\";\n  import { client } from \"$lib/pocketbase\";\n  import toast from \"svelte-french-toast\";\n  import { loadingExercises } from \"$lib/stores/loading\";\n  import { onDestroy, onMount } from \"svelte\";\n\n  $metadata.title = \"Exercises\";\n\n  function isExerciseRunning(exercise_id: string) {\n    return getExerciseSessionByExercise(exercise_id)?.agentRunning;\n  }\n\n  let show = false;\n\n  onMount(() => {\n    show = true;\n  });\n\n  onDestroy(() => {\n    show = false;\n  });\n\n  async function startExercise(exercise_id: string) {\n    const data: ExerciseSessionsRecord = {\n      // @ts-ignore\n      user: client.authStore.model?.id,\n      exercise: exercise_id,\n      startTime: new Date().toISOString(),\n      endTime: \"\",\n      agentRunning: true\n    };\n\n    loadingExercises.update((exercises) => {\n      exercises.add(exercise_id);\n      return new Set(exercises); // Required for Svelte's reactivity\n    });\n\n    // const labId = window.location.pathname.split(\"/\")[2];\n\n    const exercise_session_id = getExerciseSessionByExercise(exercise_id)?.id;\n    if (exercise_session_id) {\n      await client\n        .collection(\"exercise_sessions\")\n        .update(exercise_session_id, data)\n        // @ts-ignore\n        .then((response: any) => {\n          // goto(`/labs/${labId}/${exercise_id}`);\n          toast.success(\"Exercise started\");\n\n          exercise_sessions.update((exercise_sessions) => {\n            return exercise_sessions.map((exercise_session) => {\n              if (exercise_session.id === exercise_session_id) {\n                return response;\n              }\n              return exercise_session;\n            });\n          });\n\n          // make an entry in the exercise_session_logs collection\n\n          const exercise_session_log_data: ExerciseSessionLogsRecord = {\n            // @ts-ignore\n            user: client.authStore.model?.id,\n            exercise_session: exercise_session_id,\n            type: ExerciseSessionLogsTypeOptions.start,\n            timestamp: new Date().toISOString()\n          };\n\n          client\n            .collection(\"exercise_session_logs\")\n            .create(exercise_session_log_data)\n            .then((response) => {\n            })\n            .catch((error) => {\n              console.log(error);\n            });\n\n        })\n        .catch((error) => {\n          toast.error(error.message);\n        })\n        .finally(() => {\n          loadingExercises.update((exercises) => {\n            exercises.delete(exercise_id);\n            return new Set(exercises); // Required for Svelte's reactivity\n          });\n        });\n    }\n  }\n</script>\n\n<a class=\"btn btn-neutral  top-5 absolute\" href=\"/labs/\" on:click={() => show = false}>\n  <ArrowLeft class=\"inline-block w-4 h-4 mr-2\" />\n  Labs\n</a>\n<h1 class=\"text-center text-4xl font-bold my-4\">Exercises</h1>\n<div class=\"grid grid-cols-3 gap-4\">\n  {#if show}\n    {#key $exercise_sessions}\n      {#each $exercises as exercise, i}\n        <div\n          class=\"card w-full {getExerciseSessionByExercise(exercise.id)?.endTime\n            ? 'bg-green-200'\n            : 'bg-base-200'} border-4 border-neutral\"\n        >\n          <div class=\"card-body\">\n            <p class=\"badge badge-outline  absolute top-2 right-2\">#{i + 1}</p>\n            <p\n              class=\"badge badge-outline {getExerciseSessionByExercise(exercise.id)?.agentRunning\n                ? 'badge-success'\n                : 'badge-error'} absolute top-2 left-2\"\n            >\n              {getExerciseSessionByExercise(exercise.id)?.agentRunning ? \"Running\" : \"Stopped\"}\n            </p>\n            <p\n              class=\"badge badge-outline {getExerciseSessionByExercise(exercise.id)?.endTime\n                ? ''\n                : 'badge-error'} absolute bottom-2 left-2\"\n            >\n              {getExerciseSessionByExercise(exercise.id)?.endTime ? \"Completed\" : \"Not Completed\"}\n            </p>\n            <h2 class=\"card-title mt-2\">{exercise.title}</h2>\n            <div class=\"flex gap-2 justify-end\">\n              <div class=\"tooltip\" data-tip=\"start exercise\">\n                <button\n                  class=\"btn {isExerciseRunning(exercise.id) ? 'btn-disabled' : 'btn-neutral'}\"\n                  on:click={() => startExercise(exercise.id)}\n                >\n                  {#if $loadingExercises.has(exercise.id)}\n                    <span class=\"loading loading-dots loading-md\" />\n                  {:else}\n                    <Play />\n                  {/if}\n                </button>\n              </div>\n              <a href={exercise.id} class=\"card-actions justify-end cursor-pointer\">\n                <div class=\"tooltip\" data-tip=\"open console\">\n                  <button class=\"btn btn-neutral\"><TerminalSquare /></button>\n                </div>\n              </a>\n            </div>\n          </div>\n        </div>\n      {/each}\n    {/key}\n  {/if}\n</div>\n"
  },
  {
    "path": "kubelab-ui/src/routes/labs/[id]/+page.ts",
    "content": "import {\n    UpdateFilterEnum,\n    filterExerciseSessionsByLab,\n    filterExercisesByLab,\n    updateDataStores\n} from \"$lib/stores/data\";\nimport toast from \"svelte-french-toast\";\nimport type { PageLoad } from \"../$types\";\n\nexport const load: PageLoad = async ({ params }: any) => {\n    const { id } = params;\n\n    await updateDataStores({\n        filter: UpdateFilterEnum.ExercisesByLab,\n        labId: id\n    }).catch((error) => {\n        toast.error(error);\n    });\n};\n"
  },
  {
    "path": "kubelab-ui/src/routes/labs/[id]/[id]/+layout.svelte",
    "content": "<script lang=\"ts\">\n  import { metadata } from \"$lib/stores/metadata\";\n  import { goto } from \"$app/navigation\";\n  import {\n    ArrowLeft,\n    CheckCircle,\n    FileCode2,\n    HelpCircle,\n    LifeBuoy,\n    RotateCw,\n    StopCircle,\n    StretchHorizontal,\n    StretchVertical,\n    Terminal\n  } from \"lucide-svelte\";\n  import { client } from \"$lib/pocketbase/index.js\";\n  import toast from \"svelte-french-toast\";\n  import {\n    checkIfExerciseIsDone,\n    exercise,\n    exercise_session,\n    exercise_sessions,\n    exercises,\n    filterExercisesByLab,\n    loadingCodeEditor\n  } from \"$lib/stores/data.js\";\n\n  // @ts-ignore\n  import { Confetti } from \"svelte-confetti\";\n  import ToggleConfetti from \"$lib/components/base/ToggleConfetti.svelte\";\n  import horizontalView from \"$lib/stores/tableView.js\";\n  import {\n    sidebarOpen,\n    sidebar_exercise_sessions,\n    sidebar_exercises,\n    sidebar_lab\n  } from \"$lib/stores/sidebar.js\";\n  import {\n    ExerciseSessionLogsTypeOptions,\n    type ExerciseSessionLogsRecord,\n    type NotificationsRecord,\n    NotificationsTypeOptions\n  } from \"$lib/pocketbase/generated-types.js\";\n  import { Tooltip } from \"flowbite-svelte\";\n  import codeView from \"$lib/stores/codeView.js\";\n\n  $metadata.title = \"Exercises\";\n\n  export let data;\n  let restartLoading = false;\n  let helpRequested = false;\n\n  function handleSwitchExercise(exercise_session_id: string) {\n    let lab_session_id = data.pathname.split(\"/\")[2];\n    goto(\"/labs/\" + lab_session_id + \"/\" + exercise_session_id);\n  }\n\n  function filterDuplicateLines(text: any) {\n    const lines = text.split(\"\\n\"); // Split the text into lines\n    const uniqueLines = [...new Set(lines)]; // Filter out duplicate lines\n    return uniqueLines.join(\"\\n\").trim(); // Join the unique lines back into a single string\n  }\n\n  async function handleCheckExercise() {\n    // fetch get https://agentUrl/check\n    // if 200, then exercise is completed\n    // if 500, then exercise is not completed\n    let lab_session_id = data.pathname.split(\"/\")[2];\n    let exercise_id = window.location.pathname.split(\"/\")[3];\n    let agentHost = window.location.host === \"localhost:5173\" ? \"kubelab.ch\" : window.location.host;\n\n    let agentUrl =\n      agentHost +\n      \"/kubelab-\" +\n      lab_session_id +\n      \"-\" +\n      exercise_id +\n      \"-\" +\n      client.authStore.model?.id;\n\n    await fetch(\"https://\" + agentUrl + \"/check\")\n      .then((response) =>\n        response.text().then((text) => {\n          if (response.status === 200) {\n            client\n              .collection(\"exercise_sessions\")\n              .update($exercise_session.id, {\n                endTime: new Date().toISOString(),\n                agentRunning: false\n              })\n              .then((record: any) => {\n                let duration =\n                  new Date(record.endTime).getTime() - new Date(record.startTime).getTime();\n                // show in m and s\n                let diffString = Math.floor(duration / 1000 / 60) + \"m \";\n                diffString += Math.floor((duration / 1000) % 60) + \"s\";\n                toast.success(\"Exercise completed in \" + diffString, {\n                  style: \"border: 1px solid #008000; padding: 16px; color: #008000;\", // green-themed style\n                  iconTheme: {\n                    primary: \"#008000\",\n                    secondary: \"#F0FFF0\" // light green background\n                  }\n                });\n\n                exercise_session.set(record);\n\n                let labId = window.location.pathname.split(\"/\")[2];\n\n                exercise_sessions.update((exercise_sessions) => {\n                  return exercise_sessions.map((exercise_session) => {\n                    if (exercise_session.id === record.id) {\n                      return record;\n                    }\n                    return exercise_session;\n                  });\n                });\n\n                sidebar_exercise_sessions.update((exercise_sessions) => {\n                  return exercise_sessions.map((exercise_session) => {\n                    if (exercise_session.id === record.id) {\n                      return record;\n                    }\n                    return exercise_session;\n                  });\n                });\n\n                filterExercisesByLab(labId);\n\n                // make an entry in the exercise_session_logs collection\n\n                const exercise_session_log_data: ExerciseSessionLogsRecord = {\n                  // @ts-ignore\n                  user: client.authStore.model?.id,\n                  exercise_session: $exercise_session.id,\n                  type: ExerciseSessionLogsTypeOptions.end,\n                  timestamp: new Date().toISOString()\n                };\n\n                client\n                  .collection(\"exercise_session_logs\")\n                  .create(exercise_session_log_data)\n                  .then((response) => {\n                    console.log(response);\n                  })\n                  .catch((error) => {\n                    console.log(error);\n                  });\n              });\n          } else {\n            let filteredText = filterDuplicateLines(text);\n            toast.error(\"Not completed: \" + filteredText, {\n              style: \"border: 1px solid #B22222; padding: 16px; color: #B22222;\", // red-themed style\n              iconTheme: {\n                primary: \"#B22222\",\n                secondary: \"#FFE4E1\" // light red background\n              }\n            });\n          }\n        })\n      )\n      .catch((error) => {\n        console.error(error);\n        toast.error(\"Exercise not completed\");\n      });\n  }\n\n  async function askForHelp() {\n    // help if last notification is older than 5 minutes\n    let notification = await client\n      .collection(\"notifications\")\n      .getFullList({\n        sort: \"-created\"\n      })\n      .then((response) => {\n        return response.find((notification) => {\n          return (\n            notification.user === client.authStore.model?.id &&\n            notification.type === NotificationsTypeOptions.help\n          );\n        });\n      })\n      .catch((error) => {\n        console.log(error);\n      });\n\n    if (notification) {\n      let notificationDate = new Date(notification.created);\n      let now = new Date();\n      let diff = now.getTime() - notificationDate.getTime();\n      let diffMinutes = Math.floor(diff / 1000 / 60);\n      if (diffMinutes < 5) {\n        toast.error(\"You can only ask for help every 5 minutes\");\n        return;\n      }\n    }\n\n    const notification_data: NotificationsRecord = {\n      // @ts-ignore\n      user: client.authStore.model?.id,\n      exercise: $exercise.id,\n      type: NotificationsTypeOptions.help\n    };\n\n    client\n      .collection(\"notifications\")\n      .create(notification_data)\n      .then((response) => {\n        toast.success(\"Help requested, please wait for your trainer.\");\n        helpRequested = true;\n      })\n      .catch((error) => {\n        console.log(error);\n        toast.error(\"Help request failed\");\n      });\n  }\n\n  async function handleRestartExercise() {\n    // fetch POST https://agentUrl/bootstrap\n    // if 200, then exercise is restarted\n    // if 500, then exercise is not restarted\n    restartLoading = true;\n    let lab_session_id = data.pathname.split(\"/\")[2];\n    let exercise_id = window.location.pathname.split(\"/\")[3];\n    let agentHost = window.location.host === \"localhost:5173\" ? \"kubelab.ch\" : window.location.host;\n\n    let agentUrl =\n      agentHost +\n      \"/kubelab-\" +\n      lab_session_id +\n      \"-\" +\n      exercise_id +\n      \"-\" +\n      client.authStore.model?.id;\n\n    await fetch(\"https://\" + agentUrl + \"/bootstrap\", {\n      method: \"POST\"\n    })\n      .then((response) => {\n        if (response.status === 200) {\n          toast.success(\"Exercise restarted\");\n        } else {\n          toast.error(\"Exercise not restarted\");\n        }\n      })\n      .catch((error) => {\n        console.error(error);\n        toast.error(\"Exercise not restarted\");\n      })\n      .finally(() => {\n        restartLoading = false;\n      });\n  }\n\n  async function handleStopExercise() {\n    await client\n      .collection(\"exercise_sessions\")\n      .update($exercise_session.id, {\n        agentRunning: false\n      })\n      .then((record: any) => {\n        toast.success(\"Exercise stopped\");\n        exercise_session.set(record);\n      })\n      .catch((error) => {\n        console.error(error);\n        toast.error(\"Exercise failed to stop\");\n      });\n  }\n\n  function handleOpenVSCode() {\n    // window.open(codeUrl, \"_blank\");\n    $codeView = !$codeView;\n    $loadingCodeEditor = true;\n  }\n\n  function isCurrentExercise(exercise_id: string) {\n    return $exercise.id === exercise_id;\n  }\n</script>\n\n{#key $exercise}\n  <div class=\"absolute top-0 h-20 left-0 right-0 \">\n    <div class=\"mt-5 flex justify-between px-2\">\n      <div class=\"grid grid-cols-2 gap-2\">\n        <button\n          class=\"btn btn-neutral\"\n          on:click={() => {\n            if ($sidebar_lab) {\n              sidebarOpen.set(true);\n            }\n            goto(\"/labs/\");\n          }}\n        >\n          <ArrowLeft class=\"inline-block w-4 h-4 mr-2\" />\n          Labs\n        </button>\n        <div class=\"join \">\n          <button\n            on:click={() => {\n              codeView.set(false);\n            }}\n            class=\"join-item btn {!$codeView\n              ? ' btn-neutral dark:btn-primary dark:text-neutral'\n              : 'btn-outline'} \"\n          >\n            <Terminal class=\"inline-block\" />\n          </button>\n          <button\n            on:click={() => {\n              codeView.set(true);\n            }}\n            class=\"join-item btn {!$codeView\n              ? 'btn-outline'\n              : ' btn-neutral dark:btn-primary dark:text-neutral '}\"\n          >\n            <FileCode2 class=\"inline-block\" />\n          </button>\n        </div>\n      </div>\n      <div class=\"grid grid-cols-2 gap-2\">\n        <div class=\"join grid grid-cols-2\">\n          <button\n            on:click={() => {\n              horizontalView.set(true);\n            }}\n            class=\"join-item btn {$horizontalView\n              ? ' btn-neutral dark:btn-primary dark:text-neutral'\n              : 'btn-outline'} \"\n          >\n            <StretchHorizontal />\n          </button>\n          <button\n            on:click={() => {\n              horizontalView.set(false);\n            }}\n            class=\"join-item btn {$horizontalView\n              ? 'btn-outline'\n              : ' btn-neutral dark:btn-primary dark:text-neutral '}\"\n          >\n            <StretchVertical />\n          </button>\n        </div>\n        <ToggleConfetti>\n          <button\n            slot=\"label\"\n            class=\"btn {!$exercise_session.agentRunning ? 'hidden' : 'btn-success'}\"\n            on:click={() => handleCheckExercise()}\n          >\n            <CheckCircle class=\"inline-block mr-2\" />\n            <span> Check </span>\n          </button>\n          <div\n            style=\"position: fixed; top: -10px; left: 0; height: 100vh; width: 100vw; display: flex; justify-content: center; overflow: hidden; z-index: 10;\"\n          >\n            {#if $exercise_session.endTime}\n              <Confetti\n                x={[-5, 5]}\n                y={[0, 0.1]}\n                delay={[0, 2000]}\n                duration=\"3000\"\n                amount=\"100\"\n                fallDistance=\"100vh\"\n              />\n            {/if}\n          </div>\n        </ToggleConfetti>\n      </div>\n    </div>\n  </div>\n  <div class=\"absolute top-16 bottom-16 left-0 right-0 z-0\">\n    <slot />\n  </div>\n\n  <div class=\"absolute h-16 bottom-0 left-0 right-0\">\n    <div class=\"mt-2 flex justify-between px-2\">\n      <div>\n        {#if $exercise_session.agentRunning}\n          {#if client.authStore.model?.workshop == true}\n            <button\n              class=\"btn btn-accent dark:text-black {helpRequested ? 'btn-disabled' : 'btn-accent'}\"\n              on:click={() => {\n                askForHelp();\n              }}\n            >\n              <HelpCircle class=\"inline-block\" />\n            </button>\n            <Tooltip class=\"bg-neutral\">Call for Help</Tooltip>\n          {/if}\n\n          <button class=\"btn btn-error\" on:click={() => handleStopExercise()}>\n            <StopCircle class=\"inline-block\" />\n          </button>\n          <Tooltip class=\"bg-neutral\">Stop Exercise</Tooltip>\n\n          <button class=\"btn btn-warning\" on:click={() => handleRestartExercise()}>\n            <RotateCw class=\"inline-block {restartLoading ? 'animate-spin' : ''}\" />\n          </button>\n          <Tooltip class=\"bg-neutral\">Reset Exercise</Tooltip>\n        {/if}\n      </div>\n      <div class=\"\">\n        <ul class=\"steps mt-1\">\n          {#if ($sidebar_exercises && $sidebar_exercises.length > 0) || $exercises.length > 0}\n            {#key ($exercise.id, $exercise_session.id, $sidebar_exercises)}\n              {#key ($exercise_session.endTime, $exercise_session.agentRunning, $sidebar_exercises)}\n                {#if $exercises.length > 0}\n                  {#each $exercises as currentExercise, i}\n                    <button\n                      on:click={() => handleSwitchExercise(currentExercise.id)}\n                      data-content={isCurrentExercise(currentExercise.id) ? \"●\" : i + 1}\n                      class=\"step\n      {checkIfExerciseIsDone(currentExercise.id) ? 'step-success' : ''}\n      \"\n                    />\n                  {/each}\n                {:else if $sidebar_exercises}\n                  {#each $sidebar_exercises as currentExercise, i}\n                    <button\n                      on:click={() => handleSwitchExercise(currentExercise.id)}\n                      data-content={isCurrentExercise(currentExercise.id) ? \"●\" : i + 1}\n                      class=\"step\n          {checkIfExerciseIsDone(currentExercise.id) ? 'step-success' : ''}\n          \"\n                    />\n                  {/each}\n                {/if}\n              {/key}\n            {/key}\n          {/if}\n        </ul>\n      </div>\n    </div>\n  </div>\n{/key}\n"
  },
  {
    "path": "kubelab-ui/src/routes/labs/[id]/[id]/+layout.ts",
    "content": "import {\n    UpdateFilterEnum,\n    setExerciseByExerciseSession,\n    setExerciseSessionByExercise,\n    updateDataStores\n} from \"$lib/stores/data\";\nimport toast from \"svelte-french-toast\";\nimport type { PageLoad } from \"../$types\";\n\nexport const load: PageLoad = async ({ params }: any) => {\n    const { id } = params;\n    const labId = window.location.pathname.split(\"/\")[2];\n\n    await updateDataStores({\n        filter: UpdateFilterEnum.ExercisesByLab,\n        labId: labId\n    }).catch((error) => {\n        toast.error(error);\n    });\n\n    await setExerciseByExerciseSession(id).catch((error) => {\n        toast.error(error);\n    });\n\n    await setExerciseSessionByExercise(id).catch((error) => {\n        toast.error(error);\n    });\n};\n"
  },
  {
    "path": "kubelab-ui/src/routes/labs/[id]/[id]/+page.svelte",
    "content": "<script lang=\"ts\">\n  import { Pane, Splitpanes } from \"svelte-splitpanes\";\n  import PlaceholderComponent from \"$lib/components/base/PlaceholderComponent.svelte\";\n  import { onMount, type ComponentType, type SvelteComponentTyped, onDestroy } from \"svelte\";\n  import Desktop from \"$lib/components/base/Desktop.svelte\";\n  import { metadata } from \"$lib/stores/metadata\";\n  import { terminal_size } from \"$lib/stores/terminal\";\n  import SvelteMarkdown from \"svelte-markdown\";\n  import { page } from \"$app/stores\";\n  import {\n    ExerciseSessionLogsTypeOptions,\n    type ExerciseSessionLogsRecord,\n    type ExerciseSessionsRecord\n  } from \"$lib/pocketbase/generated-types.js\";\n  import { client } from \"$lib/pocketbase/index.js\";\n  import toast from \"svelte-french-toast\";\n  import { CheckCircle, InfoIcon, Lightbulb, Play } from \"lucide-svelte\";\n  import {\n    checkIfExerciseIsDone,\n    exercise,\n    exercise_session,\n    exercise_sessions,\n    filterExercisesByLab,\n    loadingCodeEditor\n  } from \"$lib/stores/data\";\n  import CodeSpanComponent from \"$lib/components/markdown/CodeSpanComponent.svelte\";\n  import CodeComponent from \"$lib/components/markdown/CodeComponent.svelte\";\n  import LinkComponent from \"$lib/components/markdown/LinkComponent.svelte\";\n  import ListComponent from \"$lib/components/markdown/ListComponent.svelte\";\n  import horizontalView from \"$lib/stores/tableView\";\n  import { loadingExercises } from \"$lib/stores/loading\";\n  import codeView from \"$lib/stores/codeView\";\n  let Console: ComponentType<SvelteComponentTyped> = PlaceholderComponent;\n\n  let loadingMD = \"\";\n  let showSolution = \"\";\n\n  let docs: string;\n  let hint: string;\n  let solution: string;\n  let clear: any;\n\n  export let data: any;\n  let codeUrl: string;\n\n  function handleIframeLoad() {\n    $loadingCodeEditor = false;\n  }\n\n  $: {\n    // open target blank new tab with the url\n    let lab_session_id = data.pathname.split(\"/\")[2];\n    let exercise_id = window.location.pathname.split(\"/\")[3];\n    let agentHost = window.location.host === \"localhost:5173\" ? \"kubelab.ch\" : window.location.host;\n    codeUrl =\n      \"https://kubelab-\" +\n      lab_session_id +\n      \"-\" +\n      exercise_id +\n      \"-\" +\n      client.authStore.model?.id +\n      \".\" +\n      agentHost;\n  }\n\n  async function getMarkdown() {\n    loadingMD = window.location.pathname.split(\"/\")[3];\n\n    new Promise((resolve, reject) => {\n      fetch($exercise.docs)\n        .then((response) => response.text())\n        .then((text) => {\n          docs = text;\n        })\n        .catch((error) => {\n          reject(error);\n        });\n\n      fetch($exercise.hint)\n        .then((response) => response.text())\n        .then((text) => {\n          hint = text;\n        })\n        .catch((error) => {\n          reject(error);\n        });\n\n      fetch($exercise.solution)\n        .then((response) => response.text())\n        .then((text) => {\n          solution = text;\n        })\n        .catch((error) => {\n          reject(error);\n        });\n    }).finally(() => {\n      loadingMD = \"\";\n    });\n  }\n\n  $: {\n    $page.params.id;\n    if (exercise) {\n      getMarkdown();\n    }\n    clearInterval(clear);\n    clear = setInterval(() => {\n      if (isRecentlyStarted($exercise_session.startTime)) {\n        showSolution = window.location.pathname.split(\"/\")[3];\n      }\n    }, 1000);\n  }\n\n  onMount(async () => {\n    $loadingCodeEditor = true;\n    Console = (await import(\"$lib/components/Console.svelte\")).default;\n  });\n\n  onDestroy(() => {\n    $loadingCodeEditor = false;\n    Console = PlaceholderComponent;\n    clearInterval(clear);\n  });\n\n  $metadata.title = \"Exercise \" + $exercise.title;\n\n  async function handleStartExercise() {\n    const data: ExerciseSessionsRecord = {\n      // @ts-ignore\n      user: client.authStore.model?.id,\n      exercise: $exercise.id,\n      startTime: new Date().toISOString(),\n      endTime: \"\",\n      agentRunning: true\n    };\n\n    loadingExercises.update((exercises) => {\n      exercises.add($exercise.id);\n      return new Set(exercises); // Required for Svelte's reactivity\n    });\n\n    await client\n      .collection(\"exercise_sessions\")\n      .update($exercise_session.id, data)\n      // @ts-ignore\n      .then((record: LabSessionsResponse) => {\n        toast.success(\"Exercise started\");\n        exercise_session.set(record);\n\n        let labId = window.location.pathname.split(\"/\")[2];\n\n        exercise_sessions.update((exercise_sessions) => {\n          return exercise_sessions.map((exercise_session) => {\n            if (exercise_session.id === record.id) {\n              return record;\n            }\n            return exercise_session;\n          });\n        });\n\n        filterExercisesByLab(labId);\n\n        // make an entry in the exercise_session_logs collection\n\n        const exercise_session_log_data: ExerciseSessionLogsRecord = {\n          // @ts-ignore\n          user: client.authStore.model?.id,\n          exercise_session: $exercise_session.id,\n          type: ExerciseSessionLogsTypeOptions.start,\n          timestamp: new Date().toISOString()\n        };\n\n        client\n          .collection(\"exercise_session_logs\")\n          .create(exercise_session_log_data)\n          .then((response) => {\n            console.log(response);\n          })\n          .catch((error) => {\n            console.log(error);\n          });\n      })\n      .catch((error) => {\n        console.error(error);\n        toast.error(\"Exercise failed to start. Lab is probably still starting.\");\n      })\n      .finally(() => {\n        loadingExercises.update((exercises) => {\n          exercises.delete($exercise.id);\n          return new Set(exercises); // Required for Svelte's reactivity\n        });\n      });\n  }\n\n  function isRecentlyStarted(startTime: string) {\n    let now = new Date();\n    let start = new Date(startTime);\n    let diff = Math.abs(now.getTime() - start.getTime());\n    let minutes = Math.floor(diff / 1000 / 60);\n    return minutes > 5;\n  }\n\n  function openModal() {\n    // @ts-ignore\n    window.my_modal_1.showModal();\n  }\n\n  function openModal2() {\n    // @ts-ignore\n    window.my_modal_2.showModal();\n  }\n</script>\n\n{#key ($page.params, $loadingExercises)}\n  {#if $horizontalView}\n    <Splitpanes horizontal class=\"p-2 mt-2 pb-2\">\n      <Pane class=\" rounded-lg\">\n        <Splitpanes>\n          <Pane class=\"relative\">\n            <div\n              class=\"py-6 px-8 leading-8 h-full overflow-y-scroll dark:bg-base-100 bg-white scrollbar-none\"\n            >\n              {#key $page.params}\n                <SvelteMarkdown\n                  source={docs}\n                  renderers={{\n                    codespan: CodeSpanComponent,\n                    code: CodeComponent,\n                    link: LinkComponent,\n                    list: ListComponent\n                  }}\n                />\n              {/key}\n            </div>\n            <div class=\"bottom-0 flex w-full justify-between absolute p-2\">\n              <div>\n                <!-- svelte-ignore missing-declaration -->\n                <div class=\"tooltip\" data-tip=\"Hint\">\n                  <button\n                    class=\"btn btn-circle btn-neutral dark:btn-primary dark:text-neutral\"\n                    on:click={() => openModal2()}\n                  >\n                    <InfoIcon size={16} />\n                  </button>\n                </div>\n                <dialog id=\"my_modal_2\" class=\"modal\">\n                  <form method=\"dialog\" class=\"modal-box\">\n                    <h3 class=\"font-bold text-lg\">Hint</h3>\n                    <button class=\"btn btn-sm btn-circle btn-ghost absolute right-2 top-2\">✕</button\n                    >\n                    <SvelteMarkdown\n                      source={hint}\n                      renderers={{\n                        codespan: CodeSpanComponent,\n                        code: CodeComponent,\n                        link: LinkComponent,\n                        list: ListComponent\n                      }}\n                    />\n                  </form>\n                </dialog>\n              </div>\n              <div>\n                <!-- svelte-ignore missing-declaration -->\n                <div class=\"tooltip\" data-tip=\"Solution\">\n                  <button\n                    class=\"btn btn-circle {(showSolution ===\n                      window.location.pathname.split('/')[3] &&\n                      $exercise_session.agentRunning) ||\n                    $exercise_session.endTime\n                      ? 'btn-neutral  dark:btn-primary dark:text-neutral'\n                      : 'btn-disabled'}\"\n                    on:click={() => openModal()}\n                  >\n                    <Lightbulb size={16} />\n                  </button>\n                </div>\n                <dialog id=\"my_modal_1\" class=\"modal\">\n                  <form method=\"dialog\" class=\"modal-box\">\n                    <h3 class=\"font-bold text-lg\">Solution</h3>\n                    <button class=\"btn btn-sm btn-circle btn-ghost absolute right-2 top-2\">✕</button\n                    >\n                    <SvelteMarkdown\n                      source={solution}\n                      renderers={{\n                        codespan: CodeSpanComponent,\n                        code: CodeComponent,\n                        link: LinkComponent,\n                        list: ListComponent\n                      }}\n                    />\n                  </form>\n                </dialog>\n              </div>\n            </div>\n          </Pane>\n        </Splitpanes>\n      </Pane>\n      <Pane bind:size={$terminal_size.height} class=\"rounded-lg\">\n        {#if $exercise_session.agentRunning}\n          {#key $page.params}\n            {#if $exercise_session.exercise === $exercise.id}\n              {#if $codeView}\n                {#if $loadingCodeEditor}\n                  <div class=\"flex justify-center items-center h-full dark:bg-neutral\">\n                    <span class=\"loading loading-dots loading-lg\" />\n                  </div>\n                {/if}\n                <iframe\n                  src={codeUrl}\n                  on:load={() => handleIframeLoad()}\n                  title=\"Code Editor\"\n                  class=\"w-full h-full  bg-transparent\"\n                />\n              {:else}\n                <Desktop {Console} />\n              {/if}\n            {/if}\n          {/key}\n        {:else}\n          <!-- button to start the agent -->\n          {#key $page.params}\n            <div\n              class=\"flex justify-center items-center h-full dark:bg-neutral {checkIfExerciseIsDone(\n                $exercise.id\n              )\n                ? 'bg-green-200'\n                : ''}\"\n            >\n              <div class=\"text-center\">\n                {#if checkIfExerciseIsDone($exercise.id)}\n                  <h1 class=\"text-4xl font-bold\">\n                    <CheckCircle class=\"inline-block w-12 h-12\" />\n                    {checkIfExerciseIsDone($exercise.id) ? \"Exercise done\" : \"Exercise not started\"}\n                  </h1>\n                {:else}\n                  <button\n                    class=\"btn {checkIfExerciseIsDone($exercise.id)\n                      ? 'btn-warning'\n                      : 'btn-neutral  dark:btn-primary dark:text-neutral'} mt-4\"\n                    on:click={() => handleStartExercise()}\n                  >\n                    {#if $loadingExercises.has($exercise.id)}\n                      <span class=\"loading loading-dots loading-md\" /> starting...\n                    {:else}\n                      <Play /> Start Terminal\n                    {/if}\n                  </button>\n                {/if}\n              </div>\n            </div>\n          {/key}\n        {/if}\n      </Pane>\n    </Splitpanes>\n  {:else}\n    <Splitpanes class=\"p-2 mt-2\">\n      <Pane bind:size={$terminal_size.height} class=\"rounded-lg\">\n        {#if $exercise_session.agentRunning}\n          {#key $page.params}\n            {#if $exercise_session.exercise === $exercise.id}\n              {#if $codeView}\n                {#if $loadingCodeEditor}\n                  <div class=\"flex justify-center items-center h-full dark:bg-neutral\">\n                    <span class=\"loading loading-dots loading-lg\" />\n                  </div>\n                {/if}\n                {#key ($page.params, codeUrl)}\n                  <iframe\n                    src={codeUrl}\n                    on:load={() => handleIframeLoad()}\n                    title=\"Code Editor\"\n                    class=\"w-full h-full  bg-transparent\"\n                  />\n                {/key}\n              {:else}\n                <Desktop {Console} />\n              {/if}\n            {/if}\n          {/key}\n        {:else}\n          <!-- button to start the agent -->\n          {#key $page.params}\n            <div\n              class=\"flex justify-center items-center h-full dark:bg-neutral {checkIfExerciseIsDone(\n                $exercise.id\n              )\n                ? 'bg-green-200'\n                : ''}\"\n            >\n              <div class=\"text-center\">\n                {#if checkIfExerciseIsDone($exercise.id)}\n                  <h1 class=\"text-4xl font-bold\">\n                    <CheckCircle class=\"inline-block w-12 h-12\" />\n                    {checkIfExerciseIsDone($exercise.id) ? \"Exercise done\" : \"Exercise not started\"}\n                  </h1>\n                {:else}\n                  <button\n                    class=\"btn {checkIfExerciseIsDone($exercise.id)\n                      ? 'btn-warning'\n                      : 'btn-neutral  dark:btn-primary dark:text-neutral'} mt-4\"\n                    on:click={() => handleStartExercise()}\n                  >\n                    {#if $loadingExercises.has($exercise.id)}\n                      <span class=\"loading loading-dots loading-md\" /> starting...\n                    {:else}\n                      <Play /> Start Terminal\n                    {/if}\n                  </button>\n                {/if}\n              </div>\n            </div>\n          {/key}\n        {/if}\n      </Pane>\n      <Pane class=\"rounded-lg\">\n        <Splitpanes horizontal>\n          <Pane class=\"relative\">\n            <div\n              class=\"p-6 leading-8 h-full overflow-y-scroll dark:bg-base-100 bg-white scrollbar-none\"\n            >\n              {#key $page.params}\n                <SvelteMarkdown\n                  source={docs}\n                  renderers={{\n                    codespan: CodeSpanComponent,\n                    code: CodeComponent,\n                    link: LinkComponent,\n                    list: ListComponent\n                  }}\n                />\n              {/key}\n            </div>\n            <div class=\"bottom-0 flex w-full justify-between absolute p-2\">\n              <div>\n                <!-- svelte-ignore missing-declaration -->\n                <div class=\"tooltip\" data-tip=\"Hint\">\n                  <button\n                    class=\"btn btn-circle btn-neutral dark:btn-primary dark:text-neutral\"\n                    on:click={() => openModal2()}\n                  >\n                    <InfoIcon size={16} />\n                  </button>\n                </div>\n                <dialog id=\"my_modal_2\" class=\"modal\">\n                  <form method=\"dialog\" class=\"modal-box\">\n                    <h3 class=\"font-bold text-lg\">Hint</h3>\n                    <button class=\"btn btn-sm btn-circle btn-ghost absolute right-2 top-2\">✕</button\n                    >\n                    <SvelteMarkdown\n                      source={hint}\n                      renderers={{\n                        codespan: CodeSpanComponent,\n                        code: CodeComponent,\n                        link: LinkComponent,\n                        list: ListComponent\n                      }}\n                    />\n                  </form>\n                </dialog>\n              </div>\n              <div>\n                <!-- svelte-ignore missing-declaration -->\n                <div class=\"tooltip\" data-tip=\"Solution\">\n                  <button\n                    class=\"btn btn-circle {(showSolution ===\n                      window.location.pathname.split('/')[3] &&\n                      $exercise_session.agentRunning) ||\n                    $exercise_session.endTime\n                      ? 'btn-neutral  dark:btn-primary dark:text-neutral'\n                      : 'btn-disabled'}\"\n                    on:click={() => openModal()}\n                  >\n                    <Lightbulb size={16} />\n                  </button>\n                </div>\n                <dialog id=\"my_modal_1\" class=\"modal\">\n                  <form method=\"dialog\" class=\"modal-box\">\n                    <h3 class=\"font-bold text-lg\">Solution</h3>\n\n                    <SvelteMarkdown\n                      source={solution}\n                      renderers={{\n                        codespan: CodeSpanComponent,\n                        code: CodeComponent,\n                        link: LinkComponent,\n                        list: ListComponent\n                      }}\n                    />\n                    <div class=\"modal-action\">\n                      <!-- if there is a button in form, it will close the modal -->\n                      <button class=\"btn\">Close</button>\n                    </div>\n                  </form>\n                </dialog>\n              </div>\n            </div>\n          </Pane>\n        </Splitpanes>\n      </Pane>\n    </Splitpanes>\n  {/if}\n{/key}\n\n<style>\n  * {\n    word-break: keep-all;\n    line-height: 2;\n  }\n</style>\n"
  },
  {
    "path": "kubelab-ui/src/routes/login/+page.svelte",
    "content": "<script lang=\"ts\">\n  import { goto } from \"$app/navigation\";\n  import ToggleConfetti from \"$lib/components/base/ToggleConfetti.svelte\";\n  import { login } from \"$lib/pocketbase\";\n  import { alertOnFailure } from \"$lib/pocketbase/ui\";\n  import darkTheme from \"$lib/stores/theme\";\n\n  // @ts-ignore\n  import { Confetti } from \"svelte-confetti\";\n  import toast from \"svelte-french-toast\";\n\n  const DEFAULTS = {\n    email: \"\",\n    password: \"\"\n  };\n  let user = { ...DEFAULTS };\n  let loading = false;\n\n  async function submit() {\n    loading = true;\n    await alertOnFailure(async function () {\n      await login(user.email, user.password);\n      toast.success(\"Logged in successfully!\");\n      goto(\"/app\");\n    }).finally(() => {\n      loading = false;\n    });\n  }\n</script>\n\n<!-- component -->\n<div\n  class=\"bg-no-repeat bg-cover bg-center relative\n  bg-gradient-to-r from-blue-500 to-purple-500 dark:from-base-100 dark:to-base-100\n  \"\n>\n  <div class=\"absolute sm:inset-0 z-0\" />\n  <div class=\"min-h-screen sm:flex sm:flex-row mx-0 justify-center\">\n    <div class=\"flex-col flex self-center p-10 sm:max-w-5xl xl:max-w-2xl  z-10\">\n      <div class=\"self-start hidden lg:flex flex-col text-white\">\n        <h1 class=\"mb-3 font-bold text-5xl\">\n          Hi, Welcome to <span class=\"font-bold text-5xl\"> KubeLab</span>\n        </h1>\n        <p class=\"pr-3\">Experience Kubernetes Mastery Through Practice.</p>\n        <a href=\"https://natron.io\" target=\"_blank\" class=\"mt-10\">\n          <span class=\"text-xs font-semibold leading-6 dark:text-gray-900 text-white\">Powered by</span>\n          {#if $darkTheme === false}\n            <img class=\"h-4 w-auto\" src={\"/images/natron-dark.png\"} alt=\"Switzerland\" />\n          {:else}\n            <img class=\"h-4 w-auto\" src={\"/images/natron.png\"} alt=\"Switzerland\" />\n          {/if}\n        </a>\n      </div>\n    </div>\n    <form\n      class=\"flex justify-center self-center z-10 \"\n      method=\"POST\"\n      on:submit|preventDefault={submit}\n    >\n      <div class=\"p-12 bg-white dark:bg-neutral mx-auto rounded-2xl w-100\">\n        <div class=\"mb-4\">\n          <ToggleConfetti>\n            <div class=\"btn btn-block btn-ghost normal-case text-xl mb-10\" slot=\"label\">\n              <img src=\"/images/kubelab-logo.png\" alt=\"logo\" class=\"w-8 h-8 mr-2\" /> KubeLab\n            </div>\n            <Confetti />\n          </ToggleConfetti>\n          <h3 class=\"font-semibold text-2xl \">Sign In</h3>\n          <p class=\"text-gray-500\">Please sign in to your account.</p>\n        </div>\n        <div class=\"space-y-5\">\n          <div class=\"space-y-2\">\n            <label class=\"text-sm font-medium tracking-wide\">Email</label>\n            <input\n              type=\"text\"\n              placeholder=\"your@email.com\"\n              class=\"input input-bordered w-full max-w-xs\"\n              required\n              bind:value={user.email}\n            />\n          </div>\n          <div class=\"space-y-2\">\n            <label class=\"mb-5 text-sm font-medium tracking-wide\"> Password </label>\n            <input\n              type=\"password\"\n              placeholder=\"Password\"\n              class=\"input input-bordered w-full max-w-xs\"\n              required\n              bind:value={user.password}\n            />\n          </div>\n          <div>\n            <button type=\"submit\" class=\"btn btn-neutral btn-block\">\n              {#if loading}\n                <span class=\"loading loading-dots loading-md\" /> Loading\n              {:else}\n                Sign in\n              {/if}\n            </button>\n          </div>\n        </div>\n        <div class=\"pt-5 text-center text-gray-400 text-xs\">\n          <span>\n            Copyright © {new Date().getFullYear()}\n            <a\n              href=\"https://natron.io\"\n              rel=\"\"\n              target=\"_blank\"\n              title=\"Natron Tech\"\n              class=\"text-blue hover:text-blue-500 \">Natron Tech</a\n            ></span\n          >\n        </div>\n      </div>\n    </form>\n  </div>\n</div>\n"
  },
  {
    "path": "kubelab-ui/src/routes/material/+layout.svelte",
    "content": "<div class=\"absolute top-[78px] bottom-0 right-0 left-0 overflow-y-scroll p-2\">\n  <slot />\n</div>\n"
  },
  {
    "path": "kubelab-ui/src/routes/material/+page.svelte",
    "content": "<script>\n  import { Download, ExternalLink, Presentation } from \"lucide-svelte\";\n</script>\n\n<h1 class=\"text-center text-4xl font-bold my-4\">Material</h1>\n<div class=\"p-2 grid grid-cols-1 sm:grid-cols-3 gap-4\">\n  <div class=\"card card-compact col-span-1 w-96 shadow bg-blue-500 text-white mx-auto\">\n    <div class=\"card-body\">\n      <h2 class=\"card-title\">Presentation Slides Day 1</h2>\n      <p class=\"text-white\">Here you can find the slides for the presentation of the first day.</p>\n      <div class=\"card-actions justify-end\">\n        <button\n          class=\"btn btn-neutral\"\n          on:click={() => window.open(\"/assets/slides_day_1.pdf\")}\n        >\n          <Download /> Download\n        </button>\n      </div>\n    </div>\n  </div>\n  <div class=\"card card-compact col-span-1 w-96 shadow bg-blue-500 text-white mx-auto\">\n    <div class=\"card-body\">\n      <h2 class=\"card-title\">Presentation Slides Day 2</h2>\n      <p class=\"text-white\">Here you can find the slides for the presentation of the second day.</p>\n      <div class=\"card-actions justify-end\">\n        <button\n          class=\"btn btn-neutral\"\n          on:click={() => window.open(\"/assets/slides_day_2.pdf\")}\n        >\n          <Download /> Download\n        </button>\n      </div>\n    </div>\n  </div>\n  <div class=\"card card-compact col-span-1 w-96 shadow bg-neutral text-white mx-auto\">\n    <div class=\"card-body\">\n      <h2 class=\"card-title\">Cheat Sheet</h2>\n      <p class=\"text-white\">Here you can find the cheat sheet for the exercises.</p>\n      <div class=\"card-actions justify-end\">\n        <button\n          class=\"btn btn-neutral\"\n          on:click={() => window.open(\"/assets/cheat-sheet-compressed.pdf\")}\n        >\n          <Download /> Download\n        </button>\n      </div>\n    </div>\n  </div>\n  <div class=\"divider col-span-3\">AND</div>\n  <h1 class=\"text-center text-4xl font-bold my-4 col-span-3\">Links</h1>\n  <div class=\"card card-compact w-96 shadow bg-orange-500 text-white mx-auto\">\n    <div class=\"card-body\">\n      <h2 class=\"card-title\">Kubernetes Documentation</h2>\n      <p class=\"text-white\">Here you can find the official documentation for Kubernetes.</p>\n      <div class=\"card-actions justify-end\">\n        <button\n          class=\"btn btn-neutral\"\n          on:click={() => window.open(\"https://kubernetes.io/docs/home/\")}\n        >\n          <ExternalLink /> Go to Docs\n        </button>\n      </div>\n    </div>\n  </div>\n  <div class=\"card card-compact w-96 shadow bg-red-500 text-white mx-auto\">\n    <div class=\"card-body\">\n      <h2 class=\"card-title\">Kubernetes API Reference</h2>\n      <p class=\"text-white\">Here you can find the official API reference for Kubernetes.</p>\n      <div class=\"card-actions justify-end\">\n        <button\n          class=\"btn btn-neutral\"\n          on:click={() =>\n            window.open(\"https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/\")}\n        >\n          <ExternalLink /> Go to Docs\n        </button>\n      </div>\n    </div>\n  </div>\n  <div class=\"card card-compact w-96 shadow bg-purple-500 text-white mx-auto\">\n    <div class=\"card-body\">\n      <h2 class=\"card-title\">Troubleshooting Kubernetes Deployments</h2>\n      <p class=\"text-white\">\n        Here you can find a guide on how to troubleshoot Kubernetes deployments.\n      </p>\n      <div class=\"card-actions justify-end\">\n        <button\n          class=\"btn btn-neutral\"\n          on:click={() =>\n            window.open(\n              \"https://gandrille.github.io/tech-notes/Docker_and_Kubernetes/Kubernetes_Core/Security_and_monitoring/Monitoring,_Logging,_Troubleshooting/troubleshooting-kubernetes.pdf\"\n            )}\n        >\n          <ExternalLink /> Go to Docs\n        </button>\n      </div>\n    </div>\n  </div>\n  <div class=\"divider col-span-3\">AND</div>\n  <h1 class=\"text-center text-4xl font-bold my-4 col-span-3\">Tools</h1>\n  <div class=\"card card-compact w-96 bg-base-100 shadow-xl mx-auto\">\n    <figure><img src=\"/assets/openlens.png\" alt=\"Shoes\" /></figure>\n    <div class=\"card-body\">\n      <h2 class=\"card-title\">Openlens</h2>\n      <p>\n        OpenLens is the official UI for Lens, the Kubernetes IDE. It is open source and free to use.\n      </p>\n      <div class=\"card-actions justify-end\">\n        <button\n          class=\"btn btn-neutral\"\n          on:click={() => window.open(\"https://github.com/MuhammedKalkan/OpenLens\")}\n        >\n          <ExternalLink /> Go to Website\n        </button>\n      </div>\n    </div>\n  </div>\n  <div class=\"card card-compact w-96 bg-base-100 shadow-xl mx-auto\">\n    <figure><img src=\"/assets/k9s.png\" alt=\"Shoes\" /></figure>\n    <div class=\"card-body\">\n      <h2 class=\"card-title\">K9s</h2>\n      <p>\n        K9s provides a curses based terminal UI to interact with your Kubernetes clusters. The aim\n        of this project is to make it easier to navigate, observe and manage your applications in\n        the wild. K9s continually watches Kubernetes for changes and offers subsequent commands to\n        interact with your observed resources.\n      </p>\n      <div class=\"card-actions justify-end\">\n        <button\n          class=\"btn btn-neutral\"\n          on:click={() => window.open(\"https://k9scli.io/\", \"_blank\")}\n        >\n          <ExternalLink /> Go to Website\n        </button>\n      </div>\n    </div>\n  </div>\n  <div class=\"card card-compact w-96 bg-base-100 shadow-xl mx-auto\">\n    <figure><img src=\"/assets/kubeview.png\" alt=\"Shoes\" /></figure>\n    <div class=\"card-body\">\n      <h2 class=\"card-title\">Kubeview</h2>\n      <p>\n        KubeView is a Kubernetes cluster visualiser and graphical explorer. It provides a high-level\n        view of Kubernetes clusters and the objects running inside them. This gives administrators\n        and developers a better understanding of their Kubernetes clusters, as well as providing a\n        visual way to interact with and diagnose their applications.\n      </p>\n      <div class=\"card-actions justify-end\">\n        <button\n          class=\"btn btn-neutral\"\n          on:click={() => window.open(\"https://github.com/benc-uk/kubeview\")}\n        >\n          <ExternalLink /> Go to Website\n        </button>\n      </div>\n    </div>\n  </div>\n  <div class=\"card card-compact w-96 bg-base-100 shadow-xl mx-auto\">\n    <figure><img src=\"/assets/kubeshark.png\" alt=\"Shoes\" /></figure>\n    <div class=\"card-body\">\n      <h2 class=\"card-title\">Kubeshark</h2>\n      <p>\n        Kubeshark is a Kubernetes cluster visualiser and graphical explorer. It provides a\n        high-level view of Kubernetes clusters and the objects running inside them. This gives\n        administrators and developers a better understanding of their Kubernetes clusters, as well\n        as providing a visual way to interact with and diagnose their applications.\n      </p>\n      <div class=\"card-actions justify-end\">\n        <button\n          class=\"btn btn-neutral\"\n          on:click={() => window.open(\"https://kubeshark.co/\")}\n        >\n          <ExternalLink /> Go to Website\n        </button>\n      </div>\n    </div>\n  </div>\n\n</div>\n"
  },
  {
    "path": "kubelab-ui/src/styles/prism.css",
    "content": "/* PrismJS 1.29.0\nhttps://prismjs.com/download.html#themes=prism&languages=markup+css+bash+yaml */\ncode[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}\n"
  },
  {
    "path": "kubelab-ui/src/styles/xterm.css",
    "content": "/**\n * Copyright (c) 2014 The xterm.js authors. All rights reserved.\n * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)\n * https://github.com/chjj/term.js\n * @license MIT\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n *\n * Originally forked from (with the author's permission):\n *   Fabrice Bellard's javascript vt100 for jslinux:\n *   http://bellard.org/jslinux/\n *   Copyright (c) 2011 Fabrice Bellard\n *   The original design remains. The terminal itself\n *   has been extended to include xterm CSI codes, among\n *   other features.\n */\n\n/**\n *  Default styles for xterm.js\n */\n\n.xterm {\n    cursor: text;\n    position: relative;\n    user-select: none;\n    -ms-user-select: none;\n    -webkit-user-select: none;\n}\n\n.xterm.focus,\n.xterm:focus {\n    outline: none;\n}\n\n.xterm .xterm-helpers {\n    position: absolute;\n    top: 0;\n    /**\n     * The z-index of the helpers must be higher than the canvases in order for\n     * IMEs to appear on top.\n     */\n    z-index: 5;\n}\n\n.xterm .xterm-helper-textarea {\n    padding: 0;\n    border: 0;\n    margin: 0;\n    /* Move textarea out of the screen to the far left, so that the cursor is not visible */\n    position: absolute;\n    opacity: 0;\n    left: -9999em;\n    top: 0;\n    width: 0;\n    height: 0;\n    z-index: -5;\n    /** Prevent wrapping so the IME appears against the textarea at the correct position */\n    white-space: nowrap;\n    overflow: hidden;\n    resize: none;\n}\n\n.xterm .composition-view {\n    /* TODO: Composition position got messed up somewhere */\n    background: #000;\n    color: #fff;\n    display: none;\n    position: absolute;\n    white-space: nowrap;\n    z-index: 1;\n}\n\n.xterm .composition-view.active {\n    display: block;\n}\n\n.xterm .xterm-viewport {\n    /* On OS X this is required in order for the scroll bar to appear fully opaque */\n    background-color: #000;\n    overflow-y: scroll;\n    cursor: default;\n    position: absolute;\n    right: 0;\n    left: 0;\n    top: 0;\n    bottom: 0;\n}\n\n.xterm .xterm-screen {\n    position: relative;\n}\n\n.xterm .xterm-screen canvas {\n    position: absolute;\n    left: 0;\n    top: 0;\n}\n\n.xterm .xterm-scroll-area {\n    visibility: hidden;\n}\n\n.xterm-char-measure-element {\n    display: inline-block;\n    visibility: hidden;\n    position: absolute;\n    top: 0;\n    left: -9999em;\n    line-height: normal;\n}\n\n.xterm.enable-mouse-events {\n    /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */\n    cursor: default;\n}\n\n.xterm.xterm-cursor-pointer,\n.xterm .xterm-cursor-pointer {\n    cursor: pointer;\n}\n\n.xterm.column-select.focus {\n    /* Column selection mode */\n    cursor: crosshair;\n}\n\n.xterm .xterm-accessibility,\n.xterm .xterm-message {\n    position: absolute;\n    left: 0;\n    top: 0;\n    bottom: 0;\n    z-index: 10;\n    color: transparent;\n}\n\n.xterm .live-region {\n    position: absolute;\n    left: -9999px;\n    width: 1px;\n    height: 1px;\n    overflow: hidden;\n}\n\n.xterm-dim {\n    opacity: 0.5;\n}\n\n.xterm-underline-1 {\n    text-decoration: underline;\n}\n.xterm-underline-2 {\n    text-decoration: double underline;\n}\n.xterm-underline-3 {\n    text-decoration: wavy underline;\n}\n.xterm-underline-4 {\n    text-decoration: dotted underline;\n}\n.xterm-underline-5 {\n    text-decoration: dashed underline;\n}\n\n.xterm-strikethrough {\n    text-decoration: line-through;\n}\n\n.xterm-screen .xterm-decoration-container .xterm-decoration {\n    z-index: 6;\n    position: absolute;\n}\n\n.xterm-decoration-overview-ruler {\n    z-index: 7;\n    position: absolute;\n    top: 0;\n    right: 0;\n    pointer-events: none;\n}\n\n.xterm-decoration-top {\n    z-index: 2;\n    position: relative;\n}\n"
  },
  {
    "path": "kubelab-ui/svelte.config.js",
    "content": "// import adapter from '@sveltejs/adapter-node';\nimport adapter from \"@sveltejs/adapter-static\";\nimport { vitePreprocess } from \"@sveltejs/kit/vite\";\nimport preprocess from \"svelte-preprocess\";\n\n/** @type {import('@sveltejs/kit').Config} */\nconst config = {\n    // Consult https://kit.svelte.dev/docs/integrations#preprocessors\n    // for more information about preprocessors\n    preprocess: [\n        vitePreprocess(),\n        preprocess({\n            postcss: true\n        })\n    ],\n    kit: {\n        // adapter: adapter({\n        // \tout: 'build',\n        // \tenvPrefix: 'KUBELAB_',\n        // \tpolyfill: false\n        // }),\n        alias: {\n            $lib: \"src/lib\"\n        },\n        adapter: adapter({\n            // Prerendering turned off. Turn it on if you know what you're doing.\n            prerender: { entries: [] },\n            fallback: \"index.html\" // enable SPA mode\n        }),\n        csrf: {\n            checkOrigin: false\n        }\n    },\n    onwarn: (warning, handler) => {\n        if (warning.code.startsWith(\"a11y-\")) return;\n        handler(warning);\n    }\n};\n\nexport default config;\n"
  },
  {
    "path": "kubelab-ui/tailwind.config.cjs",
    "content": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  darkMode: \"class\",\n  content: [\"./src/**/*.{html,js,svelte,ts}\"],\n  theme: {\n    extend: {\n      screens: {\n        \"hover-hover\": { raw: \"(hover: hover)\" }\n      }\n    }\n  },\n  plugins: [\n    require(\"@tailwindcss/forms\"),\n    require(\"@tailwindcss/typography\"),\n    require(\"tailwind-scrollbar\"),\n    require(\"daisyui\"),\n    require(\"flowbite/plugin\")\n  ],\n  daisyui: {\n    logs: false,\n    themes: [\"light\", \"dark\"],\n  }\n};\n"
  },
  {
    "path": "kubelab-ui/tsconfig.json",
    "content": "{\n  \"extends\": \"./.svelte-kit/tsconfig.json\",\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"checkJs\": true,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true,\n    \"skipLibCheck\": true,\n    \"sourceMap\": true,\n    \"strict\": true\n  }\n  // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias\n  //\n  // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes\n  // from the referenced tsconfig.json - TypeScript does not merge them in\n}\n"
  },
  {
    "path": "kubelab-ui/vite.config.ts",
    "content": "import { sveltekit } from \"@sveltejs/kit/vite\";\nimport type { UserConfig } from \"vite\";\nimport fs from \"fs\";\n\n// detect if we're running inside docker and set the backend accordingly\nconst pocketbase_url = fs.existsSync(\"/.dockerenv\")\n    ? \"http://pb:8090\" // docker-to-docker\n    : \"http://localhost:8090\"; // localhost-to-localhost\n\nconst config: UserConfig = {\n    resolve: {\n        alias: {\n            \"@\": __dirname + \"/src\"\n        }\n    },\n    plugins: [sveltekit()],\n    server: {\n        proxy: {\n            // proxy \"/api\" and \"/_\" to pocketbase_url\n            \"/api\": pocketbase_url,\n            \"/_\": pocketbase_url\n        },\n        headers: {\n            \"Cross-Origin-Embedder-Policy\": \"require-corp\",\n            \"Cross-Origin-Opener-Policy\": \"same-origin\"\n        }\n    }\n};\n\nexport default config;\n"
  }
]