/
Dmitry-F
/
cbt_tool
Обзор
Документация
Войти
/
Dmitry-F
/
cbt_tool
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
audit.txt
462 строки
26 KB
Dmitriy-Filatov
feat: диагностика, аудит, чистка Node.js
11 мар 2026, 21:28
11 мар 2026, 21:28
ed932ac
Код
Авторство
О чём код?
=== GEMFILE === source "https://rubygems.org" gem "rails", "~> 8.1.2" gem "pg", "~> 1.1" gem "puma", "~> 6.4" gem "bcrypt", "~> 3.1.7" gem "jbuilder" gem "tzinfo-data", platforms: %i[ windows jruby ] gem "solid_cache" gem "solid_queue" gem "solid_cable" gem "bootsnap", require: false gem "kamal", require: false gem "thruster", require: false gem "image_processing", "~> 1.2" gem "importmap-rails" group :development, :test do gem "debug", platforms: %i[ mri windows ] gem "web-console" end group :development do gem "brakeman", require: false gem "bundler-audit", require: false gem "rubocop-rails-omakase", require: false end gem "tailwindcss-rails", "~> 4.4" gem "propshaft", "~> 1.3" gem "turbo-rails" gem "smer_diary", path: "engines/smer_diary" === IMPORTMAP === pin "application" pin "@hotwired/turbo", to: "@hotwired--turbo.js" pin "@hotwired/turbo-rails", to: "@hotwired--turbo-rails.js" pin "@hotwired/stimulus", to: "@hotwired--stimulus.js" pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" pin_all_from "app/javascript/controllers", under: "controllers" === ROUTES === Routes for application: Prefix Verb URI Pattern Controller#Action signin GET /auth/signin(.:format) auth_sessions#new auth_sessions POST /auth/signin(.:format) auth_sessions#create logout DELETE /auth/logout(.:format) auth_sessions#destroy therapists GET /therapists(.:format) therapists#index POST /therapists(.:format) therapists#create new_therapist GET /therapists/new(.:format) therapists#new edit_therapist GET /therapists/:id/edit(.:format) therapists#edit therapist GET /therapists/:id(.:format) therapists#show PATCH /therapists/:id(.:format) therapists#update PUT /therapists/:id(.:format) therapists#update DELETE /therapists/:id(.:format) therapists#destroy search_clients POST /clients/search(.:format) clients#search client_sessions GET /clients/:client_id/sessions(.:format) sessions#index POST /clients/:client_id/sessions(.:format) sessions#create new_client_session GET /clients/:client_id/sessions/new(.:format) sessions#new edit_client_session GET /clients/:client_id/sessions/:id/edit(.:format) sessions#edit client_session GET /clients/:client_id/sessions/:id(.:format) sessions#show PATCH /clients/:client_id/sessions/:id(.:format) sessions#update PUT /clients/:client_id/sessions/:id(.:format) sessions#update DELETE /clients/:client_id/sessions/:id(.:format) sessions#destroy client_smer_diary /clients/:client_id/smer SmerDiary::Engine clients GET /clients(.:format) clients#index POST /clients(.:format) clients#create new_client GET /clients/new(.:format) clients#new edit_client GET /clients/:id/edit(.:format) clients#edit client GET /clients/:id(.:format) clients#show PATCH /clients/:id(.:format) clients#update PUT /clients/:id(.:format) clients#update DELETE /clients/:id(.:format) clients#destroy search GET /search(.:format) search#show sessions GET /sessions(.:format) sessions#index {:shadow=>true} POST /sessions(.:format) sessions#create {:shadow=>true} new_session GET /sessions/new(.:format) sessions#new {:shadow=>true} edit_session GET /sessions/:id/edit(.:format) sessions#edit {:shadow=>true} session GET /sessions/:id(.:format) sessions#show {:shadow=>true} PATCH /sessions/:id(.:format) sessions#update {:shadow=>true} PUT /sessions/:id(.:format) sessions#update {:shadow=>true} DELETE /sessions/:id(.:format) sessions#destroy {:shadow=>true} root GET / clients#index turbo_recede_historical_location GET /recede_historical_location(.:format) turbo/native/navigation#recede turbo_resume_historical_location GET /resume_historical_location(.:format) turbo/native/navigation#resume turbo_refresh_historical_location GET /refresh_historical_location(.:format) turbo/native/navigation#refresh rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create rails_mandrill_inbound_health_check GET /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#health_check rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create rails_conductor_inbound_emails GET /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#index POST /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#create new_rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/new(.:format) rails/conductor/action_mailbox/inbound_emails#new rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#show new_rails_conductor_inbound_email_source GET /rails/conductor/action_mailbox/inbound_emails/sources/new(.:format) rails/conductor/action_mailbox/inbound_emails/sources#new rails_conductor_inbound_email_sources POST /rails/conductor/action_mailbox/inbound_emails/sources(.:format) rails/conductor/action_mailbox/inbound_emails/sources#create rails_conductor_inbound_email_reroute POST /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format) rails/conductor/action_mailbox/reroutes#create rails_conductor_inbound_email_incinerate POST /rails/conductor/action_mailbox/:inbound_email_id/incinerate(.:format) rails/conductor/action_mailbox/incinerates#create rails_service_blob GET /rails/active_storage/blobs/redirect/:signed_id/*filename(.:format) active_storage/blobs/redirect#show rails_service_blob_proxy GET /rails/active_storage/blobs/proxy/:signed_id/*filename(.:format) active_storage/blobs/proxy#show GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs/redirect#show rails_blob_representation GET /rails/active_storage/representations/redirect/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/redirect#show rails_blob_representation_proxy GET /rails/active_storage/representations/proxy/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/proxy#show GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/redirect#show rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create Routes for SmerDiary::Engine: Prefix Verb URI Pattern Controller#Action entries GET /entries(.:format) smer_diary/entries#index POST /entries(.:format) smer_diary/entries#create new_entry GET /entries/new(.:format) smer_diary/entries#new root GET / smer_diary/entries#index === DATABASE CONFIG === # PostgreSQL. Versions 9.3 and up are supported. # # Install the pg driver: # gem install pg # On macOS with Homebrew: # gem install pg -- --with-pg-config=/opt/homebrew/bin/pg_config # On Windows: # gem install pg # Choose the win32 build. # Install PostgreSQL and put its /bin directory on your path. # # Configure Using Gemfile # gem "pg" # default: &default adapter: postgresql encoding: unicode # For details on connection pooling, see Rails configuration guide # https://guides.rubyonrails.org/configuring.html#database-pooling max_connections: <%= ENV.fetch("RAILS_MAX_THREADS") { 25 } %> development: <<: *default database: cbt_therapist_tool_development # The specified database role being used to connect to PostgreSQL. # To create additional roles in PostgreSQL see `$ createuser --help`. # When left blank, PostgreSQL will use the default role. This is # the same name as the operating system user running Rails. #username: cbt_therapist_tool # The password associated with the PostgreSQL role (username). #password: # Connect on a TCP socket. Omitted by default since the client uses a # domain socket that doesn't need configuration. Windows does not have # domain sockets, so uncomment these lines. #host: localhost # The TCP port the server listens on. Defaults to 5432. # If your server runs on a different port number, change accordingly. #port: 5432 # Schema search path. The server defaults to $user,public #schema_search_path: myapp,sharedapp,public # Minimum log levels, in increasing order: # debug5, debug4, debug3, debug2, debug1, # log, notice, warning, error, fatal, and panic # Defaults to warning. #min_messages: notice # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: <<: *default database: cbt_therapist_tool_test # As with config/credentials.yml, you never want to store sensitive information, # like your database password, in your source code. If your source code is # ever seen by anyone, they now have access to your database. # # Instead, provide the password or a full connection URL as an environment # variable when you boot the app. For example: # # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" # # If the connection URL is provided in the special DATABASE_URL environment # variable, Rails will automatically merge its configuration values on top of # the values provided in this file. Alternatively, you can specify a connection # URL environment variable explicitly: # # production: # url: <%= ENV["MY_APP_DATABASE_URL"] %> # # Connection URLs for non-primary databases can also be configured using # environment variables. The variable name is formed by concatenating the # connection name with `_DATABASE_URL`. For example: # # CACHE_DATABASE_URL="postgres://cacheuser:cachepass@localhost/cachedatabase" # # Read https://guides.rubyonrails.org/configuring.html#configuring-a-database # for a full overview on how database connection configuration can be specified. # production: primary: &primary_production <<: *default database: cbt_therapist_tool_production username: cbt_therapist_tool password: <%= ENV["CBT_THERAPIST_TOOL_DATABASE_PASSWORD"] %> cache: <<: *primary_production database: cbt_therapist_tool_production_cache migrations_paths: db/cache_migrate queue: <<: *primary_production database: cbt_therapist_tool_production_queue migrations_paths: db/queue_migrate cable: <<: *primary_production database: cbt_therapist_tool_production_cable migrations_paths: db/cable_migrate === SCHEMA (TABLES & INDEXES) === create_table "action_text_rich_texts", force: :cascade do |t| t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true create_table "active_storage_attachments", force: :cascade do |t| t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true create_table "active_storage_blobs", force: :cascade do |t| t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true create_table "active_storage_variant_records", force: :cascade do |t| t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true create_table "clients", force: :cascade do |t| t.index ["email", "therapist_id"], name: "index_clients_on_email_and_therapist_id", unique: true t.index ["email"], name: "index_clients_on_email_for_search" t.index ["name"], name: "index_clients_on_name_for_search" t.index ["phone"], name: "index_clients_on_phone_for_search" t.index ["therapist_id"], name: "index_clients_on_therapist_id" create_table "sessions", force: :cascade do |t| t.index ["client_id", "session_date", "created_at"], name: "index_sessions_on_client_date_created" t.index ["client_id"], name: "index_sessions_on_client_id" t.index ["notes"], name: "index_sessions_on_notes_for_search" t.index ["therapist_id", "session_date"], name: "index_sessions_on_therapist_and_date" t.index ["topic"], name: "index_sessions_on_topic_for_search" create_table "smer_diary_diary_entries", force: :cascade do |t| t.index ["client_id"], name: "index_smer_diary_diary_entries_on_client_id" t.index ["emotion"], name: "index_smer_diary_entries_on_emotion_for_search" t.index ["event"], name: "index_smer_diary_entries_on_event_for_search" t.index ["reaction"], name: "index_smer_diary_entries_on_reaction_for_search" t.index ["thought"], name: "index_smer_diary_entries_on_thought_for_search" create_table "therapists", force: :cascade do |t| t.index ["email"], name: "index_therapists_on_email", unique: true === APPLICATION LAYOUT === <!DOCTYPE html> <html class="h-full"> <head> <title>CBT Tool</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag "tailwind", "trix", "fonts", "actiontext", "data-turbo-track": "reload" %> <%= javascript_importmap_tags %> <style> #search_results { transition: opacity 0.2s ease, visibility 0.2s, transform 0.2s ease; transform-origin: top center; } #search_results.hidden { opacity: 0; visibility: hidden; transform: translateY(-10px); pointer-events: none; } #search-loading { transition: opacity 0.15s ease; } #search-loading.hidden { opacity: 0; pointer-events: none; } .search-error { transition: opacity 0.2s ease; } </style> </head> <body class="bg-calm-beige font-sans text-slate-900 antialiased min-h-full"> <div class="min-h-screen flex flex-col"> <%# Навигация с исправленным поиском %> <nav class="flex-none w-full bg-white border-b border-slate-200 px-6 py-4 flex items-center justify-between z-40"> <div class="flex items-center space-x-8"> <%= link_to main_app.clients_path, class: "font-serif text-xl font-bold text-primary" do %>CBT Tool<% end %> <%# Обертка поиска %> <div class="relative w-80"> <%= form_with url: search_path, method: :get, data: { turbo_frame: "search_results" } do |form| %> <div class="relative"> <%= form.text_field :q, placeholder: "Поиск по клиентам, сессиям, записям...", class: "w-full bg-slate-100 border-none rounded-xl text-sm focus:ring-2 focus:ring-primary/20 transition-all px-4 py-2", data: { clients: @clients.to_json } %> </div> <% end %> <%# Результаты внутри wrapper, привязаны к инпуту %> <div class="absolute left-0 right-0 top-full mt-2 z-50"> <%= turbo_frame_tag "search_results", class: "block bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden hidden pb-3" do %> <% end %> </div> </div> </div> <% if current_therapist %> <div class="flex items-center space-x-6"> <%= link_to "Профиль", main_app.therapist_path(Current.therapist), class: "text-xs font-medium text-slate-400 hover:text-primary leading-none" %> <%= button_to "Выйти", main_app.logout_path, method: :delete, class: "text-xs text-red-600/40 hover:text-red-600 font-medium leading-none" %> </div> <% end %> </nav> <%# Main без ограничений - контент определяет высоту и ширину %> <main class="flex-1 p-4"> <div class="w-full"> <%= yield %> </div> </main> </div> <%# Скрипт для управления поиском (кэширование элементов, анимация, полная обработка) %> <script> (function() { const searchFrame = document.getElementById("search_results"); const searchForm = document.querySelector("form[action='/search']"); const loading = document.getElementById("search-loading"); const searchInput = document.querySelector("input[name='q']"); if (!searchFrame || !searchForm) return; function updateSearchVisibility(visible) { if (visible) { searchFrame.classList.remove("hidden"); searchFrame.setAttribute("aria-hidden", "false"); if (searchInput) searchInput.setAttribute("aria-expanded", "true"); } else { searchFrame.classList.add("hidden"); searchFrame.setAttribute("aria-hidden", "true"); if (searchInput) searchInput.setAttribute("aria-expanded", "false"); } } function clearErrors() { const existingError = searchFrame.querySelector(".search-error"); if (existingError) existingError.remove(); } document.addEventListener("turbo:submit-start", function() { if (loading) { loading.classList.remove("hidden"); loading.setAttribute("aria-hidden", "false"); } clearErrors(); }); document.addEventListener("turbo:fetch-complete", function() { if (loading) { loading.classList.add("hidden"); loading.setAttribute("aria-hidden", "true"); } }); document.addEventListener("turbo:fetch-error", function(event) { if (loading) { loading.classList.add("hidden"); loading.setAttribute("aria-hidden", "true"); } clearErrors(); const errorMsg = document.createElement("div"); errorMsg.textContent = "Произошла ошибка при поиске. Попробуйте ещё раз."; errorMsg.className = "text-red-600 text-sm mt-2 search-error"; errorMsg.setAttribute("role", "alert"); searchFrame.appendChild(errorMsg); updateSearchVisibility(true); }); document.addEventListener("turbo:frame-load", function(event) { if (event.target.id === "search_results" && searchFrame) { clearErrors(); if (searchFrame.innerHTML.trim() !== "") { updateSearchVisibility(true); } else { updateSearchVisibility(false); } } }); document.addEventListener("click", function(event) { const isOutsideForm = !searchForm.contains(event.target); const isOutsideFrame = !searchFrame.contains(event.target); const isOutsideLoading = !loading || !loading.contains(event.target); if (isOutsideForm && isOutsideFrame && isOutsideLoading) { updateSearchVisibility(false); } }); searchFrame.addEventListener("click", function(event) { event.stopPropagation(); }); document.addEventListener("keydown", function(event) { if (event.key === "Escape" && !searchFrame.classList.contains("hidden")) { updateSearchVisibility(false); if (searchInput) searchInput.blur(); } }); if (searchInput) { searchInput.addEventListener("input", function() { if (this.value.trim() && searchFrame.classList.contains("hidden")) { updateSearchVisibility(true); } }); searchInput.addEventListener("focus", function() { if (searchFrame.innerHTML.trim() !== "" && searchFrame.classList.contains("hidden")) { updateSearchVisibility(true); } }); searchInput.addEventListener("blur", function() { setTimeout(() => { if (this.value.trim() === "" && !searchFrame.matches(":hover")) { updateSearchVisibility(false); } }, 300); }); } updateSearchVisibility(!searchFrame.classList.contains("hidden")); })(); </script> </body> </html>