diff --git a/.dockerignore b/.dockerignore index 9612375..69b7a82 100644 --- a/.dockerignore +++ b/.dockerignore @@ -35,3 +35,10 @@ /app/assets/builds/* !/app/assets/builds/.keep /public/assets + +# Legacy Rails HTML/Administrate code. These directories belonged to the old +# monolithic frontend and must never be eager-loaded by the API image. Keeping +# the exclusions also makes upgrades safe when a ZIP is unpacked over an older +# project directory, because archive extraction does not delete stale files. +/app/controllers/admin/ +/app/dashboards/ diff --git a/Dockerfile b/Dockerfile index a688257..90e7cf2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,21 +33,21 @@ COPY . . # Precompile bootsnap code for faster boot times RUN bundle exec bootsnap precompile app/ lib/ +# Fail the image build early when a removed legacy dependency is still +# referenced by an eagerly loaded Rails constant. +RUN SECRET_KEY_BASE_DUMMY=1 bundle exec rails zeitwerk:check + # Adjust binfiles to be executable on Linux RUN chmod +x bin/* && \ sed -i "s/\r$//g" bin/* && \ sed -i 's/ruby\.exe$/ruby/' bin/* -# Precompiling assets for production without requiring secret RAILS_MASTER_KEY -RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile - - # Final stage for app image FROM base # Install packages needed for deployment RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y curl libvips postgresql-client-17 libyaml-dev zip && \ + apt-get install --no-install-recommends -y curl libvips postgresql-client-17 libyaml-dev zip unzip && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives # Copy built artifacts: gems, application @@ -56,6 +56,7 @@ COPY --from=build /rails /rails # Run and own only the runtime files as a non-root user for security RUN useradd rails --create-home --shell /bin/bash && \ + mkdir -p db log storage tmp/pids tmp/cache tmp/sockets && \ chown -R rails:rails db log storage tmp USER rails:rails @@ -63,8 +64,8 @@ USER rails:rails ENTRYPOINT ["/rails/bin/docker-entrypoint"] HEALTHCHECK --interval=5m --timeout=3s \ - CMD curl -f http://localhost:3000 || exit 1 + CMD curl -f http://localhost:3000/up || exit 1 # Start the server by default, this can be overwritten at runtime #EXPOSE 3000 -#CMD ["./bin/rails", "server"] +CMD ["./bin/rails", "server", "-b", "0.0.0.0"] diff --git a/Gemfile b/Gemfile index de74b49..6f1d5de 100644 --- a/Gemfile +++ b/Gemfile @@ -6,25 +6,18 @@ source "https://rubygems.org" gem "rails", "~> 7.1.5", ">= 7.1.5.2" # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] -gem "sprockets-rails" # Use postgresql as the database for Active Record gem "pg", "~> 1.1" # Use the Puma web server [https://github.com/puma/puma] gem "puma", ">= 5.0" -gem 'turbolinks', '~> 5' # Build JSON APIs with ease [https://github.com/rails/jbuilder] -gem "jbuilder" +# CSV is no longer bundled by default in newer Ruby versions. The API uses it +# for the entry export, therefore keep it as an explicit dependency. +gem "csv", "~> 3.3" gem 'tzinfo-data' -gem 'jquery-rails' - -# Use jquery-ui for pretty UI -gem 'jquery-ui-rails' - -# Use Sass to process CSS -gem "sassc-rails" gem "redis" # Use Redis adapter to run Action Cable in production # gem "redis", ">= 4.0.1" @@ -44,7 +37,6 @@ gem "bootsnap", require: false # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] # gem "image_processing", "~> 1.2" -gem 'administrate' group :development do # Use console on exceptions pages [https://github.com/rails/web-console] @@ -63,9 +55,4 @@ gem 'pghero' gem "faraday" gem "nokogiri" -gem 'importmap-rails' gem "good_job", "~> 4.13" -gem "importmap-rails" -gem "turbo-rails" -gem "stimulus-rails" -gem "chartkick" \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 28dc594..8494b80 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -98,6 +98,8 @@ GEM concurrent-ruby (1.3.5) connection_pool (2.5.4) crass (1.0.6) + csv (3.3.2) + stringio date (3.5.0) devise (4.9.4) bcrypt (~> 3.0) @@ -320,16 +322,11 @@ PLATFORMS x86_64-linux DEPENDENCIES - administrate bootsnap - chartkick + csv (~> 3.3) devise faraday good_job (~> 4.13) - importmap-rails - jbuilder - jquery-rails - jquery-ui-rails nokogiri pg (~> 1.1) pghero @@ -337,11 +334,6 @@ DEPENDENCIES rails (~> 7.1.5, >= 7.1.5.2) redis rufus-scheduler - sassc-rails - sprockets-rails - stimulus-rails - turbo-rails - turbolinks (~> 5) tzinfo-data web-console diff --git a/MIGRATION_FEATURES.md b/MIGRATION_FEATURES.md new file mode 100644 index 0000000..23b9032 --- /dev/null +++ b/MIGRATION_FEATURES.md @@ -0,0 +1,29 @@ +# Funktionsabgleich mit dem ursprünglichen Rails-Projekt + +| Ursprüngliche Funktion | Neue Umsetzung | +|---|---| +| Anmeldung und Abmeldung | Angular Material + Token-API | +| Registrierung und E-Mail-Bestätigung | Angular-Seiten, API und Devise-Mailer | +| Bestätigungslink erneut senden | Angular-Seite und API | +| Passwort vergessen/zurücksetzen | Angular-Seiten, API und E-Mail-Link | +| Profil, E-Mail und Passwort ändern | Einstellungen | +| Eigene Einträge erstellen, bearbeiten und löschen | Eintragsliste und Material-Formular | +| Arbeitszeit aus Beginn/Ende und Mittagspause | Eintragsformular | +| Minuteneinträge summieren | Minutenrechner | +| Laufenden Timer starten/stoppen | Eintragsseite | +| CSV-Export | Eintragsseite und Rails-CSV-API | +| Monatsbericht | Berichte | +| Monats- und Wochenkalender | Kalender-Umschalter | +| Ziel- und Wochenstundenmatrizen | Einstellungen | +| Fortschritt, Restzeit, tatsächliche Wochenleistung und Enddatum | Dashboard | +| Fahrt-, Fortbildungs-, Selbsterfahrungs-, Supervisions- und Gesamtkosten | Dashboard | +| Mediation-Präsenztage | Dashboard | +| Kilometersätze pro Jahr | Kilometersätze | +| Datenbank-Dump herunterladen/einspielen | Administration, ZIP und SQL | +| Benutzer und globale Einträge verwalten | Administration | +| Ausbildungsstellen-Watcher, Quellen und Treffer | Administration + GoodJob | +| Täglicher Backupversand und Watcher-Cron | GoodJob-Konfiguration | +| PgHero | `/admin/pghero`, geschützt mit HTTP Basic Auth | +| Impressum | Öffentliche Angular-Seite | + +Administrationsrechte erhält die Adresse aus `ADMIN_EMAIL`. SMTP-, Benachrichtigungs- und PgHero-Zugangsdaten werden ausschließlich über `.env` konfiguriert. diff --git a/README.md b/README.md index 7db80e4..d7fca7a 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,83 @@ -# README +# Ausbildungsnachweis -This README would normally document whatever steps are necessary to get the -application up and running. +Die bisherige Rails-Webanwendung wurde in zwei klar getrennte Anwendungen umgebaut: -Things you may want to cover: +- Rails 7.1 als JSON-API unter `/api/v1` +- Angular 22 mit Angular Material im Ordner `frontend/` +- PostgreSQL, Redis und Nginx über Docker Compose -* Ruby version +## Start mit Docker -* System dependencies +```bash +cp .env.example .env +# CHANGE_ME-Werte in .env durch sichere Zufallswerte ersetzen +docker compose build +docker compose up -d +docker compose exec api bin/rails db:prepare +``` -* Configuration +Unter Windows zuerst die `.env` mit sicheren Zufallswerten erzeugen: -* Database creation +```powershell +powershell -ExecutionPolicy Bypass -File .\setup-env.ps1 +docker compose up -d --build +``` -* Database initialization +Falls Port `13131` bereits verwendet wird, in `.env` beispielsweise Folgendes setzen: -* How to run the test suite +```dotenv +FRONTEND_PORT=13132 +FRONTEND_ORIGINS=http://localhost:13132 +``` -* Services (job queues, cache servers, search engines, etc.) +Frontend: +API: +Healthcheck: -* Deployment instructions +Ein Secret kann mit `docker compose run --rm api bin/rails secret` erzeugt werden. -* ... +## Lokale Entwicklung + +Backend: + +```bash +bundle install +bin/rails db:prepare +bin/rails server +``` + +Frontend: + +```bash +cd frontend +npm install +npm start +``` + +Der Angular-Devserver leitet `/api` über `proxy.conf.json` an Rails auf Port 3000 weiter. + +## API-Bereiche + +- `auth`: Login, Logout und aktueller Benutzer +- `entries`: CRUD, Timer und CSV-Export +- `dashboard`: Fortschritt, Kilometer und Kosten +- `calendar`: Einträge für frei wählbare Datumsbereiche +- `monthly_report`: Monatsauswertung +- `settings`: Sollstunden, Wochenziele und Ausbildungsstatus +- `mileage_rates`: Kilometersätze + +`csv` ist im `Gemfile` ausdrücklich als Gem eingetragen und zusätzlich im Export-Controller sowie im Modell geladen. + +Der vollständige Abgleich mit den Funktionen des ursprünglichen Rails-Projekts steht in `MIGRATION_FEATURES.md`. + +## E-Mail und Administration + +In `.env` müssen insbesondere `SMTP_USERNAME`, `SMTP_PASSWORD`, `FRONTEND_URL`, `ADMIN_EMAIL` und die PgHero-Zugangsdaten gesetzt werden. Nach Änderungen an `.env` die Container neu erstellen: + +```bash +docker compose up -d --build --force-recreate +``` + +## Bestehende Daten + +Die vorhandenen Tabellen und Modelle bleiben erhalten. Beim ersten Start wird nur `api_token_digest` an `users` ergänzt. Vor dem Upgrade sollte wie üblich ein PostgreSQL-Backup erstellt werden. diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb deleted file mode 100644 index a4d5c55..0000000 --- a/app/controllers/admin/application_controller.rb +++ /dev/null @@ -1,25 +0,0 @@ -# All Administrate controllers inherit from this -# `Administrate::ApplicationController`, making it the ideal place to put -# authentication logic or other before_actions. -# -# If you want to add pagination or other controller-level concerns, -# you're free to overwrite the RESTful controller actions. -module Admin - class ApplicationController < Administrate::ApplicationController - before_action :authenticate_admin - - def authenticate_admin - redirect_to root_path, alert: "Kein Zugriff!" unless current_user.email =="christoph@marzell.net" - end - - def is_admin? - current_user&.email =="christoph@marzell.net" - end - - # Override this value to specify the number of elements to display at a time - # on index pages. Defaults to 20. - # def records_per_page - # params[:per_page] || 20 - # end - end -end diff --git a/app/controllers/admin/entries_controller.rb b/app/controllers/admin/entries_controller.rb deleted file mode 100644 index 0418a6c..0000000 --- a/app/controllers/admin/entries_controller.rb +++ /dev/null @@ -1,46 +0,0 @@ -module Admin - class EntriesController < Admin::ApplicationController - # Overwrite any of the RESTful controller actions to implement custom behavior - # For example, you may want to send an email after a foo is updated. - # - # def update - # super - # send_foo_updated_email(requested_resource) - # end - - # Override this method to specify custom lookup behavior. - # This will be used to set the resource for the `show`, `edit`, and `update` - # actions. - # - # def find_resource(param) - # Foo.find_by!(slug: param) - # end - - # The result of this lookup will be available as `requested_resource` - - # Override this if you have certain roles that require a subset - # this will be used to set the records shown on the `index` action. - # - # def scoped_resource - # if current_user.super_admin? - # resource_class - # else - # resource_class.with_less_stuff - # end - # end - - # Override `resource_params` if you want to transform the submitted - # data before it's persisted. For example, the following would turn all - # empty values into nil values. It uses other APIs such as `resource_class` - # and `dashboard`: - # - # def resource_params - # params.require(resource_class.model_name.param_key). - # permit(dashboard.permitted_attributes(action_name)). - # transform_values { |value| value == "" ? nil : value } - # end - - # See https://administrate-demo.herokuapp.com/customizing_controller_actions - # for more information - end -end diff --git a/app/controllers/admin/mileage_rates_controller.rb b/app/controllers/admin/mileage_rates_controller.rb deleted file mode 100644 index 27dbc9f..0000000 --- a/app/controllers/admin/mileage_rates_controller.rb +++ /dev/null @@ -1,46 +0,0 @@ -module Admin - class MileageRatesController < Admin::ApplicationController - # Overwrite any of the RESTful controller actions to implement custom behavior - # For example, you may want to send an email after a foo is updated. - # - # def update - # super - # send_foo_updated_email(requested_resource) - # end - - # Override this method to specify custom lookup behavior. - # This will be used to set the resource for the `show`, `edit`, and `update` - # actions. - # - # def find_resource(param) - # Foo.find_by!(slug: param) - # end - - # The result of this lookup will be available as `requested_resource` - - # Override this if you have certain roles that require a subset - # this will be used to set the records shown on the `index` action. - # - # def scoped_resource - # if current_user.super_admin? - # resource_class - # else - # resource_class.with_less_stuff - # end - # end - - # Override `resource_params` if you want to transform the submitted - # data before it's persisted. For example, the following would turn all - # empty values into nil values. It uses other APIs such as `resource_class` - # and `dashboard`: - # - # def resource_params - # params.require(resource_class.model_name.param_key). - # permit(dashboard.permitted_attributes(action_name)). - # transform_values { |value| value == "" ? nil : value } - # end - - # See https://administrate-demo.herokuapp.com/customizing_controller_actions - # for more information - end -end diff --git a/app/controllers/admin/training_watch_hits_controller.rb b/app/controllers/admin/training_watch_hits_controller.rb deleted file mode 100644 index 1d60d38..0000000 --- a/app/controllers/admin/training_watch_hits_controller.rb +++ /dev/null @@ -1,46 +0,0 @@ -module Admin - class TrainingWatchHitsController < Admin::ApplicationController - # Overwrite any of the RESTful controller actions to implement custom behavior - # For example, you may want to send an email after a foo is updated. - # - # def update - # super - # send_foo_updated_email(requested_resource) - # end - - # Override this method to specify custom lookup behavior. - # This will be used to set the resource for the `show`, `edit`, and `update` - # actions. - # - # def find_resource(param) - # Foo.find_by!(slug: param) - # end - - # The result of this lookup will be available as `requested_resource` - - # Override this if you have certain roles that require a subset - # this will be used to set the records shown on the `index` action. - # - # def scoped_resource - # if current_user.super_admin? - # resource_class - # else - # resource_class.with_less_stuff - # end - # end - - # Override `resource_params` if you want to transform the submitted - # data before it's persisted. For example, the following would turn all - # empty values into nil values. It uses other APIs such as `resource_class` - # and `dashboard`: - # - # def resource_params - # params.require(resource_class.model_name.param_key). - # permit(dashboard.permitted_attributes(action_name)). - # transform_values { |value| value == "" ? nil : value } - # end - - # See https://administrate-demo.herokuapp.com/customizing_controller_actions - # for more information - end -end diff --git a/app/controllers/admin/training_watch_sources_controller.rb b/app/controllers/admin/training_watch_sources_controller.rb deleted file mode 100644 index aa84371..0000000 --- a/app/controllers/admin/training_watch_sources_controller.rb +++ /dev/null @@ -1,46 +0,0 @@ -module Admin - class TrainingWatchSourcesController < Admin::ApplicationController - # Overwrite any of the RESTful controller actions to implement custom behavior - # For example, you may want to send an email after a foo is updated. - # - # def update - # super - # send_foo_updated_email(requested_resource) - # end - - # Override this method to specify custom lookup behavior. - # This will be used to set the resource for the `show`, `edit`, and `update` - # actions. - # - # def find_resource(param) - # Foo.find_by!(slug: param) - # end - - # The result of this lookup will be available as `requested_resource` - - # Override this if you have certain roles that require a subset - # this will be used to set the records shown on the `index` action. - # - # def scoped_resource - # if current_user.super_admin? - # resource_class - # else - # resource_class.with_less_stuff - # end - # end - - # Override `resource_params` if you want to transform the submitted - # data before it's persisted. For example, the following would turn all - # empty values into nil values. It uses other APIs such as `resource_class` - # and `dashboard`: - # - # def resource_params - # params.require(resource_class.model_name.param_key). - # permit(dashboard.permitted_attributes(action_name)). - # transform_values { |value| value == "" ? nil : value } - # end - - # See https://administrate-demo.herokuapp.com/customizing_controller_actions - # for more information - end -end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb deleted file mode 100644 index 2c4ab7d..0000000 --- a/app/controllers/admin/users_controller.rb +++ /dev/null @@ -1,46 +0,0 @@ -module Admin - class UsersController < Admin::ApplicationController - # Overwrite any of the RESTful controller actions to implement custom behavior - # For example, you may want to send an email after a foo is updated. - # - # def update - # super - # send_foo_updated_email(requested_resource) - # end - - # Override this method to specify custom lookup behavior. - # This will be used to set the resource for the `show`, `edit`, and `update` - # actions. - # - # def find_resource(param) - # Foo.find_by!(slug: param) - # end - - # The result of this lookup will be available as `requested_resource` - - # Override this if you have certain roles that require a subset - # this will be used to set the records shown on the `index` action. - # - # def scoped_resource - # if current_user.super_admin? - # resource_class - # else - # resource_class.with_less_stuff - # end - # end - - # Override `resource_params` if you want to transform the submitted - # data before it's persisted. For example, the following would turn all - # empty values into nil values. It uses other APIs such as `resource_class` - # and `dashboard`: - # - # def resource_params - # params.require(resource_class.model_name.param_key). - # permit(dashboard.permitted_attributes(action_name)). - # transform_values { |value| value == "" ? nil : value } - # end - - # See https://administrate-demo.herokuapp.com/customizing_controller_actions - # for more information - end -end diff --git a/app/controllers/api/v1/admin/base_controller.rb b/app/controllers/api/v1/admin/base_controller.rb new file mode 100644 index 0000000..12362f3 --- /dev/null +++ b/app/controllers/api/v1/admin/base_controller.rb @@ -0,0 +1,15 @@ +module Api + module V1 + module Admin + class BaseController < Api::V1::BaseController + before_action :require_admin! + + private + + def require_admin! + render json: { error: "Keine Administratorberechtigung" }, status: :forbidden unless current_user.is_admin? + end + end + end + end +end diff --git a/app/controllers/api/v1/admin/database_controller.rb b/app/controllers/api/v1/admin/database_controller.rb new file mode 100644 index 0000000..b601cd7 --- /dev/null +++ b/app/controllers/api/v1/admin/database_controller.rb @@ -0,0 +1,114 @@ +require "open3" + +module Api + module V1 + module Admin + class DatabaseController < BaseController + MAX_UPLOAD_SIZE = 250.megabytes + + def backup + Dir.mktmpdir("praktikum-backup") do |directory| + sql_path = File.join(directory, "praktikum-#{Time.current.strftime('%Y%m%d-%H%M%S')}.sql") + run_database_command!(pg_dump_command(sql_path)) + zip_path = "#{sql_path}.zip" + stdout, stderr, status = Open3.capture3("zip", "-j", zip_path, sql_path) + raise "ZIP-Erstellung fehlgeschlagen: #{stderr.presence || stdout}" unless status.success? + send_data File.binread(zip_path), filename: File.basename(zip_path), type: "application/zip" + end + rescue StandardError => error + Rails.logger.error("Backup failed: #{error.class}: #{error.message}") + render json: { error: "Datenbanksicherung konnte nicht erstellt werden" }, status: :internal_server_error + end + + def restore + upload = params[:file] + return render json: { error: "Keine Datei ausgewählt" }, status: :bad_request unless upload + return render json: { error: "Datei ist größer als 250 MB" }, status: :payload_too_large if upload.size.to_i > MAX_UPLOAD_SIZE + + Dir.mktmpdir("praktikum-restore") do |directory| + uploaded_path = File.join(directory, File.basename(upload.original_filename.to_s)) + File.binwrite(uploaded_path, upload.read) + sql_path = extract_sql(uploaded_path, directory) + restore_path = build_atomic_restore_file(sql_path, directory) + Rails.logger.info("Atomic database restore v2026.08.16.8 using #{File.basename(restore_path)}") + run_database_command!(psql_command(restore_path)) + # The restored schema invalidates prepared statements and the schema + # cache of already open Rails connections. Reconnect cleanly before + # the next request uses the restored database. + ActiveRecord::Base.connection_handler.clear_all_connections! + run_pending_migrations! + ActiveRecord::Base.connection_handler.clear_all_connections! + end + render json: { message: "Datenbank wurde erfolgreich wiederhergestellt" } + rescue StandardError => error + Rails.logger.error("Restore failed: #{error.class}: #{error.message}") + render json: { error: "Wiederherstellung fehlgeschlagen: #{error.message}" }, status: :unprocessable_content + end + + private + + def extract_sql(uploaded_path, directory) + return uploaded_path if File.extname(uploaded_path).downcase == ".sql" + raise "Erlaubt sind nur .zip- oder .sql-Dateien" unless File.extname(uploaded_path).downcase == ".zip" + list, stderr, status = Open3.capture3("unzip", "-Z1", uploaded_path) + raise "ZIP-Datei ist ungültig: #{stderr}" unless status.success? + entries = list.lines.map(&:strip).reject(&:blank?) + sql_entries = entries.select { |entry| File.extname(entry).downcase == ".sql" && !entry.include?("..") && !entry.start_with?("/") } + raise "ZIP muss genau eine SQL-Datei enthalten" unless sql_entries.one? + sql_path = File.join(directory, "restore.sql") + File.binwrite(sql_path, Open3.capture3("unzip", "-p", uploaded_path, sql_entries.first).then { |out, err, result| raise("SQL konnte nicht entpackt werden: #{err}") unless result.success?; out }) + sql_path + end + + def database_config = ActiveRecord::Base.connection_db_config.configuration_hash + + # Use one physical SQL input so resetting the existing schema always + # happens before the first CREATE TABLE from legacy plain-text dumps. + # psql --single-transaction rolls the reset back as well when any + # statement from the uploaded dump fails. + def build_atomic_restore_file(sql_path, directory) + restore_path = File.join(directory, "atomic_restore.sql") + File.open(restore_path, "wb") do |restore| + restore.write("DROP SCHEMA IF EXISTS public CASCADE;\n") + restore.write("CREATE SCHEMA public;\n") + File.open(sql_path, "rb") { |source| IO.copy_stream(source, restore) } + end + restore_path + end + + def pg_dump_command(path) + config = database_config + ["pg_dump", "-U", config[:username].to_s, "-h", (config[:host] || "localhost").to_s, "-p", (config[:port] || 5432).to_s, "-d", config[:database].to_s, "--clean", "--if-exists", "--no-owner", "--no-privileges", "-f", path] + end + + def psql_command(path) + config = database_config + [ + "psql", + "-v", "ON_ERROR_STOP=1", + "--single-transaction", + "-U", config[:username].to_s, + "-h", (config[:host] || "localhost").to_s, + "-p", (config[:port] || 5432).to_s, + "-d", config[:database].to_s, + "-f", path + ] + end + + def run_database_command!(command) + _stdout, stderr, status = Open3.capture3({ "PGPASSWORD" => database_config[:password].to_s }, *command) + raise stderr.presence || "Datenbankbefehl fehlgeschlagen" unless status.success? + end + + def run_pending_migrations! + stdout, stderr, status = Open3.capture3( + "bundle", "exec", "rails", "db:migrate", + chdir: Rails.root.to_s + ) + Rails.logger.info("Post-restore migrations completed: #{stdout.lines.last.to_s.strip}") if status.success? + raise "Migrationen nach dem Restore fehlgeschlagen: #{stderr.presence || stdout}" unless status.success? + end + end + end + end +end diff --git a/app/controllers/api/v1/admin/entries_controller.rb b/app/controllers/api/v1/admin/entries_controller.rb new file mode 100644 index 0000000..bf76805 --- /dev/null +++ b/app/controllers/api/v1/admin/entries_controller.rb @@ -0,0 +1,24 @@ +module Api + module V1 + module Admin + class EntriesController < BaseController + before_action :set_entry, only: %i[show update destroy] + def index + entries = Entry.includes(:user).order(date: :desc).limit(2_000) + render json: entries.map { |entry| entry.as_json.merge(user_email: entry.user.email) } + end + def show = render json: @entry.as_json.merge(user_email: @entry.user.email) + def update + @entry.update(entry_params) ? render(json: @entry) : render_validation(@entry) + end + def destroy + @entry.destroy! + head :no_content + end + private + def set_entry = @entry = Entry.find(params[:id]) + def entry_params = params.require(:entry).permit(:date, :hours, :minutes, :praktikums_typ, :entry_art, :distance_km, :beschreibung, :kosten, :start_time, :end_time, :lunch_break_minutes, :zaehlt_als_fortbildung) + end + end + end +end diff --git a/app/controllers/api/v1/admin/training_watch_controller.rb b/app/controllers/api/v1/admin/training_watch_controller.rb new file mode 100644 index 0000000..638b42b --- /dev/null +++ b/app/controllers/api/v1/admin/training_watch_controller.rb @@ -0,0 +1,12 @@ +module Api + module V1 + module Admin + class TrainingWatchController < BaseController + def run + results = TrainingWatch::Checker.new.run! + render json: { message: "Prüfung abgeschlossen", new_hits: results.size, results: results } + end + end + end + end +end diff --git a/app/controllers/api/v1/admin/training_watch_hits_controller.rb b/app/controllers/api/v1/admin/training_watch_hits_controller.rb new file mode 100644 index 0000000..5c37369 --- /dev/null +++ b/app/controllers/api/v1/admin/training_watch_hits_controller.rb @@ -0,0 +1,17 @@ +module Api + module V1 + module Admin + class TrainingWatchHitsController < BaseController + before_action :set_hit, only: %i[show destroy] + def index = render json: TrainingWatchHit.includes(:training_watch_source).order(created_at: :desc).limit(1_000).map { |hit| hit.as_json.merge(source_name: hit.training_watch_source.name) } + def show = render json: @hit.as_json.merge(source_name: @hit.training_watch_source.name) + def destroy + @hit.destroy! + head :no_content + end + private + def set_hit = @hit = TrainingWatchHit.find(params[:id]) + end + end + end +end diff --git a/app/controllers/api/v1/admin/training_watch_sources_controller.rb b/app/controllers/api/v1/admin/training_watch_sources_controller.rb new file mode 100644 index 0000000..27369cf --- /dev/null +++ b/app/controllers/api/v1/admin/training_watch_sources_controller.rb @@ -0,0 +1,25 @@ +module Api + module V1 + module Admin + class TrainingWatchSourcesController < BaseController + before_action :set_source, only: %i[show update destroy] + def index = render json: TrainingWatchSource.order(:name) + def show = render json: @source + def create + source = TrainingWatchSource.new(source_params) + source.save ? render(json: source, status: :created) : render_validation(source) + end + def update + @source.update(source_params) ? render(json: @source) : render_validation(@source) + end + def destroy + @source.destroy! + head :no_content + end + private + def set_source = @source = TrainingWatchSource.find(params[:id]) + def source_params = params.require(:training_watch_source).permit(:name, :url, :kind, :match_regex, :enabled) + end + end + end +end diff --git a/app/controllers/api/v1/admin/users_controller.rb b/app/controllers/api/v1/admin/users_controller.rb new file mode 100644 index 0000000..673c7de --- /dev/null +++ b/app/controllers/api/v1/admin/users_controller.rb @@ -0,0 +1,40 @@ +module Api + module V1 + module Admin + class UsersController < BaseController + before_action :set_user, only: %i[show update destroy] + def index = render json: User.order(:email).map { |user| payload(user) } + def show = render json: payload(@user) + def create + user = User.new(email: params.dig(:user, :email), password: params.dig(:user, :password), password_confirmation: params.dig(:user, :password)) + user.skip_confirmation! if ActiveModel::Type::Boolean.new.cast(params.dig(:user, :confirmed)) + user.save ? render(json: payload(user), status: :created) : render_validation(user) + end + def update + attributes = params.require(:user).permit(:email, :praepedeutikum_done, :confirmed, :password) + password = attributes.delete(:password) + confirmed = attributes.delete(:confirmed) + @user.assign_attributes(attributes) + @user.password = password if password.present? + @user.password_confirmation = password if password.present? + unless confirmed.nil? + if confirmed + @user.confirm + else + @user.confirmed_at = nil + end + end + @user.save ? render(json: payload(@user)) : render_validation(@user) + end + def destroy + return render json: { error: "Das eigene Administratorkonto kann nicht gelöscht werden" }, status: :unprocessable_entity if @user == current_user + @user.destroy! + head :no_content + end + private + def set_user = @user = User.find(params[:id]) + def payload(user) = user.as_json(only: %i[id email confirmed_at praepedeutikum_done created_at updated_at]).merge(is_admin: user.is_admin?, entries_count: user.entries.count) + end + end + end +end diff --git a/app/controllers/api/v1/auth_controller.rb b/app/controllers/api/v1/auth_controller.rb new file mode 100644 index 0000000..dc51b14 --- /dev/null +++ b/app/controllers/api/v1/auth_controller.rb @@ -0,0 +1,88 @@ +module Api + module V1 + class AuthController < BaseController + skip_before_action :authenticate_api_user!, only: %i[login register confirm resend_confirmation forgot_password reset_password] + + def login + user = User.find_for_database_authentication(email: params[:email].to_s.downcase.strip) + return render json: { error: "E-Mail oder Passwort ist falsch" }, status: :unauthorized unless user&.valid_password?(params[:password]) + return render json: { error: "E-Mail-Adresse wurde noch nicht bestätigt" }, status: :forbidden unless user.confirmed? + + token = SecureRandom.hex(32) + user.update!(api_token_digest: Digest::SHA256.hexdigest(token)) + render json: { token:, user: user_payload(user) } + end + + def register + user = User.new(email: params[:email], password: params[:password], password_confirmation: params[:password_confirmation]) + if user.save + render json: { message: "Registrierung erfolgreich. Bitte E-Mail bestätigen." }, status: :created + else + render_validation(user) + end + end + + def confirm + user = User.confirm_by_token(params[:token].to_s) + if user.errors.empty? + render json: { message: "E-Mail-Adresse wurde bestätigt. Du kannst dich jetzt anmelden." } + else + render json: { errors: user.errors.full_messages }, status: :unprocessable_entity + end + end + + def resend_confirmation + user = User.find_by(email: params[:email].to_s.downcase.strip) + user&.send_confirmation_instructions unless user&.confirmed? + render json: { message: "Falls ein unbestätigtes Konto existiert, wurde ein neuer Bestätigungslink versendet." } + end + + def forgot_password + user = User.find_by(email: params[:email].to_s.downcase.strip) + user&.send_reset_password_instructions + render json: { message: "Falls ein Konto existiert, wurde eine E-Mail zum Zurücksetzen versendet." } + end + + def reset_password + user = User.reset_password_by_token( + reset_password_token: params[:token], + password: params[:password], + password_confirmation: params[:password_confirmation] + ) + if user.errors.empty? + user.update!(api_token_digest: nil) + render json: { message: "Passwort wurde geändert. Du kannst dich jetzt anmelden." } + else + render json: { errors: user.errors.full_messages }, status: :unprocessable_entity + end + end + + def change_password + unless current_user.valid_password?(params[:current_password]) + return render json: { error: "Das aktuelle Passwort ist falsch" }, status: :unprocessable_entity + end + if current_user.update(password: params[:password], password_confirmation: params[:password_confirmation]) + current_user.update!(api_token_digest: nil) + render json: { message: "Passwort wurde geändert. Bitte melde dich erneut an." } + else + render_validation(current_user) + end + end + + def logout + current_user.update!(api_token_digest: nil) + head :no_content + end + + def me + render json: user_payload(current_user) + end + + private + + def user_payload(user) + { id: user.id, email: user.email, praepedeutikum_done: user.praepedeutikum_done, is_admin: user.is_admin? } + end + end + end +end diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb new file mode 100644 index 0000000..f4717df --- /dev/null +++ b/app/controllers/api/v1/base_controller.rb @@ -0,0 +1,33 @@ +module Api + module V1 + class BaseController < ApplicationController + before_action :authenticate_api_user! + after_action :expose_build_version + + rescue_from ActiveRecord::RecordNotFound, with: :not_found + + private + + attr_reader :current_user + + def authenticate_api_user! + token = request.authorization.to_s.delete_prefix("Bearer ").strip + digest = Digest::SHA256.hexdigest(token) if token.present? + @current_user = User.find_by(api_token_digest: digest) + render json: { error: "Nicht angemeldet" }, status: :unauthorized unless @current_user + end + + def not_found + render json: { error: "Nicht gefunden" }, status: :not_found + end + + def render_validation(record) + render json: { errors: record.errors.full_messages }, status: :unprocessable_content + end + + def expose_build_version + response.set_header("X-Praktikum-Build", Rails.application.config.x.build_version) + end + end + end +end diff --git a/app/controllers/api/v1/calendar_controller.rb b/app/controllers/api/v1/calendar_controller.rb new file mode 100644 index 0000000..498d0db --- /dev/null +++ b/app/controllers/api/v1/calendar_controller.rb @@ -0,0 +1,16 @@ +module Api + module V1 + class CalendarController < BaseController + def index + from = Date.iso8601(params.fetch(:from, Date.current.beginning_of_month.to_s)) + to = Date.iso8601(params.fetch(:to, Date.current.end_of_month.to_s)) + scope = current_user.entries.where(date: from..to).order(:date, :start_time) + scope = scope.where(praktikums_typ: params[:typ]) if params[:typ].present? + scope = scope.where(entry_art: params[:art]) if params[:art].present? + render json: scope.as_json(only: %i[id date hours minutes praktikums_typ entry_art beschreibung start_time end_time]) + rescue Date::Error + render json: { error: "Ungültiger Datumsbereich" }, status: :bad_request + end + end + end +end diff --git a/app/controllers/api/v1/dashboard_controller.rb b/app/controllers/api/v1/dashboard_controller.rb new file mode 100644 index 0000000..2feaeaf --- /dev/null +++ b/app/controllers/api/v1/dashboard_controller.rb @@ -0,0 +1,52 @@ +module Api + module V1 + class DashboardController < BaseController + def show + current_user.update_required_matrices! + entries = current_user.entries.where("date <= ?", Date.current) + progress = User::PRAKTIKUMSTYPEN.flat_map do |typ| + User.entry_arten_for(typ).map do |art| + spent = entries.where(praktikums_typ: typ, entry_art: art).sum("COALESCE(hours, 0) * 60 + COALESCE(minutes, 0)").to_i + target = current_user.required_hours_for(typ, art).to_f * 60 + remaining = [target - spent, 0].max.to_i + weekly_target = current_user.weekly_target_for(typ, art).to_f + dates = entries.where(praktikums_typ: typ, entry_art: art).where("COALESCE(hours, 0) > 0 OR COALESCE(minutes, 0) > 0") + first_date = dates.minimum(:date) + last_date = dates.maximum(:date) + period_end = spent >= target && last_date ? last_date : Date.current + weeks = first_date ? [[(period_end - first_date).to_i + 1, 7].max / 7.0, 1].max : nil + actual_weekly = weeks ? ((spent / 60.0) / weeks).round(2) : nil + estimated_end = weekly_target.positive? && remaining.positive? ? Date.current + (remaining / 60.0 / weekly_target).ceil.weeks : nil + { typ:, art:, spent_minutes: spent, target_minutes: target.to_i, remaining_minutes: remaining, + percent: target.positive? ? [(spent / target * 100).round(1), 100].min : 0, + weekly_target:, actual_weekly:, estimated_end: } + end + end + total_spent = progress.sum { |row| row[:spent_minutes] } + total_target = progress.sum { |row| row[:target_minutes] } + render json: { total_minutes: total_spent, target_minutes: total_target, remaining_minutes: [total_target - total_spent, 0].max, + completed_percent: total_target.positive? ? (total_spent.to_f / total_target * 100).round(1) : 0, + total_distance_km: current_user.entries.sum(:distance_km), costs_by_year: costs_by_year, + running_entry: current_user.entries.find_by(end_time: nil, beschreibung: "Timer"), + mediation_presence_days: current_user.mediation_praesenzmodule_completed, + mediation_presence_days_required: current_user.mediation_praesenzmodule_required, + last_entry: current_user.entries.where("date <= ?", Date.current).order(date: :desc).first, progress: } + end + + private + + def costs_by_year + years = current_user.entries.pluck(Arel.sql("DISTINCT EXTRACT(YEAR FROM date)::int")).compact.sort.reverse + km = Entry.total_kilometer_cost_by_year(current_user) + total = Entry.total_gesamtkosten_by_year(current_user) + training = Entry.total_fortbildungskosten_by_year(current_user) + experience = Entry.total_selbsterfahrungskosten_by_year(current_user) + supervision = Entry.total_supervision_by_year(current_user) + semester = Entry.total_semesterkosten_by_year(current_user) + years.map { |year| { year:, kilometer: km[year].to_f, fortbildung: training[year].to_f, + selbsterfahrung: experience[year].to_f, supervision: supervision[year].to_f, + semester: semester[year].to_f, total: total[year].to_f } } + end + end + end +end diff --git a/app/controllers/api/v1/entries_controller.rb b/app/controllers/api/v1/entries_controller.rb new file mode 100644 index 0000000..a811c94 --- /dev/null +++ b/app/controllers/api/v1/entries_controller.rb @@ -0,0 +1,77 @@ +require "csv" + +module Api + module V1 + class EntriesController < BaseController + before_action :set_entry, only: %i[show update destroy stop_timer] + + def index + scope = current_user.entries.order(date: :desc, created_at: :desc) + scope = scope.where(praktikums_typ: params[:typ]) if params[:typ].present? + scope = scope.where(entry_art: params[:art]) if params[:art].present? + scope = scope.where("beschreibung ILIKE ?", "%#{ActiveRecord::Base.sanitize_sql_like(params[:search])}%") if params[:search].present? + render json: scope.map { |entry| payload(entry) } + end + + def show + render json: payload(@entry) + end + + def create + entry = current_user.entries.new(entry_params) + return render json: { error: "Das Propädeutikum ist bereits abgeschlossen" }, status: :unprocessable_entity if blocked?(entry) + entry.save ? render(json: payload(entry), status: :created) : render_validation(entry) + end + + def update + return render json: { error: "Das Propädeutikum ist bereits abgeschlossen" }, status: :unprocessable_entity if params.dig(:entry, :praktikums_typ) == "propädeutikum" && current_user.praepedeutikum_abgeschlossen? + @entry.update(entry_params) ? render(json: payload(@entry)) : render_validation(@entry) + end + + def destroy + @entry.destroy! + head :no_content + end + + def start_timer + return render json: { error: "Es läuft bereits ein Timer" }, status: :unprocessable_entity if current_user.entries.exists?(end_time: nil, beschreibung: "Timer") + entry = current_user.entries.new(date: Date.current, start_time: Time.current, beschreibung: "Timer", praktikums_typ: params[:typ], entry_art: params[:art]) + return render json: { error: "Das Propädeutikum ist bereits abgeschlossen" }, status: :unprocessable_entity if blocked?(entry) + entry.save ? render(json: payload(entry), status: :created) : render_validation(entry) + end + + def stop_timer + return render json: { error: "Dieser Eintrag besitzt keine Startzeit" }, status: :unprocessable_entity unless @entry.start_time + @entry.end_time = Time.current + @entry.lunch_break_minutes = ActiveModel::Type::Boolean.new.cast(params[:lunch_break]) ? 30 : 0 + minutes = @entry.total_minutes_including_break.to_i + @entry.hours, @entry.minutes = minutes.divmod(60) + @entry.save! + render json: payload(@entry) + end + + def export_csv + data = current_user.entries.order(date: :desc).to_csv + send_data data, filename: "eintraege-#{Date.current}.csv", type: "text/csv; charset=utf-8" + end + + private + + def set_entry + @entry = current_user.entries.find(params[:id]) + end + + def entry_params + params.require(:entry).permit(:date, :hours, :minutes, :praktikums_typ, :entry_art, :distance_km, :beschreibung, :kosten, :start_time, :end_time, :lunch_break_minutes, :zaehlt_als_fortbildung) + end + + def blocked?(entry) + current_user.praepedeutikum_abgeschlossen? && entry.praktikums_typ == "propädeutikum" + end + + def payload(entry) + entry.as_json(only: %i[id date hours minutes praktikums_typ entry_art distance_km beschreibung kosten start_time end_time lunch_break_minutes zaehlt_als_fortbildung created_at updated_at]).merge(kilometer_pauschale: entry.kilometer_pauschale) + end + end + end +end diff --git a/app/controllers/api/v1/mileage_rates_controller.rb b/app/controllers/api/v1/mileage_rates_controller.rb new file mode 100644 index 0000000..633b256 --- /dev/null +++ b/app/controllers/api/v1/mileage_rates_controller.rb @@ -0,0 +1,19 @@ +module Api + module V1 + class MileageRatesController < BaseController + before_action :set_rate, only: %i[show update] + def index = render json: MileageRate.order(year: :desc) + def show = render json: @rate + def create + rate = MileageRate.new(rate_params) + rate.save ? render(json: rate, status: :created) : render_validation(rate) + end + def update + @rate.update(rate_params) ? render(json: @rate) : render_validation(@rate) + end + private + def set_rate = @rate = MileageRate.find(params[:id]) + def rate_params = params.require(:mileage_rate).permit(:year, :rate_per_km) + end + end +end diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb new file mode 100644 index 0000000..fa05ff4 --- /dev/null +++ b/app/controllers/api/v1/reports_controller.rb @@ -0,0 +1,12 @@ +module Api + module V1 + class ReportsController < BaseController + def monthly + rows = current_user.entries.group(Arel.sql("DATE_TRUNC('month', date)"), :praktikums_typ, :entry_art) + .order(Arel.sql("DATE_TRUNC('month', date) DESC")) + .pluck(Arel.sql("DATE_TRUNC('month', date)"), :praktikums_typ, :entry_art, Arel.sql("SUM(COALESCE(hours, 0) * 60 + COALESCE(minutes, 0))")) + render json: rows.map { |month, typ, art, minutes| { month: month.to_date, typ:, art:, total_minutes: minutes.to_i } } + end + end + end +end diff --git a/app/controllers/api/v1/settings_controller.rb b/app/controllers/api/v1/settings_controller.rb new file mode 100644 index 0000000..c9594ba --- /dev/null +++ b/app/controllers/api/v1/settings_controller.rb @@ -0,0 +1,19 @@ +module Api + module V1 + class SettingsController < BaseController + def show + current_user.update_required_matrices! + render json: payload + end + def update + attributes = params.require(:settings).permit(:email, :total_required_hours, :weekly_target_hours, :praepedeutikum_done, required_hours_matrix: {}, weekly_target_matrix: {}) + current_user.update(attributes) ? render(json: payload) : render_validation(current_user) + end + private + def payload + current_user.as_json(only: %i[email total_required_hours weekly_target_hours required_hours_matrix weekly_target_matrix praepedeutikum_done]).merge( + praktikums_typen: User::PRAKTIKUMSTYPEN, entry_arten: User::ENTRY_ARTEN, entry_arten_by_typ: User::ENTRY_ARTEN_BY_TYP) + end + end + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 083c599..4ac8823 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,14 +1,2 @@ -class ApplicationController < ActionController::Base - before_action :configure_permitted_parameters, if: :devise_controller? - - def authenticate_admin - redirect_to root_path, alert: "Kein Zugriff!" unless current_user.email =="christoph@marzell.net" - end - def configure_permitted_parameters - devise_parameter_sanitizer.permit(:account_update, keys: [:total_required_hours, :weekly_target_hours, weekly_target_matrix: {}, required_hours_matrix: {}]) - end - def is_admin? - current_user&.email =="christoph@marzell.net" - end - +class ApplicationController < ActionController::API end diff --git a/app/dashboards/entry_dashboard.rb b/app/dashboards/entry_dashboard.rb deleted file mode 100644 index 99c7173..0000000 --- a/app/dashboards/entry_dashboard.rb +++ /dev/null @@ -1,81 +0,0 @@ -require "administrate/base_dashboard" - -class EntryDashboard < Administrate::BaseDashboard - # ATTRIBUTE_TYPES - # a hash that describes the type of each of the model's fields. - # - # Each different type represents an Administrate::Field object, - # which determines how the attribute is displayed - # on pages throughout the dashboard. - ATTRIBUTE_TYPES = { - id: Field::Number, - date: Field::Date, - distance_km: Field::Number, - entry_art: Field::String, - hours: Field::Number, - minutes: Field::Number, - praktikums_typ: Field::String, - user: Field::BelongsTo, - created_at: Field::DateTime, - updated_at: Field::DateTime, - }.freeze - - # COLLECTION_ATTRIBUTES - # an array of attributes that will be displayed on the model's index page. - # - # By default, it's limited to four items to reduce clutter on index pages. - # Feel free to add, remove, or rearrange items. - COLLECTION_ATTRIBUTES = %i[ - id - date - distance_km - entry_art - ].freeze - - # SHOW_PAGE_ATTRIBUTES - # an array of attributes that will be displayed on the model's show page. - SHOW_PAGE_ATTRIBUTES = %i[ - id - date - distance_km - entry_art - hours - minutes - praktikums_typ - user - created_at - updated_at - ].freeze - - # FORM_ATTRIBUTES - # an array of attributes that will be displayed - # on the model's form (`new` and `edit`) pages. - FORM_ATTRIBUTES = %i[ - date - distance_km - entry_art - hours - minutes - praktikums_typ - user - ].freeze - - # COLLECTION_FILTERS - # a hash that defines filters that can be used while searching via the search - # field of the dashboard. - # - # For example to add an option to search for open resources by typing "open:" - # in the search field: - # - # COLLECTION_FILTERS = { - # open: ->(resources) { resources.where(open: true) } - # }.freeze - COLLECTION_FILTERS = {}.freeze - - # Overwrite this method to customize how entries are displayed - # across all pages of the admin dashboard. - # - # def display_resource(entry) - # "Entry ##{entry.id}" - # end -end diff --git a/app/dashboards/mileage_rate_dashboard.rb b/app/dashboards/mileage_rate_dashboard.rb deleted file mode 100644 index 30594cf..0000000 --- a/app/dashboards/mileage_rate_dashboard.rb +++ /dev/null @@ -1,66 +0,0 @@ -require "administrate/base_dashboard" - -class MileageRateDashboard < Administrate::BaseDashboard - # ATTRIBUTE_TYPES - # a hash that describes the type of each of the model's fields. - # - # Each different type represents an Administrate::Field object, - # which determines how the attribute is displayed - # on pages throughout the dashboard. - ATTRIBUTE_TYPES = { - id: Field::Number, - rate_per_km: Field::String.with_options(searchable: false), - year: Field::Number, - created_at: Field::DateTime, - updated_at: Field::DateTime, - }.freeze - - # COLLECTION_ATTRIBUTES - # an array of attributes that will be displayed on the model's index page. - # - # By default, it's limited to four items to reduce clutter on index pages. - # Feel free to add, remove, or rearrange items. - COLLECTION_ATTRIBUTES = %i[ - id - rate_per_km - year - created_at - ].freeze - - # SHOW_PAGE_ATTRIBUTES - # an array of attributes that will be displayed on the model's show page. - SHOW_PAGE_ATTRIBUTES = %i[ - id - rate_per_km - year - created_at - updated_at - ].freeze - - # FORM_ATTRIBUTES - # an array of attributes that will be displayed - # on the model's form (`new` and `edit`) pages. - FORM_ATTRIBUTES = %i[ - rate_per_km - year - ].freeze - - # COLLECTION_FILTERS - # a hash that defines filters that can be used while searching via the search - # field of the dashboard. - # - # For example to add an option to search for open resources by typing "open:" - # in the search field: - # - # COLLECTION_FILTERS = { - # open: ->(resources) { resources.where(open: true) } - # }.freeze - COLLECTION_FILTERS = {}.freeze - - # Overwrite this method to customize how mileage rates are displayed - # across all pages of the admin dashboard. - # - # def display_resource(mileage_rate) - # "MileageRate ##{mileage_rate.id}" - # end -end diff --git a/app/dashboards/training_watch_hit_dashboard.rb b/app/dashboards/training_watch_hit_dashboard.rb deleted file mode 100644 index c034e7a..0000000 --- a/app/dashboards/training_watch_hit_dashboard.rb +++ /dev/null @@ -1,78 +0,0 @@ -require "administrate/base_dashboard" - -class TrainingWatchHitDashboard < Administrate::BaseDashboard - # ATTRIBUTE_TYPES - # a hash that describes the type of each of the model's fields. - # - # Each different type represents an Administrate::Field object, - # which determines how the attribute is displayed - # on pages throughout the dashboard. - ATTRIBUTE_TYPES = { - id: Field::Number, - fingerprint: Field::String, - hit_url: Field::String, - published_at: Field::DateTime, - snippet: Field::Text, - title: Field::String, - training_watch_source: Field::BelongsTo, - created_at: Field::DateTime, - updated_at: Field::DateTime, - }.freeze - - # COLLECTION_ATTRIBUTES - # an array of attributes that will be displayed on the model's index page. - # - # By default, it's limited to four items to reduce clutter on index pages. - # Feel free to add, remove, or rearrange items. - COLLECTION_ATTRIBUTES = %i[ - id - fingerprint - hit_url - published_at - ].freeze - - # SHOW_PAGE_ATTRIBUTES - # an array of attributes that will be displayed on the model's show page. - SHOW_PAGE_ATTRIBUTES = %i[ - id - fingerprint - hit_url - published_at - snippet - title - training_watch_source - created_at - updated_at - ].freeze - - # FORM_ATTRIBUTES - # an array of attributes that will be displayed - # on the model's form (`new` and `edit`) pages. - FORM_ATTRIBUTES = %i[ - fingerprint - hit_url - published_at - snippet - title - training_watch_source - ].freeze - - # COLLECTION_FILTERS - # a hash that defines filters that can be used while searching via the search - # field of the dashboard. - # - # For example to add an option to search for open resources by typing "open:" - # in the search field: - # - # COLLECTION_FILTERS = { - # open: ->(resources) { resources.where(open: true) } - # }.freeze - COLLECTION_FILTERS = {}.freeze - - # Overwrite this method to customize how training watch hits are displayed - # across all pages of the admin dashboard. - # - # def display_resource(training_watch_hit) - # "TrainingWatchHit ##{training_watch_hit.id}" - # end -end diff --git a/app/dashboards/training_watch_source_dashboard.rb b/app/dashboards/training_watch_source_dashboard.rb deleted file mode 100644 index 5d82570..0000000 --- a/app/dashboards/training_watch_source_dashboard.rb +++ /dev/null @@ -1,87 +0,0 @@ -require "administrate/base_dashboard" - -class TrainingWatchSourceDashboard < Administrate::BaseDashboard - # ATTRIBUTE_TYPES - # a hash that describes the type of each of the model's fields. - # - # Each different type represents an Administrate::Field object, - # which determines how the attribute is displayed - # on pages throughout the dashboard. - ATTRIBUTE_TYPES = { - id: Field::Number, - enabled: Field::Boolean, - kind: Field::String, - last_checked_at: Field::DateTime, - last_etag: Field::String, - last_modified: Field::String, - match_regex: Field::String, - name: Field::String, - training_watch_hits: Field::HasMany, - url: Field::String, - created_at: Field::DateTime, - updated_at: Field::DateTime, - }.freeze - - # COLLECTION_ATTRIBUTES - # an array of attributes that will be displayed on the model's index page. - # - # By default, it's limited to four items to reduce clutter on index pages. - # Feel free to add, remove, or rearrange items. - COLLECTION_ATTRIBUTES = %i[ - id - enabled - kind - last_checked_at - ].freeze - - # SHOW_PAGE_ATTRIBUTES - # an array of attributes that will be displayed on the model's show page. - SHOW_PAGE_ATTRIBUTES = %i[ - id - enabled - kind - last_checked_at - last_etag - last_modified - match_regex - name - training_watch_hits - url - created_at - updated_at - ].freeze - - # FORM_ATTRIBUTES - # an array of attributes that will be displayed - # on the model's form (`new` and `edit`) pages. - FORM_ATTRIBUTES = %i[ - enabled - kind - last_checked_at - last_etag - last_modified - match_regex - name - training_watch_hits - url - ].freeze - - # COLLECTION_FILTERS - # a hash that defines filters that can be used while searching via the search - # field of the dashboard. - # - # For example to add an option to search for open resources by typing "open:" - # in the search field: - # - # COLLECTION_FILTERS = { - # open: ->(resources) { resources.where(open: true) } - # }.freeze - COLLECTION_FILTERS = {}.freeze - - # Overwrite this method to customize how training watch sources are displayed - # across all pages of the admin dashboard. - # - # def display_resource(training_watch_source) - # "TrainingWatchSource ##{training_watch_source.id}" - # end -end diff --git a/app/dashboards/user_dashboard.rb b/app/dashboards/user_dashboard.rb deleted file mode 100644 index 6a138f1..0000000 --- a/app/dashboards/user_dashboard.rb +++ /dev/null @@ -1,90 +0,0 @@ -require "administrate/base_dashboard" - -class UserDashboard < Administrate::BaseDashboard - # ATTRIBUTE_TYPES - # a hash that describes the type of each of the model's fields. - # - # Each different type represents an Administrate::Field object, - # which determines how the attribute is displayed - # on pages throughout the dashboard. - ATTRIBUTE_TYPES = { - id: Field::Number, - email: Field::String, - encrypted_password: Field::String, - entries: Field::HasMany, - remember_created_at: Field::DateTime, - required_hours_matrix: Field::String.with_options(searchable: false), - reset_password_sent_at: Field::DateTime, - reset_password_token: Field::String, - total_required_hours: Field::Number, - weekly_target_hours: Field::Number, - weekly_target_matrix: Field::String.with_options(searchable: false), - created_at: Field::DateTime, - updated_at: Field::DateTime, - }.freeze - - # COLLECTION_ATTRIBUTES - # an array of attributes that will be displayed on the model's index page. - # - # By default, it's limited to four items to reduce clutter on index pages. - # Feel free to add, remove, or rearrange items. - COLLECTION_ATTRIBUTES = %i[ - id - email - encrypted_password - entries - ].freeze - - # SHOW_PAGE_ATTRIBUTES - # an array of attributes that will be displayed on the model's show page. - SHOW_PAGE_ATTRIBUTES = %i[ - id - email - encrypted_password - entries - remember_created_at - required_hours_matrix - reset_password_sent_at - reset_password_token - total_required_hours - weekly_target_hours - weekly_target_matrix - created_at - updated_at - ].freeze - - # FORM_ATTRIBUTES - # an array of attributes that will be displayed - # on the model's form (`new` and `edit`) pages. - FORM_ATTRIBUTES = %i[ - email - encrypted_password - entries - remember_created_at - required_hours_matrix - reset_password_sent_at - reset_password_token - total_required_hours - weekly_target_hours - weekly_target_matrix - ].freeze - - # COLLECTION_FILTERS - # a hash that defines filters that can be used while searching via the search - # field of the dashboard. - # - # For example to add an option to search for open resources by typing "open:" - # in the search field: - # - # COLLECTION_FILTERS = { - # open: ->(resources) { resources.where(open: true) } - # }.freeze - COLLECTION_FILTERS = {}.freeze - - # Overwrite this method to customize how users are displayed - # across all pages of the admin dashboard. - # - # def display_resource(user) - # "User ##{user.id}" - # end -end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 3c34c81..146dd06 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,4 +1,4 @@ class ApplicationMailer < ActionMailer::Base - default from: "from@example.com" + default from: -> { ENV.fetch("MAILER_FROM", "praktikum@marzell.net") } layout "mailer" end diff --git a/app/mailers/dump_mailer.rb b/app/mailers/dump_mailer.rb index 3c8477a..f7cb8e3 100644 --- a/app/mailers/dump_mailer.rb +++ b/app/mailers/dump_mailer.rb @@ -4,10 +4,10 @@ class DumpMailer < ApplicationMailer def daily_dump(zip_path) attachments[File.basename(zip_path)] = File.read(zip_path) mail( - to: "christoph@marzell.net", + to: ENV.fetch("BACKUP_NOTIFY_EMAIL", ENV.fetch("ADMIN_EMAIL", "christoph@marzell.net")), subject: "📦 Täglicher SQL Dump" ) do |format| format.text { render plain: "Dump im Anhang." } end end -end \ No newline at end of file +end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb new file mode 100644 index 0000000..d0f53f8 --- /dev/null +++ b/app/mailers/user_mailer.rb @@ -0,0 +1,38 @@ +require "cgi" + +# Devise calls these methods with (record, token, options). Using a regular +# ActionMailer class keeps the API independent from Devise's controller/route +# mapping, which is not present in an API-only application. +class UserMailer < ApplicationMailer + def confirmation_instructions(record, token, opts = {}) + @resource = record + @token = token + @confirmation_url = "#{frontend_url}/confirm-email?token=#{CGI.escape(token)}" + + mail( + to: record.email, + subject: opts[:subject].presence || "E-Mail-Adresse bestätigen", + template_path: "devise/mailer", + template_name: "confirmation_instructions" + ) + end + + def reset_password_instructions(record, token, opts = {}) + @resource = record + @token = token + @reset_password_url = "#{frontend_url}/reset-password?token=#{CGI.escape(token)}" + + mail( + to: record.email, + subject: opts[:subject].presence || "Passwort zurücksetzen", + template_path: "devise/mailer", + template_name: "reset_password_instructions" + ) + end + + private + + def frontend_url + ENV.fetch("FRONTEND_URL", "http://localhost:13131").delete_suffix("/") + end +end diff --git a/app/models/user.rb b/app/models/user.rb index f146edd..00a63f8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -106,7 +106,7 @@ class User < ApplicationRecord end def is_admin? - email == "christoph@marzell.net" + email.casecmp?(ENV.fetch("ADMIN_EMAIL", "christoph@marzell.net")) end def self.entry_arten_for(typ) @@ -410,4 +410,4 @@ class User < ApplicationRecord end end end -end \ No newline at end of file +end diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb new file mode 100644 index 0000000..71a0773 --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,4 @@ +

Willkommen beim Ausbildungsnachweis!

+

Bitte bestätige deine E-Mail-Adresse über den folgenden Link:

+

E-Mail-Adresse bestätigen

+

Falls du dich nicht registriert hast, kannst du diese Nachricht ignorieren.

diff --git a/app/views/devise/mailer/confirmation_instructions.text.erb b/app/views/devise/mailer/confirmation_instructions.text.erb new file mode 100644 index 0000000..1fb5a15 --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.text.erb @@ -0,0 +1,6 @@ +Willkommen beim Ausbildungsnachweis! + +Bitte bestätige deine E-Mail-Adresse: +<%= @confirmation_url %> + +Falls du dich nicht registriert hast, kannst du diese Nachricht ignorieren. diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb new file mode 100644 index 0000000..cafcab2 --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -0,0 +1,3 @@ +

Für dein Konto wurde das Zurücksetzen des Passworts angefordert.

+

Neues Passwort festlegen

+

Falls du das nicht angefordert hast, kannst du diese Nachricht ignorieren.

diff --git a/app/views/devise/mailer/reset_password_instructions.text.erb b/app/views/devise/mailer/reset_password_instructions.text.erb new file mode 100644 index 0000000..3cc51e8 --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.text.erb @@ -0,0 +1,6 @@ +Für dein Konto wurde das Zurücksetzen des Passworts angefordert. + +Neues Passwort festlegen: +<%= @reset_password_url %> + +Falls du das nicht angefordert hast, kannst du diese Nachricht ignorieren. diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint index e16dbe2..f8490fc 100644 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -15,7 +15,7 @@ bundle exec rails db:create RAILS_ENV=${RAILS_ENV} || true echo =========================================== echo ${RAILS_ENV} echo =========================================== -bundle exec rails db:migrate RAILS_ENV=${RAILS_ENV} || true +bundle exec rails db:migrate RAILS_ENV=${RAILS_ENV} echo =========================================== echo ${RAILS_ENV} echo =========================================== diff --git a/config/application.rb b/config/application.rb index e711ad3..337d8fe 100644 --- a/config/application.rb +++ b/config/application.rb @@ -22,6 +22,7 @@ module Praktikum class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 7.1 + config.api_only = true config.time_zone = 'Europe/Vienna' config.active_record.default_timezone = :utc # Please, add to the `ignore` list any other `lib` subdirectories that do diff --git a/config/environments/development.rb b/config/environments/development.rb index 0e45e80..15a33a7 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -43,21 +43,17 @@ Rails.application.configure do # Don't care if the mailer can't send. config.action_mailer.perform_caching = false - config.action_mailer.default_url_options = { :host => "localhost:3000", :protocol => 'http', from: 'praktikum@marzell.net' } - + config.action_mailer.default_url_options = { host: ENV.fetch("APP_HOST", "localhost:4200"), protocol: ENV.fetch("APP_PROTOCOL", "http") } config.action_mailer.delivery_method = :smtp - # SMTP settings for gmail - - config.action_mailer.perform_deliveries = true - config.action_mailer.smtp_settings = { - :address => 'smtp.ionos.de', - :port => 587, - domain: "marzell.net", - :user_name => 'praktikum@marzell.net', - :password => 'CSSABXGRhSzUFK9', - authentication: 'plain', - enable_starttls_auto: true - + config.action_mailer.perform_deliveries = ENV.fetch("SMTP_ENABLED", "false") == "true" + config.action_mailer.smtp_settings = { + address: ENV.fetch("SMTP_ADDRESS", "smtp.ionos.de"), + port: ENV.fetch("SMTP_PORT", "587").to_i, + domain: ENV.fetch("SMTP_DOMAIN", "marzell.net"), + user_name: ENV["SMTP_USERNAME"], + password: ENV["SMTP_PASSWORD"], + authentication: ENV.fetch("SMTP_AUTHENTICATION", "plain"), + enable_starttls_auto: ENV.fetch("SMTP_STARTTLS", "true") == "true" } # Print deprecation notices to the Rails logger. @@ -79,10 +75,8 @@ Rails.application.configure do config.active_job.verbose_enqueue_logs = true # Suppress logger output for asset requests. - config.assets.quiet = true - config.assets.digest = true # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true diff --git a/config/environments/production.rb b/config/environments/production.rb index 415df9d..d0196c5 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -27,7 +27,6 @@ Rails.application.configure do # config.assets.css_compressor = :sass # Do not fall back to assets pipeline if a precompiled asset is missed. - config.assets.compile = false # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.asset_host = "http://assets.example.com" @@ -99,20 +98,21 @@ Rails.application.configure do config.action_mailer.perform_caching = false - config.action_mailer.default_url_options = { :host => "praktikum.marzell.net", :protocol => 'https', from: 'praktikum@marzell.net' } - + config.action_mailer.default_url_options = { + host: ENV.fetch("APP_HOST", "localhost:13131"), + protocol: ENV.fetch("APP_PROTOCOL", "http") + } config.action_mailer.delivery_method = :smtp - # SMTP settings for gmail - - config.action_mailer.perform_deliveries = true - config.action_mailer.smtp_settings = { - :address => 'smtp.ionos.de', - :port => 587, - :user_name => 'praktikum@marzell.net', - :password => 'CSSABXGRhSzUFK9', - domain: "marzell.net", - authentication: 'plain', - enable_starttls_auto: true + config.action_mailer.perform_deliveries = ENV.fetch("SMTP_ENABLED", "true") == "true" + config.action_mailer.raise_delivery_errors = true + config.action_mailer.smtp_settings = { + address: ENV.fetch("SMTP_ADDRESS", "smtp.ionos.de"), + port: ENV.fetch("SMTP_PORT", "587").to_i, + user_name: ENV["SMTP_USERNAME"], + password: ENV["SMTP_PASSWORD"], + domain: ENV.fetch("SMTP_DOMAIN", "marzell.net"), + authentication: ENV.fetch("SMTP_AUTHENTICATION", "plain"), + enable_starttls_auto: ENV.fetch("SMTP_STARTTLS", "true") == "true" } diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 07e9aed..c2179c0 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -1,8 +1,7 @@ # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. -Rails.application.config.assets.version = "1.0" -Rails.application.config.assets.precompile += %w( application.js jquery.min.js jquery_ujs.js jquery-ui.min.js ) +# API-only application: static assets are built and served by Angular/Nginx. # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path diff --git a/config/initializers/authenticated_pg_hero.rb b/config/initializers/authenticated_pg_hero.rb new file mode 100644 index 0000000..4938c7a --- /dev/null +++ b/config/initializers/authenticated_pg_hero.rb @@ -0,0 +1,25 @@ +class AuthenticatedPgHero + def initialize(app) + @app = app + end + + def call(environment) + request = Rack::Auth::Basic::Request.new(environment) + if valid_credentials?(request) + @app.call(environment) + else + [401, { "Content-Type" => "text/plain", "WWW-Authenticate" => 'Basic realm="PgHero Administration"' }, ["Anmeldung erforderlich"]] + end + end + + private + + def valid_credentials?(request) + return false unless request.provided? && request.basic? + expected_user = ENV.fetch("PGHERO_USERNAME", "admin") + expected_password = ENV["PGHERO_PASSWORD"].to_s + supplied_user, supplied_password = request.credentials + return false if expected_password.blank? + ActiveSupport::SecurityUtils.secure_compare(supplied_user.to_s, expected_user) && ActiveSupport::SecurityUtils.secure_compare(supplied_password.to_s, expected_password) + end +end diff --git a/config/initializers/build_version.rb b/config/initializers/build_version.rb new file mode 100644 index 0000000..fc4f820 --- /dev/null +++ b/config/initializers/build_version.rb @@ -0,0 +1,2 @@ +Rails.application.config.x.build_version = "2026.08.16.8" +Rails.logger.info("Praktikum API build 2026.08.16.8 loaded") diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 1e88826..ce9132d 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -24,10 +24,10 @@ Devise.setup do |config| # Configure the e-mail address which will be shown in Devise::Mailer, # note that it will be overwritten if you use your own mailer class # with default "from" parameter. - config.mailer_sender = 'praktikum@marzell.net' + config.mailer_sender = ENV.fetch("MAILER_FROM", "praktikum@marzell.net") # Configure the class responsible to send e-mails. - # config.mailer = 'Devise::Mailer' + config.mailer = 'UserMailer' # Configure the parent class responsible to send e-mails. # config.parent_mailer = 'ActionMailer::Base' diff --git a/config/initializers/good_job.rb b/config/initializers/good_job.rb index f8f9423..2d02074 100644 --- a/config/initializers/good_job.rb +++ b/config/initializers/good_job.rb @@ -17,7 +17,7 @@ Rails.application.configure do }, training_watch: { cron: "0 0 * * * Europe/Vienna", - class: "TrainingWatch", + class: "TrainingWatchJob", description: "TrainingWatch starten" } } diff --git a/config/routes.rb b/config/routes.rb index efceb2f..927e75d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,54 +1,39 @@ Rails.application.routes.draw do - namespace :admin do - resources :entries - resources :users - resources :mileage_rates - resources :training_watch_hits - resources :training_watch_sources - - - root to: "entries#index" - end - - authenticate :user, ->(u) { u.is_admin? } do - scope :admin do - mount PgHero::Engine, at: "pghero", as: :pghero - end - end - - get "/calendar/month/:year/:month", to: "calendar#month", as: :calendar_month - get "/calendar/week/:year/:week", to: "calendar#week", as: :calendar_week - - resources :mileage_rates, only: [:index, :new, :create, :edit, :update] - resource :dashboard, only: [:show] - - resources :entries do - post :start_timer, on: :collection - member do - post :stop_timer - end - collection do - get :export_csv - end - end - resource :user_goal, only: [:update] - root 'entries#index' - devise_for :users, controllers: { - registrations: 'users/registrations' - } - get '/impressum', to: 'static_pages#impressum' - get "/monthly_report", to: "entries#monthly_report", as: :monthly_report_entries - post "/db_dump/restore", to: "db_dump#restore" - get "/db_dump/dump", to: "db_dump#dump" - - get "/db_dump", to: "db_dump#index" - get "/rechner", to: "calculations#new", as: :rechner - # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html - - # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. - # Can be used by load balancers and uptime monitors to verify that the app is live. - get "up" => "rails/health#show", as: :rails_health_check - - # Defines the root path route ("/") - # root "posts#index" + namespace :api do + namespace :v1 do + post "auth/login", to: "auth#login" + post "auth/register", to: "auth#register" + post "auth/confirm", to: "auth#confirm" + post "auth/confirmation/resend", to: "auth#resend_confirmation" + post "auth/password/forgot", to: "auth#forgot_password" + patch "auth/password/reset", to: "auth#reset_password" + patch "auth/password/change", to: "auth#change_password" + delete "auth/logout", to: "auth#logout" + get "auth/me", to: "auth#me" + + resources :entries do + post :start_timer, on: :collection + post :stop_timer, on: :member + get :export_csv, on: :collection + end + get "dashboard", to: "dashboard#show" + get "calendar", to: "calendar#index" + get "monthly_report", to: "reports#monthly" + resources :mileage_rates, except: %i[new edit destroy] + resource :settings, only: %i[show update] + namespace :admin do + get "backup", to: "database#backup" + post "restore", to: "database#restore" + resources :users, only: %i[index show create update destroy] + resources :entries, only: %i[index show update destroy] + resources :training_watch_sources + resources :training_watch_hits, only: %i[index show destroy] + post "training_watch/run", to: "training_watch#run" + end + end + end + + mount AuthenticatedPgHero.new(PgHero::Engine), at: "/admin/pghero" + + get "up" => "rails/health#show", as: :rails_health_check end diff --git a/db/migrate/20260815000000_add_api_token_digest_to_users.rb b/db/migrate/20260815000000_add_api_token_digest_to_users.rb new file mode 100644 index 0000000..2b53d0d --- /dev/null +++ b/db/migrate/20260815000000_add_api_token_digest_to_users.rb @@ -0,0 +1,6 @@ +class AddApiTokenDigestToUsers < ActiveRecord::Migration[7.1] + def change + add_column :users, :api_token_digest, :string + add_index :users, :api_token_digest, unique: true + end +end diff --git a/db/migrate/20260816000000_ensure_api_token_digest_on_users.rb b/db/migrate/20260816000000_ensure_api_token_digest_on_users.rb new file mode 100644 index 0000000..2670dff --- /dev/null +++ b/db/migrate/20260816000000_ensure_api_token_digest_on_users.rb @@ -0,0 +1,6 @@ +class EnsureApiTokenDigestOnUsers < ActiveRecord::Migration[7.1] + def change + add_column :users, :api_token_digest, :string unless column_exists?(:users, :api_token_digest) + add_index :users, :api_token_digest, unique: true unless index_exists?(:users, :api_token_digest) + end +end diff --git a/docker-compose.yml b/docker-compose.yml index 0473a8d..7613140 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,55 +1,55 @@ - -version: "3.3" services: db: image: postgres:17 restart: unless-stopped environment: - POSTGRES_PASSWORD: password + POSTGRES_DB: praktikum POSTGRES_USER: praktikum - volumes: - - pgdata:/var/lib/postgresql/data - ports: - - '35432:35432' + POSTGRES_PASSWORD: ${DATABASE_PASSWORD:-password} + volumes: [pgdata:/var/lib/postgresql/data] healthcheck: - test: [ "CMD", "pg_isready", "-q" ] - timeout: 45s + test: ["CMD-SHELL", "pg_isready -U praktikum -d praktikum"] interval: 10s + timeout: 5s retries: 10 - command: -p 35432 - networks: - - praktikum-network redis: - image: 'redis' - command: redis-server - volumes: - - 'redis:/data' - networks: - - praktikum-network - environment: - - ALLOW_EMPTY_PASSWORD=yes - ports: - - '6379:6379' - web: + image: redis:7-alpine + restart: unless-stopped + volumes: [redis:/data] + api: build: . restart: unless-stopped - command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -b 0.0.0.0" - volumes: - - .:/app - ports: - - "13131:3000" depends_on: - - db + db: { condition: service_healthy } environment: - DATABASE_URL: postgres://praktikum:password@db:35432/postgres + DATABASE_URL: postgres://praktikum:${DATABASE_PASSWORD:-password}@db:5432/praktikum + REDIS_URL: redis://redis:6379/0 RAILS_ENV: production - SECRET_KEY_BASE: 5e1494fafd054d854b52661265e64e50d69787e9d4495173606f36fa3e5a685bd0264a41d6e138ea430f24008d5c07bcdf2a62a69de3c1b1cc042f05721432a0 - DB: db - networks: - - praktikum-network + SECRET_KEY_BASE: ${SECRET_KEY_BASE:?SECRET_KEY_BASE fehlt. Bitte zuerst setup-env.ps1 ausfuehren oder eine .env anlegen.} + FRONTEND_ORIGINS: ${FRONTEND_ORIGINS:-http://localhost:13131} + FRONTEND_URL: ${FRONTEND_URL:-http://localhost:13131} + APP_HOST: ${APP_HOST:-localhost:13131} + APP_PROTOCOL: ${APP_PROTOCOL:-http} + ADMIN_EMAIL: ${ADMIN_EMAIL:-christoph@marzell.net} + MAILER_FROM: ${MAILER_FROM:-praktikum@marzell.net} + SMTP_ENABLED: ${SMTP_ENABLED:-true} + SMTP_ADDRESS: ${SMTP_ADDRESS:-smtp.ionos.de} + SMTP_PORT: ${SMTP_PORT:-587} + SMTP_USERNAME: ${SMTP_USERNAME:-} + SMTP_PASSWORD: ${SMTP_PASSWORD:-} + SMTP_DOMAIN: ${SMTP_DOMAIN:-marzell.net} + SMTP_AUTHENTICATION: ${SMTP_AUTHENTICATION:-plain} + SMTP_STARTTLS: ${SMTP_STARTTLS:-true} + BACKUP_NOTIFY_EMAIL: ${BACKUP_NOTIFY_EMAIL:-christoph@marzell.net} + TRAINING_WATCH_NOTIFY_EMAIL: ${TRAINING_WATCH_NOTIFY_EMAIL:-christoph@marzell.net} + PGHERO_USERNAME: ${PGHERO_USERNAME:-admin} + PGHERO_PASSWORD: ${PGHERO_PASSWORD:-} + ports: ["${API_PORT:-13000}:3000"] + frontend: + build: ./frontend + restart: unless-stopped + depends_on: [api] + ports: ["${FRONTEND_PORT:-13131}:80"] volumes: pgdata: redis: -networks: - praktikum-network: - driver: bridge \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..c163121 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,11 @@ +FROM node:24-alpine AS build +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM nginx:1.29-alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist/praktikum/browser /usr/share/nginx/html +EXPOSE 80 diff --git a/frontend/angular.json b/frontend/angular.json new file mode 100644 index 0000000..334ec4a --- /dev/null +++ b/frontend/angular.json @@ -0,0 +1,9 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", + "projects": { "praktikum": { "projectType": "application", "root": "", "sourceRoot": "src", "prefix": "app", + "architect": { "build": { "builder": "@angular/build:application", "options": { "browser": "src/main.ts", "tsConfig": "tsconfig.app.json", "assets": [{"glob":"**/*","input":"public"}], "styles": ["node_modules/@fontsource-variable/inter/index.css", "src/styles.scss"] }, + "configurations": { "production": { "outputHashing": "all", "budgets": [{"type":"initial","maximumWarning":"1MB","maximumError":"2MB"}] } }, "defaultConfiguration": "production" }, + "serve": { "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "praktikum:build:production" }, "development": { "buildTarget": "praktikum:build" } }, "defaultConfiguration": "development" } + } + } }, "cli": { "analytics": false } +} diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..de7d359 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,20 @@ +server { + listen 80; + server_name _; + client_max_body_size 250m; + client_body_buffer_size 250m; + root /usr/share/nginx/html; + location /api/ { + proxy_pass http://api:3000; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location /admin/pghero { + proxy_pass http://api:3000; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location / { try_files $uri $uri/ /index.html; } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..8629428 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,6883 @@ +{ + "name": "praktikum-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "praktikum-frontend", + "version": "1.0.0", + "dependencies": { + "@angular/animations": "^22.0.0", + "@angular/cdk": "^22.0.0", + "@angular/common": "^22.0.0", + "@angular/compiler": "^22.0.0", + "@angular/core": "^22.0.0", + "@angular/forms": "^22.0.0", + "@angular/material": "^22.0.0", + "@angular/platform-browser": "^22.0.0", + "@angular/router": "^22.0.0", + "@fontsource-variable/inter": "^5.3.0", + "material-icons": "^1.13.14", + "rxjs": "~7.8.2", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@angular/build": "^22.0.0", + "@angular/cli": "^22.0.0", + "@angular/compiler-cli": "^22.0.0", + "typescript": "~6.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.2201.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2201.4.tgz", + "integrity": "sha512-a2J9hdBYFobhMx9G0KwCdo6+LrWIOOyIeijrA/QSZiPOjg8dAprhMh7BEQkF3eT6R1q7wDGP+IElVR9LsBlXQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "22.1.4", + "rxjs": "7.8.2" + }, + "bin": { + "architect": "bin/cli.js" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "22.1.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-22.1.4.tgz", + "integrity": "sha512-40jG/6chng9fQuWtpg1kadPO17sRTurNh0aav6Uxebn65PfNpTpvhPIYVIcRgv4tCKdMd++YH95A8SiaIOwqig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.20.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.5", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "22.1.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-22.1.4.tgz", + "integrity": "sha512-UBJ7jc7B0vRfmrVwlvpmuudo1D7AfpO6ugf1BxvkM4sw5m8cJpZtaIJFKBorv0QZo78CyDOroQfQdXaijjB2ZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "22.1.4", + "jsonc-parser": "3.3.1", + "magic-string": "1.0.0", + "ora": "9.4.1", + "rxjs": "7.8.2" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/animations": { + "version": "22.1.2", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-22.1.2.tgz", + "integrity": "sha512-SHvopFG8I92isKE6lgoWVVmmt6fSGKt76bfrBuJ9qvrKWYqWmWUh87Z5J3XRPUM8zdzuEkXjcTbn+GTGOGRiaA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/core": "22.1.2" + } + }, + "node_modules/@angular/build": { + "version": "22.1.4", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-22.1.4.tgz", + "integrity": "sha512-y5wxiyUu1DjYNG5jiFjjgFp7O5bkyYjWzy2kfyTGq+DjKMEveTzNH3tBkZrE33EJ4wYyfx+rQ01IG9nZbhfdvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.2201.4", + "@babel/core": "8.0.1", + "@babel/helper-annotate-as-pure": "8.0.0", + "@babel/helper-split-export-declaration": "7.24.7", + "@inquirer/confirm": "6.1.1", + "@vitejs/plugin-basic-ssl": "2.3.0", + "beasties": "0.4.3", + "browserslist": "^4.26.0", + "esbuild": "0.28.2", + "https-proxy-agent": "9.1.0", + "jsonc-parser": "3.3.1", + "listr2": "10.2.2", + "magic-string": "1.0.0", + "mrmime": "2.0.1", + "oxc-parser": "0.142.0", + "parse5-html-rewriting-stream": "8.0.1", + "picomatch": "4.0.5", + "piscina": "5.2.0", + "rolldown": "1.2.0", + "sass": "1.101.0", + "semver": "7.8.5", + "source-map-support": "0.5.21", + "tinyglobby": "0.2.17", + "vite": "8.1.5", + "watchpack": "2.5.2" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "lmdb": "3.5.6" + }, + "peerDependencies": { + "@angular/compiler": "^22.0.0", + "@angular/compiler-cli": "^22.0.0", + "@angular/core": "^22.0.0", + "@angular/localize": "^22.0.0", + "@angular/platform-browser": "^22.0.0", + "@angular/platform-server": "^22.0.0", + "@angular/service-worker": "^22.0.0", + "@angular/ssr": "^22.1.4", + "istanbul-lib-instrument": "^6.0.0", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "^22.0.0", + "postcss": "^8.4.0", + "rollup": "^4.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "tslib": "^2.3.0", + "typescript": ">=6.0 <6.1", + "vitest": "^4.0.8" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, + "@angular/localize": { + "optional": true + }, + "@angular/platform-browser": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "istanbul-lib-instrument": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "rollup": { + "optional": true + }, + "tailwindcss": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@angular/build/node_modules/listr2": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.2.tgz", + "integrity": "sha512-JtNtbZj8q5BnDMR7trpwvwk3RIrANtIVzEUm8w7amp6xelLgyuq+4WZoTH913XaQAoH/cNdYhaNzBPA2U3xbDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.2.0", + "eventemitter3": "^5.0.4", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^10.0.0" + }, + "engines": { + "node": ">=22.13.0" + } + }, + "node_modules/@angular/cdk": { + "version": "22.1.2", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-22.1.2.tgz", + "integrity": "sha512-YwTzVnTgZIQDLq3lnnPLINIB24e0SiDn7XxBxHSrOzD/+Y0zLAid5XIiMALXWOqa7rfdhfVh1kEeM93RRLnllQ==", + "license": "MIT", + "dependencies": { + "parse5": "^8.0.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^22.0.0 || ^23.0.0", + "@angular/core": "^22.0.0 || ^23.0.0", + "@angular/platform-browser": "^22.0.0 || ^23.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cli": { + "version": "22.1.4", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-22.1.4.tgz", + "integrity": "sha512-Yt6q7a1vXh0ICFHnRdUOmx/gEXZ87zpkvE1eUWREUqeQ3ARkwqNZVzF3IP+s9NpDDrnc+EB9cqel9SnGZ/0z2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.2201.4", + "@angular-devkit/core": "22.1.4", + "@angular-devkit/schematics": "22.1.4", + "@inquirer/prompts": "8.5.2", + "@listr2/prompt-adapter-inquirer": "4.2.4", + "@modelcontextprotocol/sdk": "1.30.0", + "@schematics/angular": "22.1.4", + "jsonc-parser": "3.3.1", + "listr2": "10.2.2", + "npm-package-arg": "14.0.0", + "parse5-html-rewriting-stream": "8.0.1", + "semver": "7.8.5", + "yargs": "18.1.0", + "zod": "4.4.3" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/listr2": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.2.tgz", + "integrity": "sha512-JtNtbZj8q5BnDMR7trpwvwk3RIrANtIVzEUm8w7amp6xelLgyuq+4WZoTH913XaQAoH/cNdYhaNzBPA2U3xbDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.2.0", + "eventemitter3": "^5.0.4", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^10.0.0" + }, + "engines": { + "node": ">=22.13.0" + } + }, + "node_modules/@angular/common": { + "version": "22.1.2", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-22.1.2.tgz", + "integrity": "sha512-8TzyYPBDvSItV8Vqq8k5u8a9fWaomC9bMEr8z1vVLRZDIPhNQ/UcCyuTYbC7GZkeqrrKGu5m8tjUrA09RCwuaQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/core": "22.1.2", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "22.1.2", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-22.1.2.tgz", + "integrity": "sha512-aQv0p5MeXuguCeftUUxK4H8Hbw1hC5Zyu+cFsGbsi025LZ9Ngw+BW+iUHDQZAcqHvWDw2wgGOdKh4e7IkcfRyQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@angular/compiler-cli": { + "version": "22.1.2", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-22.1.2.tgz", + "integrity": "sha512-GWRtvQQz/LvKejF/Zpls+hGnnJkvaPPtd3TZnxfZQ1fYCs7b7gpi0YM9PP02TJz51vhlsgkPYJwgtJAKDJEHNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "8.0.1", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^5.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^18.0.0" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/compiler": "22.1.2", + "typescript": ">=6.0 <6.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular/core": { + "version": "22.1.2", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-22.1.2.tgz", + "integrity": "sha512-t7BrgfdeqbdSbFe3CsGX8ySMeKDlZNBPxFMCOanAAqd6W0H1Kh1WFILBs3DIg0NfjpybfSRJAzKcagOFQWNtiw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/compiler": "22.1.2", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0 || ~0.16.0" + }, + "peerDependenciesMeta": { + "@angular/compiler": { + "optional": true + }, + "zone.js": { + "optional": true + } + } + }, + "node_modules/@angular/forms": { + "version": "22.1.2", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-22.1.2.tgz", + "integrity": "sha512-pKDYSNKje/jox74KzswBdX4sL7kJ8OFkJwLnqng8oc7a/NVl/Vf4/Xq4kyowRAdtPcZfLTUnZG6s0qbegfV5mg==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "tslib": "^2.3.0", + "zod": "^4.0.10" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/common": "22.1.2", + "@angular/core": "22.1.2", + "@angular/platform-browser": "22.1.2", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/material": { + "version": "22.1.2", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-22.1.2.tgz", + "integrity": "sha512-KX0bVUppfwjDfUepsjpG1CAQGUGHI0l5e0QYpFcskB+1wNRCocBGsIO1br5z6neYu0rzsbhtWocjCeH05gwi7w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/cdk": "22.1.2", + "@angular/common": "^22.0.0 || ^23.0.0", + "@angular/core": "^22.0.0 || ^23.0.0", + "@angular/forms": "^22.0.0 || ^23.0.0", + "@angular/platform-browser": "^22.0.0 || ^23.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "22.1.2", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-22.1.2.tgz", + "integrity": "sha512-MQO735ve/tk4gLsWbw8NyBD/DxPfbbmf1pM/vp6K7tZz2xyysMXilqF/k8PBdQBZVOofST8S13inZHQ35nHuRA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/animations": "22.1.2", + "@angular/common": "22.1.2", + "@angular/core": "22.1.2" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/router": { + "version": "22.1.2", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-22.1.2.tgz", + "integrity": "sha512-C2c6NJ9HZUfb1L07M554Zpgvi0TvpOBO6Og9L36v/jsv42m7tpOenvqQVPa8R3va7eqLEszCL7wLFcWeJ8naew==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/common": "22.1.2", + "@angular/core": "22.1.2", + "@angular/platform-browser": "22.1.2", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.0.tgz", + "integrity": "sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^8.0.0", + "js-tokens": "^10.0.0" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-8.0.0.tgz", + "integrity": "sha512-DOjnob/cXOUgDOozCDeq/aK2p5y8dUIVdf6tNhEV1HQRd6I8aQ4f4fbtHRVEvb6lP3BGomrKHiS8ICAASSVQSw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/core": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-8.0.1.tgz", + "integrity": "sha512-5FgxM4dLQpMJHSiVATk8foW263dVHQHBVpXYiimNECVWG01f4nFyEbQixeT6Mwvg7TayREJ2gpKl3o2RoMdnqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^8.0.0", + "@babel/generator": "^8.0.0", + "@babel/helper-compilation-targets": "^8.0.0", + "@babel/helpers": "^8.0.0", + "@babel/parser": "^8.0.0", + "@babel/template": "^8.0.0", + "@babel/traverse": "^8.0.0", + "@babel/types": "^8.0.0", + "@types/gensync": "^1.0.5", + "convert-source-map": "^2.0.0", + "empathic": "^2.0.1", + "gensync": "^1.0.0-beta.2", + "import-meta-resolve": "^4.2.0", + "json5": "^2.2.3", + "obug": "^2.1.1", + "semver": "^7.7.3" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz", + "integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^8.0.0", + "@babel/types": "^8.0.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "@types/jsesc": "^2.5.0", + "jsesc": "^3.0.2" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-8.0.0.tgz", + "integrity": "sha512-NSpMkMsvvZqzThJ0p1B02cbtA2ObEyfBvq950bmNkyxsxvcxwhvvCB036rKhlEnuBBo30bOrk13u3FzlKSoRrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.0" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-8.0.0.tgz", + "integrity": "sha512-JwculLABZvyPvyLBpwU/E/IbH2uM3mnxNtIJpxnIfb24y1PrdVxK5Dqjle4DpgqpGRnwgC7G8IkzPdSXZrO1Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^8.0.0", + "@babel/helper-validator-option": "^8.0.0", + "browserslist": "^4.24.0", + "lru-cache": "^11.0.0", + "semver": "^7.7.3" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-8.0.0.tgz", + "integrity": "sha512-lLozHOM6sWWlxNo8CYqHy4MBZeTvHXNgVPBfPOGsjPKUzHC2Az9QwB6gxdQmpwHl6GlQtbGgS+lj5887guDiLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration/node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration/node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration/node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", + "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", + "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-8.0.0.tgz", + "integrity": "sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helpers": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-8.0.0.tgz", + "integrity": "sha512-wfbi91pM3py96oIiJEz7qIpyXDytgr9zQC1HEWwlGNVRAEmItuU/0a41ZUKu1sJGyhhOIpc4t5vk4PYzt8wpsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^8.0.0", + "@babel/types": "^8.0.0" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/parser": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz", + "integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/template": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-8.0.0.tgz", + "integrity": "sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^8.0.0", + "@babel/parser": "^8.0.0", + "@babel/types": "^8.0.0" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/traverse": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.4.tgz", + "integrity": "sha512-bZnmqzGG8UZneG1lLxBoWIH0G6Gr1D846Yu4/3XnY6FhCndMR49u26nTY08u/dAxWmLWF9vGQOuC+84FfIUoeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^8.0.0", + "@babel/generator": "^8.0.0", + "@babel/helper-globals": "^8.0.0", + "@babel/parser": "^8.0.4", + "@babel/template": "^8.0.0", + "@babel/types": "^8.0.4", + "obug": "^2.1.1" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/types": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz", + "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^8.0.0", + "@babel/helper-validator-identifier": "^8.0.4" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fontsource-variable/inter": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz", + "integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@harperfast/extended-iterable": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz", + "integrity": "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@hono/node-server": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", + "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz", + "integrity": "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.1.tgz", + "integrity": "sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.1.1.tgz", + "integrity": "sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.1.tgz", + "integrity": "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.2.2.tgz", + "integrity": "sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/external-editor": "^3.0.3", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.1.tgz", + "integrity": "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", + "integrity": "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.2.tgz", + "integrity": "sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.1.1.tgz", + "integrity": "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.1.1.tgz", + "integrity": "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.5.2.tgz", + "integrity": "sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.2.1", + "@inquirer/confirm": "^6.1.1", + "@inquirer/editor": "^5.2.2", + "@inquirer/expand": "^5.1.1", + "@inquirer/input": "^5.1.2", + "@inquirer/number": "^4.1.1", + "@inquirer/password": "^5.1.1", + "@inquirer/rawlist": "^5.3.1", + "@inquirer/search": "^4.2.1", + "@inquirer/select": "^5.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.1.tgz", + "integrity": "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.2.1.tgz", + "integrity": "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.1.tgz", + "integrity": "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", + "integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-4.2.4.tgz", + "integrity": "sha512-/KRI2DMD7JGSYaREF0Ygl7AefJ/2ase4Gc5cBiKqT5l4tFjsSJfhFGcc5nSkgl0Sp9LkCQNzl/cqbVJYP2L3dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=22.13.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 9", + "listr2": "10.2.1" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.6.tgz", + "integrity": "sha512-mY5FG4TjPAkY4P0w+OhHaUka5mDh2TX2WKYIwuKzJ1zeW3VvRgxdam/lGJTquI+bthTx5CSHDW+BAQCnNAzkEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.6.tgz", + "integrity": "sha512-foa+pwitysO8k+xhs7psBFfTKnVgR69NlZRRTHaFVDqphh7AdGpLeyRzKw/ofatr/sN6TiHRRW6mmop0ZrrppQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.6.tgz", + "integrity": "sha512-QR4YRyR5h5Z8eGXrNQjiyo2NNDfqi3tCc9dQG5Is1blCt+qWw1ZoBWhlWAr5d+jshkifMIJjVHzHGKbkKzF8Tw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.6.tgz", + "integrity": "sha512-HmiyFFdJa38s1heCMSooSPaBSFTHJ3C+ERPp28xAPlDX1YiALJVOgbry065nXd8Y7KISWjnw05zpG1RX8IfftA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.6.tgz", + "integrity": "sha512-ADzCuCF2cTNiX9kDScqcz1fjnAkxPpQNneV3KFTdV3wWtVlI2sTGzySoMTgDpinkMMFj1NTJlxA6XR8fwc4hlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-arm64": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.6.tgz", + "integrity": "sha512-J7A9aEQsQiv0TYtBGL7NDIPp2lOS8nnl+zm4sWZm1xlsTTaQ4PgD096Adzdrk27rw3UxCkDXdCUa4ax41oztBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.6.tgz", + "integrity": "sha512-1g7G0knRX2iV/voDu54yxrGqw5Dk0w2oIYb7dgJq8IkOi+m7wbD8Q3QpPFjh0C01G58S88dqGn03len6UPCXsg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", + "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", + "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", + "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", + "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-openharmony-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", + "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.142.0.tgz", + "integrity": "sha512-ZiRGDutGsv1G6bL/ozy/koC0Sv39T1DqyoC4KD1DOy9ZoACm1O5UWhEK2c02Qdk+4lfLVkvFa/mQ0fm/4h1BtQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.142.0.tgz", + "integrity": "sha512-WZkvGRLNQTz8lR9zP5nLjUdlroRCopBu3g9zF1p/laE6DzT1UbQo8Rdz5MWhaJUPYg/6gp+jo7HUgsyKaN1FtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.142.0.tgz", + "integrity": "sha512-l4khS8LQOOVYsGRVARo1gSaCT/aBSceUVXgtovWc2+drnxVuDr082WA3OCHVdVzIz5JIrP/y9CWsSKxBDNmYGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.142.0.tgz", + "integrity": "sha512-QBsNF3nqlXmcH2B1YOPqQYmCJoy4HuIjUxGbBO/k5JAJUl68ghU2psRY2zPk+RyBaWqKP/qfL4oaFgEMCdwskA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.142.0.tgz", + "integrity": "sha512-b7Q7m4Cqc6XqNhri3R+QhU+GVy646Pn+bkdhrDdWym/Fdi0ZUa+d73H9dm5H91JtbtAQ/z1d8XKMW3oOV8a4tQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.142.0.tgz", + "integrity": "sha512-3riVS5IhdH3uCZj1Y9ftDQlR0dvLsIlw/edrRqk8JhgNd5K0XSs+UBtgh50N13CAlW9/TXj6sVGXaKNBocd0Yg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.142.0.tgz", + "integrity": "sha512-NmXUOpgpTSkhl795TiXmWppTwmSJ92RC1qvD6e4XOF+slgmo3e6Ah+kEu+6AN8s7NAOEwqGmir58MgSQSWmBSA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.142.0.tgz", + "integrity": "sha512-gc0EXsKtXgerujmU2Bql3u1L1HsSQ2774R83idq/FoNMPVV/RY/1ErFsvnit7KoiP/sLvzQixeUo4Ut0ic0wmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.142.0.tgz", + "integrity": "sha512-F2XvmWSE0uWpie+jHKKIFgdVOe9ypGhkEZxKx5DuW215K6cbAC274yYaPkcM7EqY4Df3Weyhpcz3lsURyH2LVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.142.0.tgz", + "integrity": "sha512-wLMbT21U/QxknQsk+VvNF0b9D2/aGWhcaQQQ+VYlE8FwD5+GoWZIPPXNzyHmkYyhm0KB3itL+TBavjMatqNnYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.142.0.tgz", + "integrity": "sha512-+G8F/4ckwT7FCJV4H2bt09xEzJbjNCfuL4Sp1AYNaFtFMVtgIGMuJlteT82U+K0UIZ/DzAR/LDlMFnEuajG7Kw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.142.0.tgz", + "integrity": "sha512-hTsHtTLxMAfCo+rpF5K3qZJKW2NpPN/CHd4mYB3y7XlSdspHkd2gehDIofP64AacA9nWQw2tY3O7wR6UY8IVOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.142.0.tgz", + "integrity": "sha512-6y7qYY3TCUDYjqswImdTGl92y+KA/80twALegQPN27kfY+bG7Ib1+L3jbmrCZQx6wrVnai9IPsEZp07I0hx7JQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.142.0.tgz", + "integrity": "sha512-i69kAWU+2LgoH5bR+zWiiu+UzAw7Oxkwv7COeJTeY19pn4e70nKQcr9Pm6cL2Z0Z54d+gl9qADlK/0yyuCPiBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.142.0.tgz", + "integrity": "sha512-4SQs678MmjYVrmhAgCWD4o0vpaFszXw9xLX5p2Z9MMFcltxiLkA88wQjh80YHjPrXtpyZ2CWI5m+1yNKM0m2Pw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.142.0.tgz", + "integrity": "sha512-YHpx9N7Ln3a++Tc8rv+H7mrK1zyJQOAwCFg8LZ3lTs1T5afGWeZrLPhPT9HLnIwSjCyJqPWVMIrMxbjcmBr2oQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.142.0.tgz", + "integrity": "sha512-3pLDyY3+oogW73RM5uehNgAiR/Xfb7fvO2Q1Z1gIqZ2+50XDVQmBVlRkHXZTU4gKnQHpwETNsYQVsJ3joVB2iA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.2", + "@emnapi/runtime": "1.11.2", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.142.0.tgz", + "integrity": "sha512-Had/VeVY28Oyb0K+Q4FV8KCzoBycIh93oDK6pCbya9lkzdq+ikMHMgBubsdqqlybjJmQRawCQRrnBRHyQwYvcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.142.0.tgz", + "integrity": "sha512-GGi3+YphVHavvgs6gum2UXoNCqzHAmPt/nXkn8ZQZstV2Q1qZD1Mn8fz/nWrDkefHQtrG/+1/XrbMxsBTo6Svw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.142.0.tgz", + "integrity": "sha512-Ny/Wv4Us1LGC/ljwNTp+Hx3r/pH15EFfeDF0p+n898gt+TtRd6C9SccHcuUhDiNTb8s5tt7jdeAMDRQZ4Vq6hg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz", + "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.0.tgz", + "integrity": "sha512-9yB1l95IrJuNGDFdOYe79vdApdz6WWBCObE+rQ2LUliYUlcyFwSYIb2xb5/Ifw7dAtMy2ZqNyd8QTSOc7duAKw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-pexNaW9ACLUOaBITOpU6qVu4VrsOFIjTv6bzgu0YUATo4eUJx0V605PxwZfndpPOn0ilqGqvGQ0M8UW0IE24jg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.0.tgz", + "integrity": "sha512-NqKYaq0355ZmNMG4QGpxtEDxsc7tGDhjhCm4PpE0cwnBW+5Il95LJyq414niEiaKLVjnVHBEjSo1wngKxJNiFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.0.tgz", + "integrity": "sha512-3vPoHzh6eBTz9IbB0/qZdSr0Qeks2echn+I4cHu2joV74VriPDdldswksEDzrl1mBB+oPRi+67+3Ib59paxIPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.0.tgz", + "integrity": "sha512-E6NNefZ1bUVmKJq2tJkf45J4Zyczj7qm9rUT7NY+Xo2474Y13qWAwc2tvBt0BAVbmtXR1llkxXg0Ou1jbDf2SQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.0.tgz", + "integrity": "sha512-D+TgkdgM1vu+7/Fpf8+v0ARW+RXEP9Ccazgm8zQ4JFFd9Q7SrYQ2TakU5S5ihazQDgpKyAgZDOcIFsvoHmTZ8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.0.tgz", + "integrity": "sha512-wUqdwJBbAv0APN87GecstdMUtLjjNTs0hBALpxETD73mccFxdmt/XeizXDtN5RAlBwNKmI+Tg+blect2G+8IeQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.0.tgz", + "integrity": "sha512-9DtF35qR9/NrfhM4oxLplCzVVjE+KKm8Pjemi0i/sdhAWkUasjmSo8WTTubNJClhSHCfyk2yeyoXDQEDPtDAAw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.0.tgz", + "integrity": "sha512-RzuHrBh8X8Hntd2N4VR02QGEciq/9JhcZoTpR/Cee6otRrlILGCf3cg2ygHuih+ZebUnWmMrDX6ITI85btO6rQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.0.tgz", + "integrity": "sha512-MK7L0018jjh1jR3mh21G2j1zAVcpscJBlPo2z19pRjv2XOYGRhaV4LyiD8HO6nCDdZln9IFgCMIV5yt4E3klGQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.0.tgz", + "integrity": "sha512-gyrxLQ9NfGb/9LoVnC4kb9miUghw1mghnkfYvNHSnVIXriabnfgGPUP4RLcJm87q3KgYz4FYUG8IDiWUT+CpSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.0.tgz", + "integrity": "sha512-/6VFMQGRmrhP77KXDC+StIxGzcNp5JOIyYtw0CQ8gPlzhpiIRucYfoM5FaFamHd5BJYIdH86yfP46l1p3WdrFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.2.0.tgz", + "integrity": "sha512-rwdbUL465kisF24WEJLvP3JrEG6E5GRuIHt5wpMwHGERtHe4Wm2CIvtf5gTBgr2tGOHKh5NdKEAFS2VkOPE91g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.2", + "@emnapi/runtime": "1.11.2", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.0.tgz", + "integrity": "sha512-+5suHwRiKGmhwyUaNT8a5QbrBvLFh2DbO910TEmGRH1aSxwrCezodvGQnulv4uiWEIv1Kq4ypRsJ5+O+ry1DiA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.0.tgz", + "integrity": "sha512-WfFv6/qGufotqBSBzBYwgpCkJBk8Nj7697LL9vTz/XWc67e0r3oewu8iMRwQj3AUL45GVD7wVsPjCsAAtW66Wg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@schematics/angular": { + "version": "22.1.4", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-22.1.4.tgz", + "integrity": "sha512-uiYKuiCnkTEbs4eOU1QJlM5NPw6BR1qgpct8VlrhS4n861WCCjTy5cxiAWJ3Doc0R72d44j3uoBfAEp7KEVn2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "22.1.4", + "@angular-devkit/schematics": "22.1.4", + "jsonc-parser": "3.3.1", + "typescript": "6.0.3" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/gensync": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.5.tgz", + "integrity": "sha512-MbsRCT7mTikHwKZ0X+LVUTLRrZZRLipTuXEO9qOYO+zmjMVk81axyClMROf6uoPD9MRVu46bx8zoR0Ad9q3NAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsesc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz", + "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.3.0.tgz", + "integrity": "sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.14.tgz", + "integrity": "sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/beasties": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.3.tgz", + "integrity": "sha512-fIIeLOcbAB/K1kb1HBVJoiq1alHL4RCYBSo5e7HzrNkkgMggXR1Vqt/Z9JWnkfe/qdCo66Ux3QRwZioAIBdWRA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "css-select": "^6.0.0", + "css-what": "^7.0.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^10.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3", + "postcss-safe-parser": "^7.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.406", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.406.tgz", + "integrity": "sha512-hWH5ORBi3d0IipnMh7BN5GDTaAmrSSSWmznwt2zltdiRNEWoEQyTwF0FFSBxzHO7hLSRT6loQu3IQGV0wg/Tvg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.1.tgz", + "integrity": "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz", + "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.2.tgz", + "integrity": "sha512-JydRilDRkYBQMt9qR9U92mXxmbGqsqSn/IKOrh4e7/gEbn+0zSr8igTu0obwJoNGN4sez28DIql7FBHWydoJpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/hosted-git-info": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-10.1.1.tgz", + "integrity": "sha512-DeOnSPAvOndYKfw075gt8yZzQ7S2hNztw34zBTfhIzLhmBTswIBg5/y+pqu/VD5cYWm5goAFTusDmUEmKZ0PEQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz", + "integrity": "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.8.tgz", + "integrity": "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/listr2": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.1.tgz", + "integrity": "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cli-truncate": "^5.2.0", + "eventemitter3": "^5.0.4", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^10.0.0" + }, + "engines": { + "node": ">=22.13.0" + } + }, + "node_modules/lmdb": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.6.tgz", + "integrity": "sha512-j3uE8ReKNyUWDjhfEFSJqE/1DLtfTR5Z8yFzVHvBjAk37wNg7HdScjcv8ttPHRvrdgPQMPWxFFI0SsdBzI5lBw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@harperfast/extended-iterable": "^1.0.3", + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.5.6", + "@lmdb/lmdb-darwin-x64": "3.5.6", + "@lmdb/lmdb-linux-arm": "3.5.6", + "@lmdb/lmdb-linux-arm64": "3.5.6", + "@lmdb/lmdb-linux-x64": "3.5.6", + "@lmdb/lmdb-win32-arm64": "3.5.6", + "@lmdb/lmdb-win32-x64": "3.5.6" + } + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.0.0.tgz", + "integrity": "sha512-CGvjzMN08iv6w1mm4/x3Gh1hLb4VnyRUA15FFpl6CsCIGGoe36k7kY5KNz9QDbSBN5I/fWHM6ZlIkUTa5xdUEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/material-icons": { + "version": "1.13.14", + "resolved": "https://registry.npmjs.org/material-icons/-/material-icons-1.13.14.tgz", + "integrity": "sha512-kZOfc7xCC0rAT8Q3DQixYAeT+tBqZnxkseQtp2bxBxz7q5pMAC+wmit7vJn1g/l7wRU+HEPq23gER4iPjGs5Cg==", + "license": "Apache-2.0" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.12.1.tgz", + "integrity": "sha512-4EUH9tQHnMmEgzW/MdAP0KIfa1T9AF+htl0ffe2n5vb2EKn9y2co8ccpgWko6S52Jy1PQZKwRnx5/KkYjtd9MQ==", + "dev": true, + "license": "MIT", + "optional": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm-package-arg": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-14.0.0.tgz", + "integrity": "sha512-69XQh3k+dtGa1p+7RaR57IuG3rCko96xr/nUfN4yDYBXbTYICiWcOpsFKLN2GtGE9cyIljE+f1exnaYt9MvM+Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^10.1.0", + "proc-log": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^8.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.1.tgz", + "integrity": "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.2", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ordered-binary": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", + "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/oxc-parser": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.142.0.tgz", + "integrity": "sha512-kKR+jPiRJYJDexVoziIg/FVGvr1fT1FZSSJOk6tVoMKKSlsf1Cso+cgGCJkOEDWOP174vRntCPFKg+AS7InWvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.142.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.142.0", + "@oxc-parser/binding-android-arm64": "0.142.0", + "@oxc-parser/binding-darwin-arm64": "0.142.0", + "@oxc-parser/binding-darwin-x64": "0.142.0", + "@oxc-parser/binding-freebsd-x64": "0.142.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.142.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.142.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.142.0", + "@oxc-parser/binding-linux-arm64-musl": "0.142.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.142.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.142.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.142.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.142.0", + "@oxc-parser/binding-linux-x64-gnu": "0.142.0", + "@oxc-parser/binding-linux-x64-musl": "0.142.0", + "@oxc-parser/binding-openharmony-arm64": "0.142.0", + "@oxc-parser/binding-wasm32-wasi": "0.142.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.142.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.142.0", + "@oxc-parser/binding-win32-x64-msvc": "0.142.0" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.1.tgz", + "integrity": "sha512-NaRku2aMpUN1Sh1Gyk1KWUh2A7EJx2c6qYzvwsPtqhoHoaURshdrceYK3LunVCm3WHhm6FS7Vcczbvdh3/UIVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0", + "parse5": "^8.0.0", + "parse5-sax-parser": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz", + "integrity": "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/piscina": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.2.0.tgz", + "integrity": "sha512-DszUCKeVN/5G5QKo6jAVHL8fmKnkJvQ0ACiVgY7YGCq3TUB2oznAOayvZPIAdEThvhczkXR+qm3IHsNXpFCYfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.x" + }, + "optionalDependencies": { + "@napi-rs/nice": "^1.0.4" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/proc-log": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-7.0.0.tgz", + "integrity": "sha512-FYgfaA69XZ93zaXLoMNQ+ViDXGGBgR8aLh03txzcFhV+9xOXx7+8DLCULrKKpR9+GsH9ZfHm82aSUPpozX0Ztg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent-negotiate": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-agent-negotiate/-/proxy-agent-negotiate-1.1.0.tgz", + "integrity": "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "kerberos": "^2.0.0" + }, + "peerDependenciesMeta": { + "kerberos": { + "optional": true + } + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.0.tgz", + "integrity": "sha512-u7tgm5l4Yw1iTqUL4EcYOAt7fFvCgQMLeidrnD4GALlC6aOznCjezYajgxeyKw27u0Q5N7fwgCzjVyPIWzwuBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.140.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.0", + "@rolldown/binding-darwin-arm64": "1.2.0", + "@rolldown/binding-darwin-x64": "1.2.0", + "@rolldown/binding-freebsd-x64": "1.2.0", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.0", + "@rolldown/binding-linux-arm64-gnu": "1.2.0", + "@rolldown/binding-linux-arm64-musl": "1.2.0", + "@rolldown/binding-linux-ppc64-gnu": "1.2.0", + "@rolldown/binding-linux-s390x-gnu": "1.2.0", + "@rolldown/binding-linux-x64-gnu": "1.2.0", + "@rolldown/binding-linux-x64-musl": "1.2.0", + "@rolldown/binding-openharmony-arm64": "1.2.0", + "@rolldown/binding-wasm32-wasi": "1.2.0", + "@rolldown/binding-win32-arm64-msvc": "1.2.0", + "@rolldown/binding-win32-x64-msvc": "1.2.0" + } + }, + "node_modules/rolldown/node_modules/@oxc-project/types": { + "version": "0.140.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.140.0.tgz", + "integrity": "sha512-h5LUOzGArYemnW1NMz/DuuQhBi96J6JL2Bk8zE4kvqxB5Sg3jxmCiH4uyOWHDkiKSt5vWlG4FIwCR/DbstcNRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.101.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.101.0.tgz", + "integrity": "sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stdin-discarder": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", + "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-8.0.0.tgz", + "integrity": "sha512-SCv6OOV6Xj2/3cXy3dGmADluJTNcL3o7hZAglNPTe+WYuEuvxgJzxPrSDLZhF+CwyQOubqgecjMmTJGMVLWjYQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/vite/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/vite/node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", + "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "string-width": "^8.2.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz", + "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yoctocolors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..5097996 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,31 @@ +{ + "name": "praktikum-frontend", + "version": "1.0.0", + "private": true, + "scripts": { + "start": "ng serve --proxy-config proxy.conf.json", + "build": "ng build", + "test": "ng test --watch=false" + }, + "dependencies": { + "@angular/animations": "^22.0.0", + "@angular/cdk": "^22.0.0", + "@angular/common": "^22.0.0", + "@angular/compiler": "^22.0.0", + "@angular/core": "^22.0.0", + "@angular/forms": "^22.0.0", + "@angular/material": "^22.0.0", + "@angular/platform-browser": "^22.0.0", + "@angular/router": "^22.0.0", + "@fontsource-variable/inter": "^5.3.0", + "material-icons": "^1.13.14", + "rxjs": "~7.8.2", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@angular/build": "^22.0.0", + "@angular/cli": "^22.0.0", + "@angular/compiler-cli": "^22.0.0", + "typescript": "~6.0.0" + } +} diff --git a/frontend/proxy.conf.json b/frontend/proxy.conf.json new file mode 100644 index 0000000..667bc8a --- /dev/null +++ b/frontend/proxy.conf.json @@ -0,0 +1 @@ +{"/api":{"target":"http://localhost:3000","secure":false,"changeOrigin":true},"/up":{"target":"http://localhost:3000","secure":false}} diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts new file mode 100644 index 0000000..f42dd84 --- /dev/null +++ b/frontend/src/app/app.component.ts @@ -0,0 +1,239 @@ +import { Component, OnInit, signal } from "@angular/core"; +import { + Router, + RouterLink, + RouterLinkActive, + RouterOutlet, +} from "@angular/router"; +import { MATERIAL } from "./shared/material"; +import { ApiService } from "./core/api.service"; +@Component({ + selector: "app-root", + standalone: true, + imports: [RouterOutlet, RouterLink, RouterLinkActive, ...MATERIAL], + template: ` + @if (loggedIn()) { + + + {{ api.user()?.email }} + } @else { + + } + `, + styles: [ + ` + .shell { + height: 100dvh; + min-height: 0; + overflow: hidden; + } + .nav { + width: 276px; + border-right: 1px solid var(--app-border); + background: var(--app-surface); + color: var(--app-text); + overflow: hidden; + } + .nav-layout { + height: 100%; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + padding: 10px 10px 8px; + } + .brand { + display: flex; + gap: 12px; + align-items: center; + padding: 4px 8px 10px; + } + .brand-mark { + display: grid; + place-items: center; + width: 42px; + height: 42px; + border-radius: 13px; + background: linear-gradient(135deg, #155bd7, #31a6f4); + color: white; + font-weight: 800; + font-size: 20px; + } + .brand div { + display: flex; + flex-direction: column; + } + .brand small { + color: var(--app-muted); + margin-top: 3px; + } + .active { + background: color-mix(in srgb, var(--app-primary) 15%, transparent) !important; + border-radius: 12px; + } + .nav-links { + flex: 1 1 auto; + min-height: 0; + overflow-x: hidden; + overflow-y: auto; + padding: 0 6px 6px; + scrollbar-width: thin; + scrollbar-color: color-mix(in srgb, var(--app-muted) 55%, transparent) transparent; + } + .nav-links a { + --mdc-list-list-item-one-line-container-height: 43px; + margin-bottom: 2px; + border-radius: 12px; + } + .nav-bottom { + flex: 0 0 auto; + display: flex; + flex-direction: column; + align-items: stretch; + padding: 5px 6px 0; + border-top: 1px solid var(--app-border); + } + .nav-bottom a, .nav-bottom button { justify-content:flex-start; width:100%; min-height:36px; } + .build-version { color:var(--app-muted);font-size:11px;padding:5px 12px 0; } + mat-toolbar { + position: sticky; + top: 0; + z-index: 10; + background: color-mix(in srgb, var(--app-surface) 88%, transparent); + backdrop-filter: blur(14px); + border-bottom: 1px solid var(--app-border); + color: var(--app-text); + } + .spacer { + flex: 1; + } + .user { + font-size: 14px; + margin-left: 8px; + } + .mobile-only { + display: none; + } + @media (max-width: 850px) { + .mobile-only { + display: inline-flex; + } + .user { + display: none; + } + } + `, + ], +}) +export class AppComponent implements OnInit { + readonly dark = signal(localStorage.getItem("theme") === "dark"); + readonly mobile = signal(innerWidth < 850); + constructor( + readonly api: ApiService, + private router: Router, + ) { + document.body.classList.toggle("dark", this.dark()); + addEventListener("resize", () => this.mobile.set(innerWidth < 850)); + } + ngOnInit() { + if (localStorage.getItem("praktikum_token")) + this.api.me().subscribe({ error: () => this.logout(false) }); + } + loggedIn() { + return !!localStorage.getItem("praktikum_token"); + } + toggleTheme() { + this.dark.update((v) => !v); + localStorage.setItem("theme", this.dark() ? "dark" : "light"); + document.body.classList.toggle("dark", this.dark()); + } + logout(call = true) { + if (call) + this.api.logout().subscribe(() => this.router.navigate(["/login"])); + else { + localStorage.removeItem("praktikum_token"); + this.router.navigate(["/login"]); + } + } +} diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts new file mode 100644 index 0000000..e0f33b1 --- /dev/null +++ b/frontend/src/app/app.routes.ts @@ -0,0 +1,117 @@ +import { Routes } from "@angular/router"; +import { inject } from "@angular/core"; +import { Router } from "@angular/router"; + +const authGuard = () => + localStorage.getItem("praktikum_token") + ? true + : inject(Router).createUrlTree(["/login"]); +export const routes: Routes = [ + { + path: "login", + loadComponent: () => + import("./pages/login.component").then((m) => m.LoginComponent), + }, + { + path: "register", + loadComponent: () => + import("./pages/register.component").then((m) => m.RegisterComponent), + }, + { + path: "forgot-password", + loadComponent: () => + import("./pages/forgot-password.component").then( + (m) => m.ForgotPasswordComponent, + ), + }, + { + path: "reset-password", + loadComponent: () => + import("./pages/reset-password.component").then( + (m) => m.ResetPasswordComponent, + ), + }, + { + path: "confirm-email", + loadComponent: () => + import("./pages/confirm-email.component").then( + (m) => m.ConfirmEmailComponent, + ), + }, + { path: "resend-confirmation", loadComponent: () => import("./pages/resend-confirmation.component").then((m) => m.ResendConfirmationComponent) }, + { + path: "", + canActivate: [authGuard], + children: [ + { + path: "dashboard", + loadComponent: () => + import("./pages/dashboard.component").then( + (m) => m.DashboardComponent, + ), + }, + { + path: "entries", + loadComponent: () => + import("./pages/entries.component").then((m) => m.EntriesComponent), + }, + { + path: "entries/new", + loadComponent: () => + import("./pages/entry-form.component").then( + (m) => m.EntryFormComponent, + ), + }, + { + path: "entries/:id/edit", + loadComponent: () => + import("./pages/entry-form.component").then( + (m) => m.EntryFormComponent, + ), + }, + { + path: "calendar", + loadComponent: () => + import("./pages/calendar.component").then((m) => m.CalendarComponent), + }, + { + path: "reports", + loadComponent: () => + import("./pages/reports.component").then((m) => m.ReportsComponent), + }, + { + path: "settings", + loadComponent: () => + import("./pages/settings.component").then((m) => m.SettingsComponent), + }, + { + path: "mileage-rates", + loadComponent: () => + import("./pages/mileage-rates.component").then( + (m) => m.MileageRatesComponent, + ), + }, + { + path: "calculator", + loadComponent: () => + import("./pages/calculator.component").then( + (m) => m.CalculatorComponent, + ), + }, + { + path: "admin/database", + loadComponent: () => + import("./pages/admin-database.component").then( + (m) => m.AdminDatabaseComponent, + ), + }, + { path: "", pathMatch: "full", redirectTo: "dashboard" }, + ], + }, + { + path: "impressum", + loadComponent: () => + import("./pages/impressum.component").then((m) => m.ImpressumComponent), + }, + { path: "**", redirectTo: "" }, +]; diff --git a/frontend/src/app/core/api.service.ts b/frontend/src/app/core/api.service.ts new file mode 100644 index 0000000..cdd3254 --- /dev/null +++ b/frontend/src/app/core/api.service.ts @@ -0,0 +1,206 @@ +import { Injectable, signal } from "@angular/core"; +import { HttpClient, HttpParams } from "@angular/common/http"; +import { tap } from "rxjs"; +import { + Dashboard, + Entry, + EntryInput, + MileageRate, + Settings, + User, +} from "./models"; + +@Injectable({ providedIn: "root" }) +export class ApiService { + private readonly base = "/api/v1"; + readonly user = signal(null); + constructor(private readonly http: HttpClient) {} + login(email: string, password: string) { + return this.http + .post<{ token: string; user: User }>(`${this.base}/auth/login`, { + email, + password, + }) + .pipe( + tap((r) => { + localStorage.setItem("praktikum_token", r.token); + this.user.set(r.user); + }), + ); + } + register(email: string, password: string, password_confirmation: string) { + return this.http.post<{ message: string }>(`${this.base}/auth/register`, { + email, + password, + password_confirmation, + }); + } + confirmEmail(token: string) { + return this.http.post<{ message: string }>(`${this.base}/auth/confirm`, { + token, + }); + } + resendConfirmation(email: string) { + return this.http.post<{ message: string }>(`${this.base}/auth/confirmation/resend`, { email }); + } + forgotPassword(email: string) { + return this.http.post<{ message: string }>( + `${this.base}/auth/password/forgot`, + { email }, + ); + } + resetPassword( + token: string, + password: string, + password_confirmation: string, + ) { + return this.http.patch<{ message: string }>( + `${this.base}/auth/password/reset`, + { token, password, password_confirmation }, + ); + } + changePassword( + current_password: string, + password: string, + password_confirmation: string, + ) { + return this.http.patch<{ message: string }>( + `${this.base}/auth/password/change`, + { current_password, password, password_confirmation }, + ); + } + logout() { + return this.http.delete(`${this.base}/auth/logout`).pipe( + tap(() => { + localStorage.removeItem("praktikum_token"); + this.user.set(null); + }), + ); + } + me() { + return this.http + .get(`${this.base}/auth/me`) + .pipe(tap((u) => this.user.set(u))); + } + dashboard() { + return this.http.get(`${this.base}/dashboard`); + } + entries(filters: Record = {}) { + return this.http.get(`${this.base}/entries`, { + params: new HttpParams({ fromObject: filters }), + }); + } + entry(id: number) { + return this.http.get(`${this.base}/entries/${id}`); + } + saveEntry(value: Partial, id?: number) { + return id + ? this.http.patch(`${this.base}/entries/${id}`, { entry: value }) + : this.http.post(`${this.base}/entries`, { entry: value }); + } + deleteEntry(id: number) { + return this.http.delete(`${this.base}/entries/${id}`); + } + startTimer(typ: string, art: string) { + return this.http.post(`${this.base}/entries/start_timer`, { + typ, + art, + }); + } + stopTimer(id: number, lunch_break: boolean) { + return this.http.post(`${this.base}/entries/${id}/stop_timer`, { + lunch_break, + }); + } + calendar(from: string, to: string) { + return this.http.get(`${this.base}/calendar`, { + params: { from, to }, + }); + } + settings() { + return this.http.get(`${this.base}/settings`); + } + saveSettings(settings: Partial) { + return this.http.patch(`${this.base}/settings`, { settings }); + } + mileageRates() { + return this.http.get(`${this.base}/mileage_rates`); + } + saveMileageRate(value: Partial) { + return value.id + ? this.http.patch(`${this.base}/mileage_rates/${value.id}`, { + mileage_rate: value, + }) + : this.http.post(`${this.base}/mileage_rates`, { + mileage_rate: value, + }); + } + monthlyReport() { + return this.http.get< + { month: string; typ: string; art: string; total_minutes: number }[] + >(`${this.base}/monthly_report`); + } + exportCsv() { + return this.http.get(`${this.base}/entries/export_csv`, { + responseType: "blob", + }); + } + databaseBackup() { + return this.http.get(`${this.base}/admin/backup`, { responseType: "blob" }); + } + databaseRestore(file: File) { + const data = new FormData(); + data.append("file", file); + return this.http.post<{ message: string }>( + `${this.base}/admin/restore`, + data, + ); + } + adminUsers() { + return this.http.get(`${this.base}/admin/users`); + } + createAdminUser(user: any) { + return this.http.post(`${this.base}/admin/users`, { user }); + } + updateAdminUser(id: number, user: any) { + return this.http.patch(`${this.base}/admin/users/${id}`, { user }); + } + deleteAdminUser(id: number) { + return this.http.delete(`${this.base}/admin/users/${id}`); + } + adminEntries() { + return this.http.get(`${this.base}/admin/entries`); + } + deleteAdminEntry(id: number) { + return this.http.delete(`${this.base}/admin/entries/${id}`); + } + trainingSources() { + return this.http.get(`${this.base}/admin/training_watch_sources`); + } + saveTrainingSource(value: any) { + return value.id + ? this.http.patch( + `${this.base}/admin/training_watch_sources/${value.id}`, + { training_watch_source: value }, + ) + : this.http.post(`${this.base}/admin/training_watch_sources`, { + training_watch_source: value, + }); + } + deleteTrainingSource(id: number) { + return this.http.delete( + `${this.base}/admin/training_watch_sources/${id}`, + ); + } + trainingHits() { + return this.http.get(`${this.base}/admin/training_watch_hits`); + } + deleteTrainingHit(id: number) { + return this.http.delete( + `${this.base}/admin/training_watch_hits/${id}`, + ); + } + runTrainingWatch() { + return this.http.post(`${this.base}/admin/training_watch/run`, {}); + } +} diff --git a/frontend/src/app/core/austrian-date-adapter.ts b/frontend/src/app/core/austrian-date-adapter.ts new file mode 100644 index 0000000..4a8c187 --- /dev/null +++ b/frontend/src/app/core/austrian-date-adapter.ts @@ -0,0 +1,72 @@ +import { + MatDateFormats, + NativeDateAdapter, +} from "@angular/material/core"; + +export const AUSTRIAN_DATE_FORMATS: MatDateFormats = { + parse: { + dateInput: "DD.MM.YYYY", + timeInput: "HH:mm", + }, + display: { + dateInput: { + day: "2-digit", + month: "2-digit", + year: "numeric", + }, + monthYearLabel: { + month: "long", + year: "numeric", + }, + dateA11yLabel: { + day: "2-digit", + month: "long", + year: "numeric", + }, + monthYearA11yLabel: { + month: "long", + year: "numeric", + }, + timeInput: { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }, + timeOptionLabel: { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }, + }, +}; + +export class AustrianDateAdapter extends NativeDateAdapter { + override parse(value: unknown): Date | null { + if (value instanceof Date) return value; + if (typeof value === "string") { + const match = value.trim().match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/); + if (match) { + const day = Number(match[1]); + const month = Number(match[2]) - 1; + const year = Number(match[3]); + const date = new Date(year, month, day); + return date.getFullYear() === year && + date.getMonth() === month && + date.getDate() === day + ? date + : this.invalid(); + } + } + return super.parse(value); + } + + override parseTime(value: unknown): Date | null { + if (value instanceof Date) return value; + if (typeof value !== "string") return this.invalid(); + const match = value.trim().match(/^([01]?\d|2[0-3])[:.]([0-5]\d)$/); + if (!match) return this.invalid(); + const date = this.today(); + date.setHours(Number(match[1]), Number(match[2]), 0, 0); + return date; + } +} diff --git a/frontend/src/app/core/auth.interceptor.ts b/frontend/src/app/core/auth.interceptor.ts new file mode 100644 index 0000000..89b45ef --- /dev/null +++ b/frontend/src/app/core/auth.interceptor.ts @@ -0,0 +1,2 @@ +import { HttpInterceptorFn } from '@angular/common/http'; +export const authInterceptor:HttpInterceptorFn=(req,next)=>{const token=localStorage.getItem('praktikum_token');return next(token?req.clone({setHeaders:{Authorization:`Bearer ${token}`}}):req);}; diff --git a/frontend/src/app/core/models.ts b/frontend/src/app/core/models.ts new file mode 100644 index 0000000..12f4bc3 --- /dev/null +++ b/frontend/src/app/core/models.ts @@ -0,0 +1,71 @@ +export interface User { + id: number; + email: string; + praepedeutikum_done: boolean; + is_admin: boolean; +} +export interface Entry { + id: number; + date: string; + hours: number; + minutes: number; + praktikums_typ: string; + entry_art: string; + distance_km: number; + beschreibung: string | null; + kosten: number | null; + start_time: string | null; + end_time: string | null; + lunch_break_minutes: number; + zaehlt_als_fortbildung: boolean; + kilometer_pauschale: number; +} +export type EntryInput = Omit; +export interface Progress { + typ: string; + art: string; + spent_minutes: number; + target_minutes: number; + remaining_minutes: number; + percent: number; + weekly_target: number; + actual_weekly: number | null; + estimated_end: string | null; +} +export interface Dashboard { + total_minutes: number; + target_minutes: number; + remaining_minutes: number; + completed_percent: number; + total_distance_km: number; + costs_by_year: { + year: number; + kilometer: number; + fortbildung: number; + selbsterfahrung: number; + supervision: number; + semester: number; + total: number; + }[]; + last_entry: Entry | null; + running_entry: Entry | null; + mediation_presence_days: number; + mediation_presence_days_required: number; + progress: Progress[]; +} +export interface Settings { + email: string; + total_required_hours: number; + weekly_target_hours: number; + praepedeutikum_done: boolean; + required_hours_matrix: Record>; + weekly_target_matrix: Record>; + praktikums_typen: string[]; + entry_arten: string[]; + entry_arten_by_typ: Record; +} +export interface MileageRate { + id: number; + year: number; + rate_per_km: number; +} diff --git a/frontend/src/app/pages/admin-database.component.ts b/frontend/src/app/pages/admin-database.component.ts new file mode 100644 index 0000000..1807c5a --- /dev/null +++ b/frontend/src/app/pages/admin-database.component.ts @@ -0,0 +1,368 @@ +import { Component, OnInit, signal } from "@angular/core"; +import { DatePipe } from "@angular/common"; +import { FormsModule } from "@angular/forms"; +import { Router } from "@angular/router"; +import { MatSnackBar } from "@angular/material/snack-bar"; +import { ApiService } from "../core/api.service"; +import { MATERIAL } from "../shared/material"; + +@Component({ + standalone: true, + imports: [FormsModule, DatePipe, ...MATERIAL], + template: `
+
+
+

Administration

+

+ Benutzer, Daten, Sicherungen und Ausbildungsstellen-Watcher +

+
+
+
+ @for (item of sections; track item.id) { + + } +
+ @if (section() === "database") { +
+ Datenbank sichern

Aktuellen PostgreSQL-Dump als ZIP herunterladen.

+
PgHero

Datenbankabfragen, Speicher und Performance analysieren.

monitoring PgHero öffnen
Datenbank wiederherstellen

+ Eine Sicherung überschreibt den aktuellen Datenbestand. Vorher + unbedingt ein Backup herunterladen. +

+
+
+ } + @if (section() === "users") { + E-MailStartpasswortSofort bestätigen +
+ + + + + + + + +
E-Mail + {{ u.email }} + @if (u.is_admin) { + Admin + } + Bestätigt + Einträge + {{ u.entries_count }} + + +
+ } + @if (section() === "entries") { +
+ + + + + + + + + +
Datum + {{ e.date | date: "dd.MM.yyyy" }} + Benutzer + {{ e.user_email }} + Art + {{ e.praktikums_typ }} / {{ e.entry_art }} + Zeit + {{ e.hours }} h {{ e.minutes }} min + + +
+ } + @if (section() === "watcher") { +
+

Ausbildungsstellen-Watcher

+ +
+ NameURLTypHTMLRSSAktiv +
+ @for (s of sources(); track s.id) { + {{ s.name }}{{ s.url }}Zuletzt geprüft: + {{ + s.last_checked_at + ? (s.last_checked_at | date: "dd.MM.yyyy HH:mm") + : "–" + }} +
+ +
+ } +
+

Gefundene Treffer

+ @for (h of hits(); track h.id) { + {{ h.title }}{{ h.source_name }} +

{{ h.snippet }}

+
+ Öffnen +
+ } + } +
`, + styles: [ + ` + .admin-nav { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 22px; + } + .selected { + background: #dce9ff !important; + } + .danger { + border: 1px solid #e1a4a4; + } + .danger input { + margin: 10px 0 20px; + } + .source-form { + margin-bottom: 22px; + } + .source-form mat-card-content { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + } + .source-form mat-form-field { + min-width: 220px; + } + .hit { + margin-bottom: 10px; + } + .hit mat-card-content, + .cards mat-card-content { + display: flex; + flex-direction: column; + gap: 8px; + } + table { + width: 100%; + } + `, + ], +}) +export class AdminDatabaseComponent implements OnInit { + readonly sections = [ + { id: "database", label: "Backup & Restore", icon: "database" }, + { id: "users", label: "Benutzer", icon: "group" }, + { id: "entries", label: "Alle Einträge", icon: "list_alt" }, + { id: "watcher", label: "Ausbildungs-Watcher", icon: "travel_explore" }, + ]; + section = signal("database"); + restoreFile = signal(null); + busy = signal(false); + users = signal([]); + entries = signal([]); + sources = signal([]); + hits = signal([]); + userColumns = ["email", "confirmed", "entries", "actions"]; + entryColumns = ["date", "user", "art", "time", "actions"]; + sourceDraft: any = { name: "", url: "", kind: "html", enabled: true }; + newUser: any = { email: "", password: "", confirmed: true }; + constructor( + private api: ApiService, + private snack: MatSnackBar, + router: Router, + ) { + if (api.user() && !api.user()!.is_admin) router.navigate(["/dashboard"]); + } + ngOnInit() { + this.reload(); + } + reload() { + this.api.adminUsers().subscribe((v) => this.users.set(v)); + this.api.adminEntries().subscribe((v) => this.entries.set(v)); + this.api.trainingSources().subscribe((v) => this.sources.set(v)); + this.api.trainingHits().subscribe((v) => this.hits.set(v)); + } + selectFile(event: Event) { + this.restoreFile.set((event.target as HTMLInputElement).files?.[0] || null); + } + backup() { + this.api.databaseBackup().subscribe((blob) => { + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = `praktikum-backup-${new Date().toISOString().slice(0, 10)}.zip`; + a.click(); + URL.revokeObjectURL(a.href); + }); + } + restore() { + const file = this.restoreFile(); + if ( + !file || + !confirm( + "Aktuelle Datenbank wirklich mit dieser Sicherung überschreiben?", + ) + ) + return; + this.busy.set(true); + this.api.databaseRestore(file).subscribe({ + next: (r) => { + this.busy.set(false); + this.snack.open(r.message, "OK"); + this.reload(); + }, + error: (e) => { + this.busy.set(false); + this.snack.open(e.error?.error || "Restore fehlgeschlagen", "OK"); + }, + }); + } + setConfirmed(u: any, value: boolean) { + this.api + .updateAdminUser(u.id, { confirmed: value }) + .subscribe(() => this.reload()); + } + createUser(){this.api.createAdminUser(this.newUser).subscribe({next:()=>{this.newUser={email:"",password:"",confirmed:true};this.reload();},error:e=>this.snack.open(e.error?.errors?.join(", ")||"Benutzer konnte nicht angelegt werden","OK")});} + removeUser(u: any) { + if (confirm(`${u.email} wirklich löschen?`)) + this.api.deleteAdminUser(u.id).subscribe(() => this.reload()); + } + removeEntry(e: any) { + if (confirm("Eintrag wirklich löschen?")) + this.api.deleteAdminEntry(e.id).subscribe(() => this.reload()); + } + editSource(s: any) { + this.sourceDraft = { ...s }; + } + saveSource() { + this.api.saveTrainingSource(this.sourceDraft).subscribe(() => { + this.sourceDraft = { name: "", url: "", kind: "html", enabled: true }; + this.reload(); + }); + } + removeSource(s: any) { + if (confirm("Quelle und zugehörige Treffer löschen?")) + this.api.deleteTrainingSource(s.id).subscribe(() => this.reload()); + } + removeHit(h: any) { + this.api.deleteTrainingHit(h.id).subscribe(() => this.reload()); + } + runWatcher() { + this.api.runTrainingWatch().subscribe((r) => { + this.snack.open(`${r.new_hits} neue Treffer`, "OK"); + this.reload(); + }); + } +} diff --git a/frontend/src/app/pages/calculator.component.ts b/frontend/src/app/pages/calculator.component.ts new file mode 100644 index 0000000..98ec958 --- /dev/null +++ b/frontend/src/app/pages/calculator.component.ts @@ -0,0 +1,84 @@ +import { Component } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { MATERIAL } from "../shared/material"; +@Component({ + standalone: true, + imports: [FormsModule, ...MATERIAL], + template: `
+
+
+

Minutenrechner

+

Beliebig viele Zeitwerte addieren

+
+
+ MinutenwerteTrennung durch Komma, Leerzeichen, Semikolon oder + Zeilenumbruch +
+ schedule +
+ Gesamtdauer{{ hours() }} h {{ minutes() }} min{{ total() }} Minuten +
+
+
+
`, + styles: [ + ` + .narrow { + max-width: 760px; + } + .result { + display: flex; + gap: 18px; + align-items: center; + padding: 24px; + background: #e8f0ff; + border-radius: 16px; + margin: 20px 0; + } + .result mat-icon { + font-size: 36px; + width: 38px; + height: 38px; + } + .result div { + display: flex; + flex-direction: column; + } + .result strong { + font-size: 2rem; + margin: 4px 0; + } + `, + ], +}) +export class CalculatorComponent { + input = ""; + total(): number { + return this.input + .split(/[\s,;]+/) + .map(Number) + .filter(Number.isFinite) + .reduce((a, b) => a + b, 0); + } + hours(): number { return Math.floor(this.total() / 60); } + minutes(): number { return this.total() % 60; } +} diff --git a/frontend/src/app/pages/calendar.component.ts b/frontend/src/app/pages/calendar.component.ts new file mode 100644 index 0000000..c8e7cea --- /dev/null +++ b/frontend/src/app/pages/calendar.component.ts @@ -0,0 +1,238 @@ +import { Component, OnInit, signal } from "@angular/core"; +import { RouterLink } from "@angular/router"; +import { FormsModule } from "@angular/forms"; +import { MATERIAL } from "../shared/material"; +import { ApiService } from "../core/api.service"; +import { Entry } from "../core/models"; +@Component({ + standalone: true, + imports: [RouterLink, FormsModule, ...MATERIAL], + template: `
+
+
+

Kalender

+

{{ periodLabel() }}

+
+
+ + + +
+
+ + Ausbildung + Alle + @for (type of types; track type) { {{ type }} } + + Art + Alle + @for (art of arts; track art) { {{ art }} } + + + + @if (loading()) { + + } +
+ @for (d of weekdays; track d) { + {{ d }} + } +
+
+ @for (day of days(); track day.key) { +
+
+ {{ day.date.getDate() }}add +
+ @for (e of forDay(day.key); track e.id) { + {{ e.entry_art }}{{ e.hours }}h {{ e.minutes }}min + } +
+ } +
+
`, + styles: [ + ` + .calendar { + overflow: hidden; + } + .calendar-filters { margin-bottom:18px; } + .calendar-filters mat-card-content { display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding-bottom:0; } + .calendar-filters mat-form-field { min-width:220px; } + .selected { background:#dce9ff!important; } + .weekdays, + .days { + display: grid; + grid-template-columns: repeat(7, 1fr); + } + .weekdays { + padding: 14px; + background: #edf2fb; + text-align: center; + color: #59657a; + } + .day { + min-height: 145px; + padding: 8px; + border-top: 1px solid #e1e6ef; + border-right: 1px solid #e1e6ef; + } + .day-head { + display: flex; + justify-content: space-between; + align-items: center; + } + .day-head a { + transform: scale(0.8); + } + .outside { + opacity: 0.42; + } + .today { + background: #eef5ff; + } + .today .day-head > span { + background: #155bd7; + color: white; + border-radius: 50%; + width: 28px; + height: 28px; + display: grid; + place-items: center; + } + .event { + display: flex; + flex-direction: column; + text-decoration: none; + color: inherit; + background: #dce9ff; + border-left: 3px solid #2868d8; + border-radius: 6px; + padding: 5px 7px; + margin: 4px 0; + font-size: 12px; + } + .event span { + opacity: 0.75; + margin-top: 2px; + } + .event.type-propaedeutikum { background:rgba(13,110,253,.14);border-left-color:#0d6efd; } + .event.type-fachspezifikum { background:rgba(25,135,84,.14);border-left-color:#198754; } + .event.type-mediation { background:rgba(253,126,20,.16);border-left-color:#fd7e14; } + @media (max-width: 700px) { + .weekdays strong { + font-size: 0; + } + .weekdays strong:first-letter { + font-size: 13px; + } + .day { + min-height: 90px; + padding: 4px; + } + .event strong { + overflow: hidden; + text-overflow: ellipsis; + } + .event span { + display: none; + } + } + `, + ], +}) +export class CalendarComponent implements OnInit { + readonly month = signal( + new Date(new Date().getFullYear(), new Date().getMonth(), 1), + ); + readonly weekAnchor = signal(new Date()); + readonly mode = signal<"month"|"week">("month"); + readonly entries = signal([]); + readonly loading = signal(false); + readonly weekdays = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]; + readonly types = ["propädeutikum", "fachspezifikum", "mediation"]; + readonly arts = ["Praktikum", "Selbsterfahrung", "Supervision", "Fortbildung", "Semesterkosten", "Gruppenselbsterfahrung", "Theorie/Methodikseminare", "Peergruppensupervision", "Einzel-/Kleingruppensupervision", "Eigenständige Tätigkeit", "Präsenzmodul", "Peergruppenarbeit", "Fallarbeit", "Praxisseminare", "Literatur- und Selbststudium"]; + typeFilter = ""; + artFilter = ""; + readonly todayKey = this.key(new Date()); + constructor(private api: ApiService) {} + ngOnInit() { + this.load(); + } + days() { + if(this.mode()==="week"){ + const start=new Date(this.weekAnchor()); + start.setDate(start.getDate()-((start.getDay()+6)%7)); + return Array.from({length:7},(_,i)=>{const date=new Date(start);date.setDate(start.getDate()+i);return{date,key:this.key(date),current:true};}); + } + const first = this.month(), + start = new Date(first); + const offset = (start.getDay() + 6) % 7; + start.setDate(start.getDate() - offset); + return Array.from({ length: 42 }, (_, i) => { + const date = new Date(start); + date.setDate(start.getDate() + i); + return { + date, + key: this.key(date), + current: date.getMonth() === first.getMonth(), + }; + }); + } + forDay(key: string) { + return this.entries().filter((e) => e.date === key && (!this.typeFilter || e.praktikums_typ === this.typeFilter) && (!this.artFilter || e.entry_art === this.artFilter)); + } + clearFilters(){this.typeFilter="";this.artFilter="";} + typeClass(value:string){ + const normalized=value?.toLocaleLowerCase("de-AT").normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,""); + return `type-${normalized === "propadeutikum" ? "propaedeutikum" : normalized || "unknown"}`; + } + move(n: number) { + if(this.mode()==="week"){const d=new Date(this.weekAnchor());d.setDate(d.getDate()+n*7);this.weekAnchor.set(d);this.load();return;} + const d = this.month(); + this.month.set(new Date(d.getFullYear(), d.getMonth() + n, 1)); + this.load(); + } + today() { + const d = new Date(); + this.month.set(new Date(d.getFullYear(), d.getMonth(), 1)); + this.weekAnchor.set(d); + this.load(); + } + setMode(mode:"month"|"week"){this.mode.set(mode);this.load();} + periodLabel(){if(this.mode()==="month")return new Intl.DateTimeFormat("de-AT",{month:"long",year:"numeric"}).format(this.month());const ds=this.days();return `${new Intl.DateTimeFormat("de-AT",{day:"2-digit",month:"2-digit"}).format(ds[0].date)} – ${new Intl.DateTimeFormat("de-AT",{day:"2-digit",month:"2-digit",year:"numeric"}).format(ds[6].date)}`;} + load() { + const ds = this.days(); + this.loading.set(true); + this.api.calendar(ds[0].key, ds[ds.length-1].key).subscribe({ + next: (e) => { + this.entries.set(e); + this.loading.set(false); + }, + error: () => this.loading.set(false), + }); + } + private key(d: Date) { + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; + } +} diff --git a/frontend/src/app/pages/confirm-email.component.ts b/frontend/src/app/pages/confirm-email.component.ts new file mode 100644 index 0000000..fe9a508 --- /dev/null +++ b/frontend/src/app/pages/confirm-email.component.ts @@ -0,0 +1,3 @@ +import { Component, inject, signal } from '@angular/core'; import { ActivatedRoute, RouterLink } from '@angular/router'; import { ApiService } from '../core/api.service'; import { MATERIAL } from '../shared/material'; import { authStyles } from './register.component'; +@Component({standalone:true,imports:[RouterLink,...MATERIAL],template:`
mark_email_read

E-Mail bestätigen

@if(loading()){} @else if(error()){

{{error()}}

} @else {
{{message()}}
}Zur Anmeldung
`,styles:[authStyles()]}) +export class ConfirmEmailComponent{loading=signal(true);error=signal('');message=signal('');private token=inject(ActivatedRoute).snapshot.queryParamMap.get('token')||'';constructor(api:ApiService){if(!this.token){this.loading.set(false);this.error.set('Der Bestätigungslink ist ungültig.');}else api.confirmEmail(this.token).subscribe({next:r=>{this.loading.set(false);this.message.set(r.message);},error:e=>{this.loading.set(false);this.error.set(e.error?.errors?.join(', ')||'Bestätigung fehlgeschlagen');}});}} diff --git a/frontend/src/app/pages/dashboard.component.ts b/frontend/src/app/pages/dashboard.component.ts new file mode 100644 index 0000000..5bb6cc7 --- /dev/null +++ b/frontend/src/app/pages/dashboard.component.ts @@ -0,0 +1,232 @@ +import { Component, OnInit, signal } from "@angular/core"; +import { RouterLink } from "@angular/router"; +import { DatePipe, DecimalPipe } from "@angular/common"; +import { FormsModule } from "@angular/forms"; +import { MATERIAL } from "../shared/material"; +import { ApiService } from "../core/api.service"; +import { Dashboard } from "../core/models"; +@Component({ + standalone: true, + imports: [RouterLink, FormsModule, DatePipe, DecimalPipe, ...MATERIAL], + template: `
+
+
+

Dashboard

+

Dein Ausbildungsfortschritt auf einen Blick

+
+ add Neuer Eintrag +
+ @if (loading()) { + + } @else if (data(); as d) { +
+ scheduleGeleistete Zeit{{ duration(d.total_minutes) }}flagGesamtfortschritt{{ d.completed_percent | number: "1.0-1" }} %routeGefahrene Kilometer{{ d.total_distance_km | number: "1.0-0" }} kmhourglass_bottomNoch offen{{ + duration(d.remaining_minutes) + }} +
+ Fortschritt nach Ausbildung
0 % anzeigenErledigte anzeigen
+ @for (row of active(d); track row.typ + row.art) { +
+
+ {{ row.art }}{{ label(row.typ) }} · {{ duration(row.spent_minutes) }} / + {{ duration(row.target_minutes) }} + Ist: {{ row.actual_weekly ?? 0 }} h/Woche · Ziel: + {{ row.weekly_target }} h/Woche + @if (row.estimated_end) { + Voraussichtlich fertig: + {{ row.estimated_end | date: "dd.MM.yyyy" }} + } +
+ {{ row.percent }} % +
+ }
+ Kosten nach Jahr
+ @for (cost of d.costs_by_year; track cost.year) { +
+ {{ cost.year }}Fahrtkosten + {{ cost.kilometer | number: "1.2-2" }} €Fortbildung + {{ cost.fortbildung | number: "1.2-2" }} €Selbsterfahrung + {{ cost.selbsterfahrung | number: "1.2-2" }} €Supervision + {{ cost.supervision | number: "1.2-2" }} €Semesterkosten + {{ cost.semester | number: "1.2-2" }} €Gesamt {{ cost.total | number: "1.2-2" }} € +
+ } +
+ Mediation – Präsenztage{{ d.mediation_presence_days }} von + {{ d.mediation_presence_days_required }} Tagen erledigt + } +
`, + styles: [ + ` + mat-card-content { + display: flex; + flex-direction: column; + gap: 9px; + } + mat-card-content > mat-icon { + color: #155bd7; + background: #e7efff; + border-radius: 12px; + padding: 9px; + width: 42px; + height: 42px; + } + mat-card-content > strong { + font-size: 1.65rem; + } + .progress-card { + margin-top: 22px; + padding: 10px; + } + .progress-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 22px; + margin-top: 20px; + } + .progress-row { + display: grid; + grid-template-columns: 1fr auto; + gap: 8px; + padding:14px; + border-radius:12px; + border-left:4px solid transparent; + } + .progress-options { display:flex;gap:18px;flex-wrap:wrap; } + .progress-row.type-propaedeutikum { background:rgba(13,110,253,.08);border-left-color:#0d6efd; } + .progress-row.type-fachspezifikum { background:rgba(25,135,84,.08);border-left-color:#198754; } + .progress-row.type-mediation { background:rgba(253,126,20,.09);border-left-color:#fd7e14; } + .progress-row div { + display: flex; + flex-direction: column; + } + .progress-row small { + color: #687287; + margin-top: 4px; + } + .progress-row mat-progress-bar { + grid-column: 1/-1; + } + .cost-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 18px; + margin-top: 16px; + } + .cost-year { + display: flex; + flex-direction: column; + gap: 8px; + padding: 16px; + background: #f0f4fa; + border-radius: 14px; + } + .cost-year span { + display: flex; + justify-content: space-between; + } + .cost-year > strong { + font-size: 1.3rem; + } + .cost-year .total { + border-top: 1px solid #ccd4e2; + padding-top: 9px; + } + `, + ], +}) +export class DashboardComponent implements OnInit { + readonly data = signal(null); + readonly loading = signal(true); + showZero = false; + showCompleted = false; + constructor(private api: ApiService) {} + ngOnInit() { + this.api.dashboard().subscribe({ + next: (d) => { + this.data.set(d); + this.loading.set(false); + }, + error: () => this.loading.set(false), + }); + } + duration(m: number) { + return `${Math.floor(m / 60)} h ${m % 60} min`; + } + label(v: string) { + return v.charAt(0).toUpperCase() + v.slice(1); + } + active(d: Dashboard) { + return d.progress.filter((r) => r.target_minutes > 0 && (this.showZero || r.percent > 0) && (this.showCompleted || r.percent < 100)); + } + typeClass(value:string){return value === "propädeutikum" ? "type-propaedeutikum" : `type-${value}`;} +} diff --git a/frontend/src/app/pages/entries.component.ts b/frontend/src/app/pages/entries.component.ts new file mode 100644 index 0000000..2d12268 --- /dev/null +++ b/frontend/src/app/pages/entries.component.ts @@ -0,0 +1,331 @@ +import { Component, OnInit, signal } from "@angular/core"; +import { DatePipe, DecimalPipe } from "@angular/common"; +import { RouterLink } from "@angular/router"; +import { FormsModule } from "@angular/forms"; +import { MATERIAL } from "../shared/material"; +import { ApiService } from "../core/api.service"; +import { Dashboard, Entry } from "../core/models"; +@Component({ + standalone: true, + imports: [RouterLink, FormsModule, DatePipe, DecimalPipe, ...MATERIAL], + template: `
+
+
+

Einträge

+

Stunden, Kosten und Fahrten verwalten

+
+
+ add Neuer Eintrag +
+
+ + + @if (running(); as timer) { +
timerTimer läuft{{ elapsed(timer) }}{{ timer.praktikums_typ }} · {{ timer.entry_art }}
+ 30 Minuten Pause abziehen + + } @else { +
timerZeiterfassungTimer starten
+ Ausbildung@for(t of types;track t){{{t}}} + Art@for(a of timerArts();track a){{{a}}} + + } +
+
+ SuchesearchAusbildungAlle + @for (t of types; track t) { + {{ t }} + } + Von JahrAlle@for(y of years();track y){{{y}}}Bis JahrAlle@for(y of years();track y){{{y}}} + @if (loading()) { + + } +
+ + + + + + + + + +
Datum + {{ e.date | date: "dd.MM.yyyy" }} + Ausbildung + {{ e.praktikums_typ }} + Art{{e.entry_art}}Zeit + {{ e.hours }} h {{ e.minutes }} min + Details + {{ e.beschreibung || "–" }} + Kilometer{{e.distance_km || 0 | number:"1.0-2"}} kmPauschale{{e.kilometer_pauschale || 0 | number:"1.2-2"}} €Kosten{{e.kosten || 0 | number:"1.2-2"}} €Fortbildung{{e.zaehlt_als_fortbildung ? "check_circle" : "remove"}} +
+ edit +
+
+ @if (!loading() && !entries().length) { +
+ event_busy +

Noch keine passenden Einträge vorhanden.

+
+ } +
Angezeigt: {{visibleEntries().length}} · Gesamt: {{entries().length}}
+
`, + styles: [ + ` + .filters { + margin-bottom: 20px; + } + .entries-page { + max-width: 1680px; + } + .timer-card { margin-bottom: 20px; } + .timer-card mat-card-content { display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap; } + .timer-card mat-card-content > div { display:grid;grid-template-columns:auto 1fr;gap:3px 10px;margin-right:auto; } + .timer-card mat-card-content > div > mat-icon { + grid-row:1/4; + align-self:center; + display:grid; + place-items:center; + width:40px; + height:40px; + border-radius:12px; + color:var(--app-primary); + background:color-mix(in srgb,var(--app-primary) 13%,transparent); + } + .timer-card button mat-icon { color:inherit; } + .timer-card mat-card-content > button, + .filters mat-card-content > button { + align-self:flex-start; + min-height:48px; + margin-top:4px; + } + .timer-start mat-icon, .timer-stop mat-icon { opacity:1; } + .timer-card strong { font-size:1.25rem; } + .filters mat-card-content { + display: flex; + gap: 12px; + align-items: flex-start; + flex-wrap: wrap; + } + .filters mat-form-field { + min-width: 220px; + } + .table-card { + padding: 0; + overflow: hidden; + } + .entry-count { padding:12px 18px;color:#687287;border-top:1px solid rgba(120,130,145,.2);font-size:.85rem; } + .training-state { color:var(--app-muted);vertical-align:middle; } + .training-state.is-active { color:#198754; } + .mat-column-actions { width:108px;min-width:108px;white-space:nowrap; } + .mat-column-time { width:128px;min-width:128px;white-space:nowrap; } + .entry-actions { display:flex;align-items:center;justify-content:flex-end;gap:8px;white-space:nowrap; } + .entry-actions .mat-mdc-icon-button { flex:0 0 40px;width:40px;height:40px;padding:8px;border-radius:10px; } + .edit-action { color:#0d6efd;background:rgba(13,110,253,.11); } + .delete-action { color:#c62828;background:rgba(198,40,40,.11); } + .edit-action:hover { background:rgba(13,110,253,.19); } + .delete-action:hover { background:rgba(198,40,40,.19); } + td strong, + td small { + display: block; + } + td small { + color: #687287; + margin-top: 4px; + } + th { + font-weight: 700; + } + tr.mat-mdc-row > td:first-child { + border-left: 4px solid transparent; + } + tr.mat-mdc-row.type-propaedeutikum:nth-of-type(odd) > td { + background-color: rgba(13, 110, 253, 0.055); + } + tr.mat-mdc-row.type-propaedeutikum:nth-of-type(even) > td { + background-color: rgba(13, 110, 253, 0.115); + } + tr.mat-mdc-row.type-fachspezifikum:nth-of-type(odd) > td { + background-color: rgba(25, 135, 84, 0.055); + } + tr.mat-mdc-row.type-fachspezifikum:nth-of-type(even) > td { + background-color: rgba(25, 135, 84, 0.115); + } + tr.mat-mdc-row.type-mediation:nth-of-type(odd) > td { + background-color: rgba(253, 126, 20, 0.065); + } + tr.mat-mdc-row.type-mediation:nth-of-type(even) > td { + background-color: rgba(253, 126, 20, 0.135); + } + tr.mat-mdc-row.type-propaedeutikum:hover > td { + background-color: rgba(13, 110, 253, 0.17); + } + tr.mat-mdc-row.type-fachspezifikum:hover > td { + background-color: rgba(25, 135, 84, 0.17); + } + tr.mat-mdc-row.type-mediation:hover > td { + background-color: rgba(253, 126, 20, 0.19); + } + tr.mat-mdc-row.type-propaedeutikum > td:first-child { border-left-color: #0d6efd; } + tr.mat-mdc-row.type-fachspezifikum > td:first-child { border-left-color: #198754; } + tr.mat-mdc-row.type-mediation > td:first-child { border-left-color: #fd7e14; } + tr.mat-mdc-row.entry-today { + font-weight: 700; + } + tr.mat-mdc-row.entry-today > td { + box-shadow: inset 0 3px 0 #7c3aed, inset 0 -3px 0 #7c3aed; + } + tr.mat-mdc-row.entry-today > td:first-child { + border-left:6px solid #7c3aed; + } + tr.mat-mdc-row.entry-today > td:last-child { + border-right:3px solid #7c3aed; + } + :host-context(body.dark) tr.mat-mdc-row.type-propaedeutikum:nth-of-type(odd) > td { background-color: rgba(62, 139, 255, 0.12); } + :host-context(body.dark) tr.mat-mdc-row.type-propaedeutikum:nth-of-type(even) > td { background-color: rgba(62, 139, 255, 0.18); } + :host-context(body.dark) tr.mat-mdc-row.type-fachspezifikum:nth-of-type(odd) > td { background-color: rgba(53, 184, 116, 0.12); } + :host-context(body.dark) tr.mat-mdc-row.type-fachspezifikum:nth-of-type(even) > td { background-color: rgba(53, 184, 116, 0.18); } + :host-context(body.dark) tr.mat-mdc-row.type-mediation:nth-of-type(odd) > td { background-color: rgba(255, 143, 51, 0.13); } + :host-context(body.dark) tr.mat-mdc-row.type-mediation:nth-of-type(even) > td { background-color: rgba(255, 143, 51, 0.2); } + :host-context(body.dark) tr.mat-mdc-row.type-propaedeutikum:hover > td { background-color: rgba(62, 139, 255, 0.25); } + :host-context(body.dark) tr.mat-mdc-row.type-fachspezifikum:hover > td { background-color: rgba(53, 184, 116, 0.25); } + :host-context(body.dark) tr.mat-mdc-row.type-mediation:hover > td { background-color: rgba(255, 143, 51, 0.27); } + :host-context(body.dark) tr.mat-mdc-row.entry-today > td { + box-shadow: inset 0 3px 0 #b794f4, inset 0 -3px 0 #b794f4; + } + :host-context(body.dark) tr.mat-mdc-row.entry-today > td:first-child { border-left-color:#b794f4; } + :host-context(body.dark) tr.mat-mdc-row.entry-today > td:last-child { border-right-color:#b794f4; } + :host-context(body.dark) .edit-action { color:#8bb7ff;background:rgba(62,139,255,.2); } + :host-context(body.dark) .delete-action { color:#ff9b9b;background:rgba(255,90,90,.18); } + @media (max-width: 700px) { + .filters mat-form-field { + width: 100%; + } + .mat-column-details { + display: none; + } + } + `, + ], +}) +export class EntriesComponent implements OnInit { + readonly entries = signal([]); + readonly loading = signal(false); + readonly columns = ["date", "time", "type", "art", "details", "distance", "allowance", "cost", "training", "actions"]; + readonly types = ["propädeutikum", "fachspezifikum", "mediation"]; + readonly arts: Record = {propädeutikum:["Praktikum","Selbsterfahrung","Supervision","Fortbildung","Semesterkosten"],fachspezifikum:["Praktikum","Selbsterfahrung","Supervision","Fortbildung","Semesterkosten","Gruppenselbsterfahrung","Theorie/Methodikseminare","Peergruppensupervision","Einzel-/Kleingruppensupervision","Eigenständige Tätigkeit"],mediation:["Präsenzmodul","Selbsterfahrung","Supervision","Peergruppenarbeit","Fallarbeit","Praxisseminare","Literatur- und Selbststudium","Fortbildung","Semesterkosten"]}; + readonly running=signal(null); + readonly clock=signal(Date.now()); + timerType="propädeutikum"; timerArt="Praktikum"; lunchBreak=false; + search = ""; + typ = ""; + minYear: number | "" = ""; + maxYear: number | "" = ""; + constructor(private api: ApiService) {} + ngOnInit() { + this.load(); + setInterval(()=>this.clock.set(Date.now()),30_000); + } + load() { + this.loading.set(true); + this.api.entries({ search: this.search, typ: this.typ }).subscribe({ + next: (v) => { + this.entries.set(v); + this.loading.set(false); + }, + error: () => this.loading.set(false), + }); + this.api.dashboard().subscribe((d:Dashboard)=>this.running.set(d.running_entry)); + } + timerArts(){const available=this.arts[this.timerType]||[];if(!available.includes(this.timerArt))this.timerArt=available[0];return available;} + startTimer(){this.api.startTimer(this.timerType,this.timerArt).subscribe(e=>{this.running.set(e);this.load();});} + stopTimer(entry:Entry){this.api.stopTimer(entry.id,this.lunchBreak).subscribe(()=>{this.running.set(null);this.lunchBreak=false;this.load();});} + elapsed(entry:Entry){this.clock();if(!entry.start_time)return "0 h 0 min";const minutes=Math.max(0,Math.floor((Date.now()-new Date(entry.start_time).getTime())/60000));return `${Math.floor(minutes/60)} h ${minutes%60} min`;} + reset() { + this.search = ""; + this.typ = ""; + this.minYear = ""; + this.maxYear = ""; + this.load(); + } + years(){return [...new Set(this.entries().map(e=>Number(e.date.slice(0,4))))].sort((a,b)=>b-a);} + visibleEntries(){return this.entries().filter(e=>{const year=Number(e.date.slice(0,4));return(!this.minYear||year>=this.minYear)&&(!this.maxYear||year<=this.maxYear);});} + entryRowClass(entry: Entry): string { + const normalized = entry.praktikums_typ + ?.toLocaleLowerCase("de-AT") + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, ""); + const type = normalized === "propadeutikum" + ? "propaedeutikum" + : normalized?.replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); + const classes = [`type-${type || "unknown"}`]; + const now = new Date(); + const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`; + if (entry.date === today) { + classes.push("entry-today"); + } + return classes.join(" "); + } + remove(e: Entry) { + if (confirm(`Eintrag vom ${e.date} wirklich löschen?`)) + this.api.deleteEntry(e.id).subscribe(() => this.load()); + } + download() { + this.api.exportCsv().subscribe((blob) => { + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = `eintraege-${new Date().toISOString().slice(0, 10)}.csv`; + a.click(); + URL.revokeObjectURL(a.href); + }); + } +} diff --git a/frontend/src/app/pages/entry-form.component.ts b/frontend/src/app/pages/entry-form.component.ts new file mode 100644 index 0000000..cbd4005 --- /dev/null +++ b/frontend/src/app/pages/entry-form.component.ts @@ -0,0 +1,346 @@ +import { Component, inject, OnInit, signal } from "@angular/core"; +import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; +import { ActivatedRoute, Router, RouterLink } from "@angular/router"; +import { MatSnackBar } from "@angular/material/snack-bar"; +import { + DateAdapter, + MAT_DATE_FORMATS, + MAT_DATE_LOCALE, +} from "@angular/material/core"; +import { MatTimepickerModule } from "@angular/material/timepicker"; +import { MATERIAL } from "../shared/material"; +import { ApiService } from "../core/api.service"; +import { + AUSTRIAN_DATE_FORMATS, + AustrianDateAdapter, +} from "../core/austrian-date-adapter"; +@Component({ + standalone: true, + imports: [ReactiveFormsModule, RouterLink, MatTimepickerModule, ...MATERIAL], + providers: [ + { provide: MAT_DATE_LOCALE, useValue: "de-AT" }, + { provide: MAT_DATE_FORMATS, useValue: AUSTRIAN_DATE_FORMATS }, + { provide: DateAdapter, useClass: AustrianDateAdapter }, + ], + template: `
+
+
+

{{ id ? "Eintrag bearbeiten" : "Neuer Eintrag" }}

+

Zeit, Ausbildungsbereich und Kosten erfassen

+
+
+
+ ZeitDatumBeginnEndeStundenMinuten30 Minuten Mittagspause abziehenZuordnungAusbildung + @for (t of types; track t) { + {{ t }} + } + Art + @for (a of availableArts(); track a) { + {{ a }} + } + BeschreibungEntfernungkmKosten€Zählt als Fortbildung + +
+
`, + styles: [ + ` + .narrow { + max-width: 980px; + } + .form-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px 18px; + padding-top: 20px !important; + } + .wide { + grid-column: 1/-1; + } + mat-card { + margin-bottom: 20px; + } + .footer { + justify-content: flex-end; + } + @media (max-width: 650px) { + .form-grid { + grid-template-columns: 1fr; + } + .wide { + grid-column: auto; + } + } + `, + ], +}) +export class EntryFormComponent implements OnInit { + private readonly fb = inject(FormBuilder); + id?: number; + readonly saving = signal(false); + readonly types = ["propädeutikum", "fachspezifikum", "mediation"]; + readonly arts: Record = { + propädeutikum: [ + "Praktikum", + "Selbsterfahrung", + "Supervision", + "Fortbildung", + "Semesterkosten", + ], + fachspezifikum: [ + "Praktikum", + "Selbsterfahrung", + "Supervision", + "Fortbildung", + "Semesterkosten", + "Gruppenselbsterfahrung", + "Theorie/Methodikseminare", + "Peergruppensupervision", + "Einzel-/Kleingruppensupervision", + "Eigenständige Tätigkeit", + ], + mediation: [ + "Präsenzmodul", + "Selbsterfahrung", + "Supervision", + "Peergruppenarbeit", + "Fallarbeit", + "Praxisseminare", + "Literatur- und Selbststudium", + "Fortbildung", + "Semesterkosten", + ], + }; + readonly form = this.fb.nonNullable.group({ + date: [this.today(), Validators.required], + start_time: [this.currentQuarter() as Date | null], + end_time: [null as Date | null], + hours: [0, [Validators.required, Validators.min(0)]], + minutes: [0, [Validators.required, Validators.min(0), Validators.max(59)]], + break: [false], + praktikums_typ: ["propädeutikum", Validators.required], + entry_art: ["Praktikum", Validators.required], + distance_km: [0, Validators.min(0)], + beschreibung: [""], + kosten: [null as number | null], + zaehlt_als_fortbildung: [false], + }); + constructor( + private api: ApiService, + private route: ActivatedRoute, + private router: Router, + private snack: MatSnackBar, + ) {} + ngOnInit() { + const raw = this.route.snapshot.paramMap.get("id"); + const requestedDate = this.route.snapshot.queryParamMap.get("date"); + if (requestedDate) { + const date = this.parseDate(requestedDate); + if (date) this.form.controls.date.setValue(date); + } + if (raw) { + this.id = Number(raw); + this.api + .entry(this.id) + .subscribe((e) => + this.form.patchValue({ + ...e, + date: this.parseDate(e.date) ?? this.today(), + beschreibung: e.beschreibung ?? "", + start_time: this.parseTime(e.start_time), + end_time: this.parseTime(e.end_time), + break: e.lunch_break_minutes === 30, + }), + ); + } + } + availableArts() { + return this.arts[this.form.controls.praktikums_typ.value] || []; + } + typeChanged() { + const arts = this.availableArts(); + if (!arts.includes(this.form.controls.entry_art.value)) + this.form.controls.entry_art.setValue(arts[0]); + } + calculateTime() { + const { start_time: s, end_time: e, break: b } = this.form.getRawValue(); + if (!s || !e) return; + let mins = e.getHours() * 60 + e.getMinutes() - + (s.getHours() * 60 + s.getMinutes()); + if (mins < 0) mins += 1440; + if (b) mins = Math.max(0, mins - 30); + this.form.patchValue({ hours: Math.floor(mins / 60), minutes: mins % 60 }); + } + save() { + if (this.form.invalid) return; + this.saving.set(true); + const { + break: hasBreak, + date, + start_time, + end_time, + ...rest + } = this.form.getRawValue(); + const value = { + ...rest, + date: this.formatDate(date), + start_time: this.formatTime(start_time), + end_time: this.formatTime(end_time), + }; + this.api + .saveEntry({ ...value, lunch_break_minutes: hasBreak ? 30 : 0 }, this.id) + .subscribe({ + next: () => { + this.snack.open("Eintrag gespeichert", "OK", { duration: 2500 }); + this.router.navigate(["/entries"]); + }, + error: (e) => { + this.saving.set(false); + this.snack.open( + e.error?.error || + e.error?.errors?.join(", ") || + "Speichern fehlgeschlagen", + "OK", + ); + }, + }); + } + private today() { + const now = new Date(); + return new Date(now.getFullYear(), now.getMonth(), now.getDate()); + } + private currentQuarter() { + const now = new Date(); + now.setMinutes(Math.floor(now.getMinutes() / 15) * 15, 0, 0); + return now; + } + private parseDate(value: string | null) { + if (!value) return null; + const [year, month, day] = value.slice(0, 10).split("-").map(Number); + const date = new Date(year, month - 1, day); + return Number.isNaN(date.getTime()) ? null : date; + } + private parseTime(value: string | null) { + if (!value) return null; + const parsed = new Date(value); + if (!Number.isNaN(parsed.getTime())) return parsed; + const match = value.match(/^(\d{1,2}):(\d{2})/); + if (!match) return null; + const date = this.today(); + date.setHours(Number(match[1]), Number(match[2]), 0, 0); + return date; + } + private formatDate(value: Date) { + return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, "0")}-${String(value.getDate()).padStart(2, "0")}`; + } + private formatTime(value: Date | null) { + return value + ? `${String(value.getHours()).padStart(2, "0")}:${String(value.getMinutes()).padStart(2, "0")}` + : null; + } +} diff --git a/frontend/src/app/pages/forgot-password.component.ts b/frontend/src/app/pages/forgot-password.component.ts new file mode 100644 index 0000000..b498ffa --- /dev/null +++ b/frontend/src/app/pages/forgot-password.component.ts @@ -0,0 +1,3 @@ +import { Component, inject, signal } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { RouterLink } from '@angular/router'; import { ApiService } from '../core/api.service'; import { MATERIAL } from '../shared/material'; import { authStyles } from './register.component'; +@Component({standalone:true,imports:[ReactiveFormsModule,RouterLink,...MATERIAL],template:`
lock_reset

Passwort zurücksetzen

Wir senden dir einen Link zum Festlegen eines neuen Passworts.

@if(message()){
{{message()}}
} @else {
E-Mail
}Zur Anmeldung
`,styles:[authStyles()]}) +export class ForgotPasswordComponent{private fb=inject(FormBuilder);loading=signal(false);message=signal('');form=this.fb.nonNullable.group({email:['',[Validators.required,Validators.email]]});constructor(private api:ApiService){}submit(){this.loading.set(true);this.api.forgotPassword(this.form.getRawValue().email).subscribe(r=>this.message.set(r.message));}} diff --git a/frontend/src/app/pages/impressum.component.ts b/frontend/src/app/pages/impressum.component.ts new file mode 100644 index 0000000..467c27f --- /dev/null +++ b/frontend/src/app/pages/impressum.component.ts @@ -0,0 +1,2 @@ +import { Component } from '@angular/core'; import { RouterLink } from '@angular/router'; import { MATERIAL } from '../shared/material'; +@Component({standalone:true,imports:[RouterLink,...MATERIAL],template:`

Impressum

Informationen gemäß § 5 ECG und § 25 MedienG.

Christoph Marzell
Furth an der Triesting, Österreich

Kontakt: christoph@marzell.net

Zurück
`})export class ImpressumComponent{} diff --git a/frontend/src/app/pages/login.component.ts b/frontend/src/app/pages/login.component.ts new file mode 100644 index 0000000..132c812 --- /dev/null +++ b/frontend/src/app/pages/login.component.ts @@ -0,0 +1,144 @@ +import { Component, inject, signal } from "@angular/core"; +import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; +import { Router, RouterLink } from "@angular/router"; +import { MATERIAL } from "../shared/material"; +import { ApiService } from "../core/api.service"; +@Component({ + standalone: true, + imports: [ReactiveFormsModule, RouterLink, ...MATERIAL], + template: `
+ +

Willkommen zurück

+

Ausbildungsstunden übersichtlich dokumentieren.

+
+ E-MailmailPasswortlock + @if (error()) { +

{{ error() }}

+ } + + + + +
+
`, + styles: [ + ` + .login { + min-height: 100%; + display: grid; + place-items: center; + padding: 20px; + background: + radial-gradient(circle at top left, #d9e9ff, transparent 45%), #f5f7fb; + } + .login mat-card { + width: min(430px, 100%); + padding: 24px; + border-radius: 24px !important; + } + .logo { + width: 54px; + height: 54px; + border-radius: 16px; + display: grid; + place-items: center; + background: #155bd7; + color: #fff; + font-size: 25px; + font-weight: 800; + } + h1 { + margin: 22px 0 6px; + } + .full { + margin-top: 10px; + } + .error { + color: #b3261e; + } + button.full { + height: 48px; + } + .auth-links { + display: flex; + justify-content: space-between; + gap: 16px; + margin-top: 20px; + } + .auth-links a { + color: #155bd7; + text-decoration: none; + font-weight: 500; + } + .legal { + text-align: center; + margin-top: 18px; + } + .legal a { + color: #687287; + text-decoration: none; + font-size: 13px; + } + `, + ], +}) +export class LoginComponent { + private readonly fb = inject(FormBuilder); + readonly loading = signal(false); + readonly error = signal(""); + readonly form = this.fb.nonNullable.group({ + email: ["", [Validators.required, Validators.email]], + password: ["", Validators.required], + }); + constructor( + private api: ApiService, + private router: Router, + ) {} + submit() { + if (this.form.invalid) return; + this.loading.set(true); + this.error.set(""); + this.api + .login(this.form.value.email!, this.form.value.password!) + .subscribe({ + next: () => this.router.navigate(["/dashboard"]), + error: (e) => { + this.loading.set(false); + this.error.set(e.error?.error || "Anmeldung fehlgeschlagen"); + }, + }); + } +} diff --git a/frontend/src/app/pages/mileage-rates.component.ts b/frontend/src/app/pages/mileage-rates.component.ts new file mode 100644 index 0000000..5b21b33 --- /dev/null +++ b/frontend/src/app/pages/mileage-rates.component.ts @@ -0,0 +1,95 @@ +import { Component, OnInit, signal } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { MATERIAL } from "../shared/material"; +import { ApiService } from "../core/api.service"; +import { MileageRate } from "../core/models"; +@Component({ + standalone: true, + imports: [FormsModule, ...MATERIAL], + template: `
+
+
+

Kilometersätze

+

Kilometergeld je Kalenderjahr verwalten

+
+
+ JahrSatz pro km€ +
+ @for (rate of rates(); track rate.id) { + {{ rate.year }}{{ rate.rate_per_km }} €/km + } +
+
`, + styles: [ + ` + .narrow { + max-width: 900px; + } + .form { + display: flex; + gap: 14px; + align-items: center; + flex-wrap: wrap; + } + .cards { + margin-top: 20px; + } + .cards mat-card-content { + display: flex; + flex-direction: column; + gap: 8px; + } + .cards strong { + font-size: 1.5rem; + } + `, + ], +}) +export class MileageRatesComponent implements OnInit { + rates = signal([]); + draft: Partial = { + year: new Date().getFullYear(), + rate_per_km: 0.5, + }; + constructor(private api: ApiService) {} + ngOnInit() { + this.load(); + } + load() { + this.api.mileageRates().subscribe((v) => this.rates.set(v)); + } + save() { + this.api.saveMileageRate(this.draft).subscribe(() => { + this.draft = { year: new Date().getFullYear(), rate_per_km: 0.5 }; + this.load(); + }); + } +} diff --git a/frontend/src/app/pages/register.component.ts b/frontend/src/app/pages/register.component.ts new file mode 100644 index 0000000..648451d --- /dev/null +++ b/frontend/src/app/pages/register.component.ts @@ -0,0 +1,9 @@ +import { Component, inject, signal } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import { ApiService } from '../core/api.service'; +import { MATERIAL } from '../shared/material'; + +@Component({standalone:true,imports:[ReactiveFormsModule,RouterLink,...MATERIAL],template:`
person_add

Konto registrieren

Nach der Registrierung erhältst du einen Bestätigungslink per E-Mail.

@if(message()){
{{message()}}
} @else {
E-MailPasswortPasswort wiederholen@if(error()){

{{error()}}

}
}Zur Anmeldung
`,styles:[authStyles()]}) +export class RegisterComponent{private fb=inject(FormBuilder);loading=signal(false);error=signal('');message=signal('');form=this.fb.nonNullable.group({email:['',[Validators.required,Validators.email]],password:['',[Validators.required,Validators.minLength(6)]],password_confirmation:['',Validators.required]});constructor(private api:ApiService){}submit(){const v=this.form.getRawValue();if(v.password!==v.password_confirmation){this.error.set('Die Passwörter stimmen nicht überein.');return;}this.loading.set(true);this.api.register(v.email,v.password,v.password_confirmation).subscribe({next:r=>this.message.set(r.message),error:e=>{this.loading.set(false);this.error.set(e.error?.errors?.join(', ')||'Registrierung fehlgeschlagen');}});}} +export function authStyles(){return `.auth-page{min-height:100%;display:grid;place-items:center;padding:20px;background:#f4f7fb}.auth-page mat-card{width:min(460px,100%);padding:24px;border-radius:24px!important}.hero-icon{font-size:38px;width:42px;height:42px;color:#155bd7}.full{width:100%;margin-top:10px}button.full{height:48px}.error{color:#b3261e}.success{padding:16px;border-radius:12px;background:#dff6e7;color:#125c2d;margin:18px 0}h1{margin-bottom:6px}`;} diff --git a/frontend/src/app/pages/reports.component.ts b/frontend/src/app/pages/reports.component.ts new file mode 100644 index 0000000..82e06e0 --- /dev/null +++ b/frontend/src/app/pages/reports.component.ts @@ -0,0 +1,109 @@ +import { Component, OnInit, signal } from "@angular/core"; +import { DatePipe } from "@angular/common"; +import { MATERIAL } from "../shared/material"; +import { ApiService } from "../core/api.service"; + +interface Row { + month: string; + typ: string; + art: string; + total_minutes: number; +} + +@Component({ + standalone: true, + imports: [DatePipe, ...MATERIAL], + template: `
+
+
+

Monatsbericht

+

Geleistete Zeiten nach Monat und Kategorie

+
+
+ @if (loading()) { } +
+ @for (group of groups(); track group.month) { + + +
+
+ {{ group.month | date: "MMMM yyyy" }} + {{ duration(group.total) }} +
+ @if (isCurrentMonth(group.month)) { + star Aktueller Monat + } +
+
+ + @for (row of group.rows; track row.typ + row.art) { +
+
{{ row.art }}{{ row.typ }}
+ {{ duration(row.total_minutes) }} +
+ } +
+
+ } +
+
`, + styles: [` + .report{grid-template-columns:repeat(auto-fit,minmax(340px,1fr))} + .month-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;width:100%} + mat-card.current-month{border:2px solid var(--app-primary);background:color-mix(in srgb,var(--app-primary) 5%,var(--app-surface));box-shadow:0 12px 32px color-mix(in srgb,var(--app-primary) 16%,transparent)} + .current-month mat-card-header{background:color-mix(in srgb,var(--app-primary) 10%,transparent);border-radius:14px 14px 0 0;padding-top:16px;padding-bottom:14px} + .current-badge{display:inline-flex;align-items:center;gap:5px;white-space:nowrap;border-radius:999px;padding:5px 10px;background:var(--app-primary);color:white;font-size:.78rem;font-weight:700} + .current-badge mat-icon{width:16px;height:16px;font-size:16px} + .row{display:flex;justify-content:space-between;align-items:center;margin:4px 0;padding:11px 12px;border-bottom:1px solid var(--app-border);border-left:4px solid transparent;border-radius:8px} + .row.type-propaedeutikum{background:rgba(13,110,253,.08);border-left-color:#0d6efd} + .row.type-fachspezifikum{background:rgba(25,135,84,.08);border-left-color:#198754} + .row.type-mediation{background:rgba(253,126,20,.10);border-left-color:#fd7e14} + :host-context(body.dark) .row.type-propaedeutikum{background:rgba(62,139,255,.15)} + :host-context(body.dark) .row.type-fachspezifikum{background:rgba(53,184,116,.15)} + :host-context(body.dark) .row.type-mediation{background:rgba(255,143,51,.17)} + .row:last-child{border-bottom:0}.row div{display:flex;flex-direction:column}.row small{margin-top:3px}mat-card-content{padding-top:12px!important} + @media(max-width:500px){.report{grid-template-columns:1fr}.month-heading{flex-direction:column}.current-badge{align-self:flex-start}} + `], +}) +export class ReportsComponent implements OnInit { + readonly rows = signal([]); + readonly loading = signal(true); + + constructor(private api: ApiService) {} + + ngOnInit() { + this.api.monthlyReport().subscribe({ + next: (rows) => { this.rows.set(rows); this.loading.set(false); }, + error: () => this.loading.set(false), + }); + } + + groups() { + const map = new Map(); + for (const row of this.rows()) map.set(row.month, [...(map.get(row.month) || []), row]); + return [...map].map(([month, rows]) => ({ + month, + rows, + total: rows.reduce((sum, row) => sum + row.total_minutes, 0), + })); + } + + isCurrentMonth(month: string) { + const now = new Date(); + return month.slice(0, 7) === `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`; + } + + typeClass(type: string) { + const normalized = type + ?.toLocaleLowerCase("de-AT") + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, ""); + return `type-${normalized === "propadeutikum" ? "propaedeutikum" : normalized || "unknown"}`; + } + + duration(minutes: number) { + return `${Math.floor(minutes / 60)} h ${minutes % 60} min`; + } +} diff --git a/frontend/src/app/pages/resend-confirmation.component.ts b/frontend/src/app/pages/resend-confirmation.component.ts new file mode 100644 index 0000000..2d9e600 --- /dev/null +++ b/frontend/src/app/pages/resend-confirmation.component.ts @@ -0,0 +1,3 @@ +import { Component, inject, signal } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { RouterLink } from '@angular/router'; import { ApiService } from '../core/api.service'; import { MATERIAL } from '../shared/material'; import { authStyles } from './register.component'; +@Component({standalone:true,imports:[ReactiveFormsModule,RouterLink,...MATERIAL],template:`
forward_to_inbox

Bestätigungslink erneut senden

@if(message()){
{{message()}}
} @else {
E-Mail
}Zur Anmeldung
`,styles:[authStyles()]}) +export class ResendConfirmationComponent{private fb=inject(FormBuilder);loading=signal(false);message=signal('');form=this.fb.nonNullable.group({email:['',[Validators.required,Validators.email]]});constructor(private api:ApiService){}submit(){this.loading.set(true);this.api.resendConfirmation(this.form.getRawValue().email).subscribe(r=>this.message.set(r.message));}} diff --git a/frontend/src/app/pages/reset-password.component.ts b/frontend/src/app/pages/reset-password.component.ts new file mode 100644 index 0000000..e73706e --- /dev/null +++ b/frontend/src/app/pages/reset-password.component.ts @@ -0,0 +1,3 @@ +import { Component, inject, signal } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { ActivatedRoute, RouterLink } from '@angular/router'; import { ApiService } from '../core/api.service'; import { MATERIAL } from '../shared/material'; import { authStyles } from './register.component'; +@Component({standalone:true,imports:[ReactiveFormsModule,RouterLink,...MATERIAL],template:`
password

Neues Passwort

@if(message()){
{{message()}}
} @else {
Neues PasswortPasswort wiederholen@if(error()){

{{error()}}

}
}Zur Anmeldung
`,styles:[authStyles()]}) +export class ResetPasswordComponent{private fb=inject(FormBuilder);token=inject(ActivatedRoute).snapshot.queryParamMap.get('token')||'';loading=signal(false);error=signal('');message=signal('');form=this.fb.nonNullable.group({password:['',[Validators.required,Validators.minLength(6)]],password_confirmation:['',Validators.required]});constructor(private api:ApiService){}submit(){const v=this.form.getRawValue();if(!this.token){this.error.set('Der Reset-Link ist ungültig.');return;}if(v.password!==v.password_confirmation){this.error.set('Die Passwörter stimmen nicht überein.');return;}this.loading.set(true);this.api.resetPassword(this.token,v.password,v.password_confirmation).subscribe({next:r=>this.message.set(r.message),error:e=>{this.loading.set(false);this.error.set(e.error?.errors?.join(', ')||'Passwort konnte nicht geändert werden');}});}} diff --git a/frontend/src/app/pages/settings.component.ts b/frontend/src/app/pages/settings.component.ts new file mode 100644 index 0000000..1ebc829 --- /dev/null +++ b/frontend/src/app/pages/settings.component.ts @@ -0,0 +1,222 @@ +import { Component, OnInit, signal } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { MatSnackBar } from "@angular/material/snack-bar"; +import { MATERIAL } from "../shared/material"; +import { ApiService } from "../core/api.service"; +import { Settings } from "../core/models"; +@Component({ + standalone: true, + imports: [FormsModule, ...MATERIAL], + template: `
+
+
+

Einstellungen

+

Persönliche Ausbildungsziele anpassen

+
+
+ @if (loading()) { + + } @else if (settings(); as s) { + AusbildungsstatusPropädeutikum abgeschlossen +

+ Nach dem Abschluss können keine neuen Propädeutikums-Einträge + angelegt werden. +

+ ProfilE-Mail-Adresse +

+ Bei einer Änderung muss die neue Adresse erneut bestätigt werden. +

+ @for (typ of s.praktikums_typen; track typ) { + {{ label(typ) }}Sollstunden und durchschnittliches Wochenziel
+ @for (art of s.entry_arten_by_typ[typ]; track art) { +
+ {{ art }}Sollhpro Wocheh +
+ } +
+ } + Passwort ändernAktuelles PasswortNeues PasswortNeues Passwort wiederholen + + } +
`, + styles: [ + ` + .narrow { + max-width: 1000px; + } + mat-card { + margin-bottom: 20px; + } + mat-card-content { + padding-top: 18px !important; + } + .targets > div { + display: grid; + grid-template-columns: minmax(220px, 1fr) 170px 170px; + gap: 14px; + align-items: center; + } + .targets mat-form-field { + margin-top: 8px; + } + .footer { + justify-content: flex-end; + position: sticky; + bottom: 12px; + } + .password-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 14px; + align-items: center; + } + @media (max-width: 700px) { + .targets > div { + grid-template-columns: 1fr 1fr; + } + .targets > div > span { + grid-column: 1/-1; + font-weight: 500; + } + .password-grid { + grid-template-columns: 1fr; + } + } + `, + ], +}) +export class SettingsComponent implements OnInit { + readonly settings = signal(null); + readonly loading = signal(true); + readonly saving = signal(false); + currentPassword = ""; + newPassword = ""; + confirmation = ""; + constructor( + private api: ApiService, + private snack: MatSnackBar, + ) {} + ngOnInit() { + this.api.settings().subscribe({ + next: (s) => { + this.settings.set(s); + this.loading.set(false); + }, + error: () => this.loading.set(false), + }); + } + label(v: string) { + return v.charAt(0).toUpperCase() + v.slice(1); + } + save() { + const s = this.settings(); + if (!s) return; + this.saving.set(true); + this.api.saveSettings(s).subscribe({ + next: (r) => { + this.settings.set(r); + this.saving.set(false); + this.snack.open("Einstellungen gespeichert", "OK", { duration: 2500 }); + }, + error: () => { + this.saving.set(false); + this.snack.open("Speichern fehlgeschlagen", "OK"); + }, + }); + } + changePassword() { + if (this.newPassword !== this.confirmation) { + this.snack.open("Die Passwörter stimmen nicht überein", "OK"); + return; + } + this.api + .changePassword(this.currentPassword, this.newPassword, this.confirmation) + .subscribe({ + next: (r) => { + localStorage.removeItem("praktikum_token"); + location.href = "/login"; + }, + error: (e) => + this.snack.open( + e.error?.error || + e.error?.errors?.join(", ") || + "Passwortänderung fehlgeschlagen", + "OK", + ), + }); + } +} diff --git a/frontend/src/app/shared/material.ts b/frontend/src/app/shared/material.ts new file mode 100644 index 0000000..2f7c16e --- /dev/null +++ b/frontend/src/app/shared/material.ts @@ -0,0 +1,2 @@ +import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatListModule } from '@angular/material/list'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatNativeDateModule } from '@angular/material/core'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatTableModule } from '@angular/material/table'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatDialogModule } from '@angular/material/dialog'; import { MatMenuModule } from '@angular/material/menu'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatDividerModule } from '@angular/material/divider'; import { MatChipsModule } from '@angular/material/chips'; +export const MATERIAL=[MatButtonModule,MatCardModule,MatIconModule,MatToolbarModule,MatSidenavModule,MatListModule,MatFormFieldModule,MatInputModule,MatSelectModule,MatDatepickerModule,MatNativeDateModule,MatCheckboxModule,MatTableModule,MatProgressBarModule,MatProgressSpinnerModule,MatSnackBarModule,MatDialogModule,MatMenuModule,MatTooltipModule,MatDividerModule,MatChipsModule] as const; diff --git a/frontend/src/index.html b/frontend/src/index.html new file mode 100644 index 0000000..2c1e484 --- /dev/null +++ b/frontend/src/index.html @@ -0,0 +1 @@ +Ausbildungsnachweis diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..44b4880 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,10 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; +import { AppComponent } from './app/app.component'; +import { routes } from './app/app.routes'; +import { authInterceptor } from './app/core/auth.interceptor'; + +bootstrapApplication(AppComponent, { providers: [provideRouter(routes), provideHttpClient(withInterceptors([authInterceptor])), provideAnimationsAsync()] }) + .catch(console.error); diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss new file mode 100644 index 0000000..805de3b --- /dev/null +++ b/frontend/src/styles.scss @@ -0,0 +1,80 @@ +@use '@angular/material' as mat; +@import 'material-icons/iconfont/material-icons.css'; + +$font: 'Inter Variable', 'Segoe UI', system-ui, sans-serif; + +html { + @include mat.theme((color: (primary: mat.$azure-palette, tertiary: mat.$cyan-palette), typography: 'Inter Variable', density: 0)); + height: 100%; color-scheme: light; + --app-bg:#f4f7fb; --app-surface:#fff; --app-surface-soft:#f7f9fc; --app-border:#dfe5ef; + --app-text:#172033; --app-muted:#657087; --app-primary:#155bd7; + --app-shadow:0 10px 30px rgba(31,50,81,.08); +} +html, body { + width:100%; + height:100%; + min-height:0; + overflow:hidden; +} +app-root { + display:block; + width:100%; + height:100%; + min-height:0; + overflow:hidden; +} +body { + margin:0; font-family:$font; font-size:15px; line-height:1.5; font-optical-sizing:auto; + font-synthesis:none; text-rendering:optimizeLegibility; -webkit-font-smoothing:antialiased; + background:var(--app-bg); color:var(--app-text); +} +body.dark { + @include mat.theme((color: (primary: mat.$cyan-palette, tertiary: mat.$azure-palette, theme-type: dark), typography: 'Inter Variable')); + color-scheme:dark; --app-bg:#0f1318; --app-surface:#171c22; --app-surface-soft:#1c232b; + --app-border:#2d3742; --app-text:#e8edf4; --app-muted:#9aa8ba; --app-primary:#75a7ff; + --app-shadow:0 12px 34px rgba(0,0,0,.26); background:var(--app-bg); color:var(--app-text); +} +*, *::before, *::after { box-sizing:border-box; } +button, input, textarea, select { font:inherit; } +h1,h2,h3,h4,h5,h6,p { font-family:$font; } +h1,h2,h3,h4,h5,h6 { color:var(--app-text); letter-spacing:-.025em; } +.mat-mdc-button,.mat-mdc-raised-button,.mat-mdc-unelevated-button,.mat-mdc-outlined-button, +.mat-mdc-icon-button,.mat-mdc-list-item,.mat-mdc-form-field,.mat-mdc-input-element,.mat-mdc-select, +.mat-mdc-option,.mat-mdc-table,.mat-mdc-dialog-container,.mat-mdc-card,.mat-mdc-menu-panel, +.mat-toolbar,.mat-mdc-checkbox,.mat-mdc-snack-bar-container,.cdk-overlay-container { font-family:$font !important; } +.page { + max-width:1480px; + min-height:0 !important; + height:auto !important; + margin:0 auto; + padding:30px 32px 48px; +} +.page-head { display:flex; align-items:center; justify-content:space-between; gap:18px; margin-bottom:24px; } +.page-head h1 { margin:0; font-size:clamp(1.7rem,2.5vw,2.25rem); font-weight:720; line-height:1.15; } +.page-head p { margin:7px 0 0; } +.grid { display:grid; gap:20px; } +.cards { grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); } +.full { width:100%; } .muted { color:var(--app-muted) !important; } +.actions { display:flex; flex-wrap:wrap; gap:10px; align-items:center; } +.table-wrap { overflow:auto; border-radius:16px; } table { width:100%; } +mat-card.mat-mdc-card { border:1px solid var(--app-border); border-radius:16px !important; background:var(--app-surface); color:var(--app-text); box-shadow:var(--app-shadow); } +.mat-mdc-card-title { font-weight:680 !important; letter-spacing:-.015em !important; } +.mat-mdc-card-subtitle,td small { color:var(--app-muted) !important; } +.mat-mdc-header-row { background:var(--app-surface-soft) !important; } +.mat-mdc-header-cell { color:var(--app-text) !important; font-weight:680 !important; white-space:nowrap; } +.mat-mdc-cell { color:var(--app-text) !important; border-color:var(--app-border) !important; } +.mat-mdc-row { transition:background-color .15s ease; } +.mat-mdc-form-field { --mdc-outlined-text-field-container-shape:10px; } +.mat-mdc-text-field-wrapper { background:var(--app-surface-soft); } +.mat-mdc-input-element,.mat-mdc-select-value,.mat-mdc-floating-label { color:var(--app-text) !important; } +.mat-mdc-select-arrow { color:var(--app-muted) !important; } +.mat-mdc-button,.mat-mdc-raised-button,.mat-mdc-unelevated-button,.mat-mdc-outlined-button { border-radius:10px !important; letter-spacing:0 !important; font-weight:620 !important; } +.mat-mdc-progress-bar { border-radius:999px; overflow:hidden; } +.empty { padding:48px 20px; text-align:center; color:var(--app-muted); } +body.dark .mat-drawer,body.dark .mat-drawer-content,body.dark .mat-mdc-table { background:var(--app-surface); color:var(--app-text); } +body.dark .mat-mdc-text-field-wrapper { background:var(--app-surface-soft); } +body.dark .cost-year { background:var(--app-surface-soft) !important; } +.shell.mat-drawer-container { height:100dvh; min-height:0; overflow:hidden; } +.shell .mat-drawer-inner-container { overflow:hidden; } +.shell .mat-drawer-content { height:100%; min-height:0; overflow-y:auto; overflow-x:hidden; } +@media(max-width:700px){.page{padding:20px 12px 36px}.page-head{align-items:flex-start;flex-direction:column}.hide-mobile{display:none!important}} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..e7b931f --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1 @@ +{"extends":"./tsconfig.json","compilerOptions":{"outDir":"./out-tsc/app","types":[]},"files":["src/main.ts"],"include":["src/**/*.d.ts"]} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..4313ac1 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/log/.keep b/log/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json deleted file mode 100644 index 7f414a0..0000000 --- a/node_modules/.package-lock.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "praktikum", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "node_modules/controllers": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/controllers/-/controllers-0.0.2.tgz", - "integrity": "sha512-Xhe4m8rr1reyM9jHIxaJQMR/P7ItoXvYhqFATht/5XMWcuONaFbYUNnV7/6WxR58uc+d4OHU9rLuKYK+5KUopw==", - "engines": { - "node": "*" - } - } - } -} diff --git a/node_modules/controllers/.npmignore b/node_modules/controllers/.npmignore deleted file mode 100644 index 8d87b1d..0000000 --- a/node_modules/controllers/.npmignore +++ /dev/null @@ -1 +0,0 @@ -node_modules/* diff --git a/node_modules/controllers/Cakefile b/node_modules/controllers/Cakefile deleted file mode 100644 index 7dc7c5b..0000000 --- a/node_modules/controllers/Cakefile +++ /dev/null @@ -1,21 +0,0 @@ -fs = require 'fs' -{print} = require 'util' -{spawn, exec} = require 'child_process' - -build = (watch, callback) -> - if typeof watch is 'function' - callback = watch - watch = false - options = ['-c', '-o', 'lib', 'src'] - options.unshift '-w' if watch - - coffee = spawn 'coffee', options - coffee.stdout.on 'data', (data) -> print data.toString() - coffee.stderr.on 'data', (data) -> print data.toString() - coffee.on 'exit', (status) -> callback?() if status is 0 - -task 'build', 'Compile CoffeeScript source files', -> - build() - -task 'watch', 'Recompile CoffeeScript source files when modified', -> - build true diff --git a/node_modules/controllers/README.md b/node_modules/controllers/README.md deleted file mode 100644 index eaaf0fb..0000000 --- a/node_modules/controllers/README.md +++ /dev/null @@ -1,127 +0,0 @@ -# Controllers - -A simple mvc framework and route extender for Express. - -### Installation - -```bash -$ npm install controllers -``` - -### Usage - -After setting all your middleware in Express, call the controllers method to initialise. - -``` -express = require 'express' -controllers = require 'controllers' - -app = express.createServer() -app.use(express.static(__dirname + '/public')); - -# Make sure all your app.use statements have been called -controllers app, options -``` - -Your folder system should now look like the following: - -``` -site - |-> controllers - | |-> home.js (or coffee, if you have overwritten the require calls) - | |-> blog.js - |-> views - | -> home - | |-> index.jade (these are your views, use whatever renderer you want) - | |-> welcome.jade - | -> blog - | | -> index.jade - | -> shared - | -> layout.jade -``` - -Controllers are called depending on your routing, and the render call is overwritten to access the folder with the same name as the controller, falling back to the shared folder if needed. - -### Routing - -When routing a controller and action must be defined, controllers extends the routing in Express to allow for default values - -``` -# app.get 'route', defaults, middleware... -app.get '/blogPage', { controller: 'blog', action: 'index' }, middleware -app.get '/:controller?/:action?/:id?', { controller: 'home', action: 'index' }, middleware -``` - -The above routing will route the following paths: - -``` -'/' -> Routes to the controller 'home' and runs the method 'index' -'/blogPage' -> Routes to the controller 'blog' and runs the method 'index' -'/home/welcome/1' -> Routes to the controller 'home' and runs the method 'welcome', with the 'id' param set to 1 -``` -### What does a controller look like? - -The controller actions follow the normal convention of Express, taking the request, response and next arguments: - -``` -module.exports.index = (req, res, next) -> - res.render() - -module.exports.welcome = (req, res, next) -> - id = req.param.id ?? 0 - res.partial { id: id } -``` - -The render does not take an argument as the view for this action is automatically searched for in at 'views/home/index' and if that fails falls back to 'views/shared/index'. - -### Helpers - -There are a number of useful calls available in the controllers and views. - -Controllers: - -``` -req.controller # stores current controller -req.action # stores current action -req.executeController 'controller', 'action', cb # Executes another controller and overwrites the next function with the cb -``` - -Views: - -``` -controller # stores current controller -action # stores current action -getUrl 'controller', 'action', defaultParams, queryParams # returns a url corresponding to the controller/action specified -getUrl 'action', defaultParams, queryParams # same as above but using the current controller -``` - -### Options - -The default options are: - -``` -# If the controller/action is not defined do we throw an exception? -strict: true - -# Overwrite the render/partial calls to use the 'controller/action' breakdown of the views -overwriteRender: true - -# Log when the controllers are loaded and called -log: false - -# Set the root folder for the controllers -root: app.set('controllers') || process.cwd() + '/controllers' - -# Set the share folder in the views, all render/partial calls will fall back to this folder -sharedFolder: 'shared' -``` - -### License - -©2012 Felix Jorkowski and available under the [MIT license](http://www.opensource.org/licenses/mit-license.php): - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/node_modules/controllers/lib/controllers.js b/node_modules/controllers/lib/controllers.js deleted file mode 100644 index b77c8dd..0000000 --- a/node_modules/controllers/lib/controllers.js +++ /dev/null @@ -1,343 +0,0 @@ -(function() { - var Controllers, fs, path; - var __slice = Array.prototype.slice; - - fs = require('fs'); - - path = require('path'); - - module.exports = function(app, options) { - var _ref, _ref2, _ref3, _ref4, _ref5; - if (options == null) options = {}; - if ((_ref = options.strict) == null) options.strict = true; - if ((_ref2 = options.overwriteRender) == null) options.overwriteRender = true; - if ((_ref3 = options.log) == null) options.log = false; - if ((_ref4 = options.root) == null) { - options.root = app.set('controllers') || process.cwd() + '/controllers'; - } - if ((_ref5 = options.sharedFolder) == null) options.sharedFolder = 'shared'; - return new Controllers(app, options); - }; - - Controllers = (function() { - - function Controllers(app, options) { - var originalRoute, self; - this.options = options; - self = this; - this._controllers = {}; - this.executeOnDirectory(this.options.root, function(file) { - var controller, ext, reduced; - ext = path.extname(file); - if (ext === '.js' || ext === '.coffee') { - reduced = file.replace(ext, ''); - controller = path.basename(reduced); - self._controllers[controller] = require(reduced); - if (self.options.log) { - return console.log("Controller '" + controller + "' has been loaded"); - } - } - }); - originalRoute = app.routes._route; - app.routes._route = function() { - var c, callbacks, defaults, defkey, defvalue, holder, key, method, newCallbacks, newRoute, path, result, _i, _j, _len, _len2, _ref; - method = arguments[0], path = arguments[1], defaults = arguments[2], callbacks = 4 <= arguments.length ? __slice.call(arguments, 3) : []; - if ('function' === typeof defaults) { - callbacks.push(defaults); - defaults = null; - } - if (callbacks.length === 0) callbacks.push(function(req, res) {}); - if (defaults == null) defaults = {}; - holder = {}; - for (_i = 0, _len = callbacks.length; _i < _len; _i++) { - c = callbacks[_i]; - newCallbacks = self.overwriteCallback(c, holder); - } - result = originalRoute.call(app.routes, method, path, newCallbacks); - holder.route = newRoute = result.routes[method][result.routes[method].length - 1]; - for (defkey in defaults) { - defvalue = defaults[defkey]; - key = self.getKeyInRoute(defkey, newRoute); - if (key != null) { - key["default"] = defvalue; - } else { - if (defkey === 'controller' || defkey === 'action') { - newRoute[defkey] = defvalue; - } - } - } - _ref = newRoute.keys; - for (_j = 0, _len2 = _ref.length; _j < _len2; _j++) { - key = _ref[_j]; - if (key.name === 'controller' || key.name === 'action') { - newRoute[key.name] = '*'; - } - } - return result; - }; - this.addHelpers(app); - } - - Controllers.prototype.addReqHelpers = function(req, res) { - var self; - self = this; - return req.executeController = function(controller, action, next) { - var currentA, currentC, nextFunc; - if (!(controller != null) || !(action != null)) { - throw new Error("executeController needs the controller and action specified"); - } - if (next != null) { - currentC = req.controller; - currentA = req.action; - nextFunc = next; - next = function() { - req.controller = currentC; - req.action = currentA; - return nextFunc.apply(this, arguments); - }; - } - req.controller = controller; - req.action = action; - return self._controllers[controller][action](req, res, next); - }; - }; - - Controllers.prototype.addHelpers = function(app) { - var self; - self = this; - return app.dynamicHelpers({ - controller: function(req, res) { - return req.controller; - }, - action: function(req, res) { - return req.action; - }, - getUrl: function(req, res) { - return function(controller, action, other, query) { - var def, first, hasReplaced, i, key, regExp, replacement, result, route, value, _i, _len, _ref, _ref2, _ref3, _ref4; - if (!(action != null) || 'object' === typeof action) { - query = other; - other = action; - action = controller; - controller = null; - } - if (controller == null) controller = req.controller; - if (other == null) other = {}; - other.controller = controller; - other.action = action; - if (query == null) query = {}; - if (!(action != null) || !(controller != null)) { - throw new Error("getUrl needs at minimum an action defined, but also takes a controller"); - } - _ref = app.routes.routes.get; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - route = _ref[_i]; - if (self.isMatchingPath(other, route)) { - hasReplaced = false; - result = route.path; - for (i = _ref2 = route.keys.length - 1; _ref2 <= 0 ? i <= 0 : i >= 0; _ref2 <= 0 ? i++ : i--) { - key = route.keys[i]; - def = (_ref3 = key["default"]) != null ? _ref3 : ''; - replacement = (_ref4 = other[key.name]) != null ? _ref4 : def; - if (hasReplaced && replacement === '') { - throw new Error("The optional parameter '" + key.name + "' is required for this getUrl call as an parameter further down the path has been specified"); - } else { - if (!hasReplaced) { - if ((!key.optional || replacement !== def) && (hasReplaced = true)) {} else { - replacement = ''; - } - } - } - regExp = new RegExp(":" + key.name + "(\\?)?"); - result = result.replace(regExp, replacement); - } - result = result.replace(/\/+/g, '/'); - if (result !== '/') result = result.replace(/\/+$/, ''); - first = true; - for (key in query) { - value = query[key]; - if (first) { - first = false; - result = result + '?' + key; - if ((value != null) && value !== '') { - result = result + '=' + value; - } - } else { - result = result + '&' + key; - if ((value != null) && value !== '') { - result = result + '=' + value; - } - } - } - return result; - } - } - throw new Error("Route could not be found that matches getUrl parameters, make sure to specify a valid controller, action and required parameters"); - }; - } - }); - }; - - Controllers.prototype.getKeyInRoute = function(name, route) { - var key, _i, _len, _ref; - _ref = route.keys; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - key = _ref[_i]; - if (key.name === name) return key; - } - return null; - }; - - Controllers.prototype.isMatchingPath = function(object, route) { - var key, value, _i, _len, _ref; - if (route.controller !== '*' && route.controller !== object.controller) { - return false; - } - if (route.action !== '*' && route.action !== object.action) return false; - for (key in object) { - value = object[key]; - if (key !== 'controller' && key !== 'action') { - if (!((this.getKeyInRoute(key, route)) != null)) return false; - } - } - _ref = route.keys; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - key = _ref[_i]; - if (key.name !== 'controller' && key.name !== 'action') { - if (!key.optional && !(object[key] != null)) return false; - } - } - return true; - }; - - Controllers.prototype.overwriteCallback = function(callback, routeHolder) { - var options, self; - self = this; - options = this.options; - return function(req, resp, next) { - var action, controller, key, route, _i, _len, _ref, _ref2, _ref3; - callback(req, resp, next); - self.addReqHelpers(req, resp); - route = routeHolder.route; - _ref = route.keys; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - key = _ref[_i]; - if (!(req.params[key.name] != null) && (key["default"] != null)) { - req.params[key.name] = key["default"]; - } - } - req.controller = (_ref2 = req.params.controller) != null ? _ref2 : route.controller; - req.action = (_ref3 = req.params.action) != null ? _ref3 : route.action; - if (options.log) { - console.log('Controller: ' + req.controller); - console.log('Action: ' + req.action); - } - if (options.strict) { - if (!(req.controller != null)) { - throw new Error("Is in strict mode and no controller specified"); - } - if (!(req.action != null)) { - throw new Error("Is in strict mode and no action specified"); - } - } - if ((req.controller != null) && (req.action != null)) { - if (options.overwriteRender) self.overwriteRender(req, resp); - controller = self._controllers[req.controller]; - if (!(controller != null)) { - if (options.log) { - console.log("Controller '" + req.controller + "' could not be found"); - } - next('route'); - return; - } - action = controller[req.action]; - if (!(action != null)) { - if (options.log) { - console.log("Action '" + req.action + "' could not be found on controller '" + req.controller + "' "); - } - next('route'); - return; - } - return action(req, resp, next); - } else { - if (options.log) { - return console.log('Controller or action was not specified, no action was called'); - } - } - }; - }; - - Controllers.prototype.overwriteRender = function(req, resp) { - var original, root, self; - self = this; - original = resp.render; - root = resp.app.set('views') || process.cwd() + '/views'; - return resp.render = function(view, opts, fn, parent, sub) { - var finalPass, hasHints, reset, result, secondRender, secondResult; - if ('object' === typeof view || 'function' === typeof view) { - sub = parent; - parent = fn; - fn = opts; - opts = view; - view = null; - } - if (view == null) view = req.action; - hasHints = resp.app.enabled('hints'); - resp.app.disable('hints'); - result = null; - secondResult = null; - reset = function() { - if (hasHints) return resp.app.enable('hints'); - }; - finalPass = function(err, err2, str) { - reset(); - if (err != null) err = err + '\r\n\r\n' + err2; - if (fn != null) { - return fn(err, str); - } else { - if (err != null) { - return req.next(err); - } else { - return resp.send(str); - } - } - }; - secondRender = function(err, str) { - if (err != null) { - return secondResult = original.call(resp, self.options.sharedFolder + '/' + view, opts, (function(err2, str2) { - return finalPass(err2, err, str2); - }), parent, sub); - } else { - reset(); - if (fn != null) { - return fn(err, str); - } else { - return resp.send(str); - } - } - }; - result = original.call(resp, req.controller + '/' + view, opts, secondRender, parent, sub); - if (secondResult != null) result = secondResult; - reset(); - return result; - }; - }; - - Controllers.prototype.executeOnDirectory = function(dir, action) { - return fs.readdirSync(dir).forEach(function(file) { - var localpath, stat; - localpath = dir + '/' + file; - stat = fs.statSync(localpath); - if (stat && stat.isDirectory()) { - return self.executeOnDirectory(localpath, action); - } else { - return action(localpath); - } - }); - }; - - return Controllers; - - })(); - -}).call(this); diff --git a/node_modules/controllers/package.json b/node_modules/controllers/package.json deleted file mode 100644 index b0a40bc..0000000 --- a/node_modules/controllers/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "author": "Felix Jorkowski (http://jorkowski.com)", - "name": "controllers", - "description": "A simple mvc framework and route extender for Express", - "version": "0.0.2", - "homepage": "https://github.com/ajorkowski/controllers", - "repository": { - "type": "git", - "url": "git://github.com/ajorkowski/controllers.git" - }, - "main": "lib/controllers.js", - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/node_modules/controllers/src/controllers.coffee b/node_modules/controllers/src/controllers.coffee deleted file mode 100644 index e29d58b..0000000 --- a/node_modules/controllers/src/controllers.coffee +++ /dev/null @@ -1,324 +0,0 @@ -fs = require('fs') -path = require('path') - -module.exports = (app, options = {}) -> - options.strict ?= true - options.overwriteRender ?= true - options.log ?= false - options.root ?= app.set('controllers') || process.cwd() + '/controllers' - options.sharedFolder ?= 'shared' - - new Controllers app, options - -class Controllers - constructor: (app, @options) -> - self = this - @_controllers = {} - - # Pre-load all the controllers... one time hit so done sync - this.executeOnDirectory @options.root, (file) -> - ext = path.extname file - if ext == '.js' || ext == '.coffee' - reduced = file.replace ext, '' - controller = path.basename reduced - self._controllers[controller] = require reduced - if self.options.log - console.log "Controller '#{controller}' has been loaded" - - # We are off to hijack the req.app.routes._route - # which is the point of contact of all our get/post/pull/etc methods. - # We will let the usual chain occur till the very last - # callback, and then we will make sure the controller and action - # are both defined, and then load up that controller/action. - # We have already cached the controllers to reduce require calls - originalRoute = app.routes._route - app.routes._route = (method, path, defaults, callbacks...) -> - # We might not have defaults - if 'function' == typeof defaults - callbacks.push defaults - defaults = null - - if callbacks.length == 0 - callbacks.push (req, res) -> - - defaults ?= { } - holder = { } - - # overwrite the callbacks to use this info - newCallbacks = (self.overwriteCallback c, holder) for c in callbacks - result = originalRoute.call app.routes, method, path, newCallbacks - - # Extend the routing by adding defaults - holder.route = newRoute = result.routes[method][result.routes[method].length - 1] - for defkey, defvalue of defaults - key = self.getKeyInRoute defkey, newRoute - if key? - key.default = defvalue - else - # controller/action is a special case and we need to save it - if defkey == 'controller' or defkey == 'action' - newRoute[defkey] = defvalue - - # If we have a key for controller/action that means they could be anything - for key in newRoute.keys when key.name == 'controller' or key.name == 'action' - newRoute[key.name] = '*' - - return result - - # Add all the corresponding helpers - this.addHelpers app - - addReqHelpers: (req, res) -> - self = this - req.executeController = (controller, action, next) -> - if not controller? or not action? - throw new Error("executeController needs the controller and action specified") - - # If we pass a next switch the controller/action back to our current one - if next? - currentC = req.controller - currentA = req.action - nextFunc = next - next = -> - req.controller = currentC - req.action = currentA - nextFunc.apply this, arguments - - req.controller = controller - req.action = action - self._controllers[controller][action] req, res, next - - addHelpers: (app) -> - self = this - - app.dynamicHelpers { - controller: (req, res) -> - req.controller - - action: (req, res) -> - req.action - - getUrl: (req, res) -> - (controller, action, other, query) -> - if not action? or 'object' == typeof action - query = other - other = action - action = controller - controller = null - - controller ?= req.controller - other ?= {} - other.controller = controller - other.action = action - query ?= {} - - if not action? or not controller? - throw new Error("getUrl needs at minimum an action defined, but also takes a controller") - - for route in app.routes.routes.get - if self.isMatchingPath other, route - # We have found a route that matches - # We are stepping through the keys backwards so that if - # any keys are found the rest MUST be displayed - # (that is... optional keys cannot be blank) - hasReplaced = false - result = route.path - for i in [route.keys.length-1..0] - key = route.keys[i] - def = key.default ? '' - replacement = other[key.name] ? def - if hasReplaced and replacement == '' - throw new Error("The optional parameter '#{key.name}' is required for this getUrl call as an parameter further down the path has been specified") - else - if not hasReplaced - if (not key.optional or replacement != def) and - hasReplaced = true - else - replacement = '' - - # Do the replacement - regExp = new RegExp ":#{key.name}(\\?)?" - result = result.replace regExp, replacement - - # Remove multiple slashes - result = result.replace /\/+/g, '/' - - # Remove trailing slash... unless we are at root - if result != '/' - result = result.replace /\/+$/, '' - - # Add in query strings - first = true - for key, value of query - if first - first = false - result = result + '?' + key - if value? and value != '' - result = result + '=' + value - else - result = result + '&' + key - if value? and value != '' - result = result + '=' + value - - return result - - throw new Error("Route could not be found that matches getUrl parameters, make sure to specify a valid controller, action and required parameters") - } - - getKeyInRoute: (name, route) -> - for key in route.keys when key.name == name - return key - return null - - isMatchingPath: (object, route) -> - # First check the controller and action - if route.controller != '*' and route.controller != object.controller - return false - - if route.action != '*' and route.action != object.action - return false - - # This is checking that all items in the object match with a key - for key, value of object when key != 'controller' and key != 'action' - if not (@getKeyInRoute key, route)? - return false - - # This is checking all (required) keys have an object value - for key in route.keys when key.name != 'controller' and key.name != 'action' - if not key.optional and not object[key]? - return false - - return true - - overwriteCallback: (callback, routeHolder) -> - self = this - options = @options - (req, resp, next) -> - # Call the normal callback - callback req, resp, next - - # Add helpers - self.addReqHelpers req, resp - - # set the current route - route = routeHolder.route - - # Go through our keys and if they have a default and the param value - # is not set make sure it is - for key in route.keys when not req.params[key.name]? and key.default? - req.params[key.name] = key.default - - # Grab our current controller/action either from the route or use defaults - req.controller = req.params.controller ? route.controller - req.action = req.params.action ? route.action - - if options.log - console.log 'Controller: ' + req.controller - console.log 'Action: ' + req.action - - if options.strict - if not req.controller? - throw new Error("Is in strict mode and no controller specified") - if not req.action? - throw new Error("Is in strict mode and no action specified") - - if req.controller? and req.action? - # We have a controller and an action - lets overwrite the res.render - # command so that we do not have to specify view names - if options.overwriteRender - self.overwriteRender req, resp - - # Find the controller - controller = self._controllers[req.controller] - if not controller? - if options.log - console.log "Controller '#{req.controller}' could not be found" - next 'route' - return - - # Execute the action - action = controller[req.action] - if not action? - if options.log - console.log "Action '#{req.action}' could not be found on controller '#{req.controller}' " - next 'route' - return - - # Execute the controller with a nothing followup action - action req, resp, next - else - if options.log - console.log 'Controller or action was not specified, no action was called' - - overwriteRender: (req, resp) -> - self = this - original = resp.render - # This is the root dir the render method uses - root = resp.app.set('views') || process.cwd() + '/views' - - resp.render = (view, opts, fn, parent, sub) -> - # Allow for view to be empty - if 'object' == typeof view || 'function' == typeof view - sub = parent - parent = fn - fn = opts - opts = view - view = null - - # The view defaults to the action - view ?= req.action - - # Set the root directory as the controller directory - # if that doesnt work, try the shared directory - # disable hints because it comes up funny like - hasHints = resp.app.enabled 'hints' - resp.app.disable 'hints' - - result = null - secondResult = null - - reset = -> - if hasHints - resp.app.enable 'hints' - - finalPass = (err, err2, str) -> - reset() - - if err? - err = err + '\r\n\r\n' + err2 - - if fn? - fn err, str - else - if err? - req.next err - else - resp.send str - - secondRender = (err, str) -> - if err? - # If the first render failed failed try getting view from 'shared' - secondResult = original.call resp, self.options.sharedFolder + '/' + view, opts, ((err2, str2) -> finalPass(err2, err, str2)), parent, sub - else - reset() - - if fn? - fn err, str - else - resp.send str - - result = original.call resp, req.controller + '/' + view, opts, secondRender, parent, sub - if secondResult? - result = secondResult - - reset() - return result - - executeOnDirectory: (dir, action) -> - fs.readdirSync(dir).forEach (file) -> - localpath = dir + '/' + file - stat = fs.statSync localpath - if stat and stat.isDirectory() - self.executeOnDirectory localpath, action - else - action localpath diff --git a/setup-env.ps1 b/setup-env.ps1 new file mode 100644 index 0000000..3d323af --- /dev/null +++ b/setup-env.ps1 @@ -0,0 +1,56 @@ +$ErrorActionPreference = "Stop" + +$ProjectDirectory = Split-Path -Parent $MyInvocation.MyCommand.Path +$EnvironmentFile = Join-Path $ProjectDirectory ".env" + +if (Test-Path $EnvironmentFile) { + Write-Host ".env besteht bereits; es wurde nichts ueberschrieben." + exit 0 +} + +function New-RandomHex([int]$ByteCount) { + $Bytes = New-Object byte[] $ByteCount + $Generator = [System.Security.Cryptography.RandomNumberGenerator]::Create() + + try { + $Generator.GetBytes($Bytes) + } + finally { + $Generator.Dispose() + } + + return (($Bytes | ForEach-Object { $_.ToString("x2") }) -join "") +} + +$DatabasePassword = New-RandomHex 24 +$SecretKeyBase = New-RandomHex 64 +$PgHeroPassword = New-RandomHex 16 + +$Content = @" +DATABASE_PASSWORD=$DatabasePassword +SECRET_KEY_BASE=$SecretKeyBase +API_PORT=13000 +FRONTEND_PORT=13131 +FRONTEND_ORIGINS=http://localhost:13131 +FRONTEND_URL=http://localhost:13131 +ADMIN_EMAIL=christoph@marzell.net +MAILER_FROM=praktikum@marzell.net +SMTP_ENABLED=true +SMTP_ADDRESS=smtp.ionos.de +SMTP_PORT=587 +SMTP_USERNAME=praktikum@marzell.net +SMTP_PASSWORD=CHANGE_ME +SMTP_DOMAIN=marzell.net +BACKUP_NOTIFY_EMAIL=christoph@marzell.net +TRAINING_WATCH_NOTIFY_EMAIL=christoph@marzell.net +PGHERO_USERNAME=admin +PGHERO_PASSWORD=$PgHeroPassword +"@ + +[System.IO.File]::WriteAllText( + $EnvironmentFile, + $Content, + [System.Text.UTF8Encoding]::new($false) +) + +Write-Host ".env wurde mit zufaelligen Zugangsdaten erstellt." diff --git a/storage/.keep b/storage/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/tmp/.keep b/tmp/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/tmp/pids/.keep b/tmp/pids/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/tmp/storage/.keep b/tmp/storage/.keep deleted file mode 100644 index e69de29..0000000