5
0
Fork 0
mirror of https://0xacab.org/sutty/sutty synced 2024-11-16 15:51:41 +00:00

Merge branch 'rails' of 0xacab.org:sutty/sutty into cambiar-idioma-desde-login

This commit is contained in:
f 2023-03-17 14:59:15 -03:00
commit 13ec35f61b
51 changed files with 1096 additions and 326 deletions

9
.profile Normal file
View file

@ -0,0 +1,9 @@
Color_Off='\e[0m'
BPurple='\e[1;35m'
BBlue='\e[1;34m'
is_git() {
git rev-parse --abbrev-ref HEAD 2>/dev/null
}
PS1="\[${BPurple}\]\$(is_git) \[${BBlue}\]\W\[${Color_Off}\] >_ "

18
Gemfile
View file

@ -48,9 +48,9 @@ gem 'image_processing'
gem 'icalendar' gem 'icalendar'
gem 'inline_svg' gem 'inline_svg'
gem 'httparty' gem 'httparty'
gem 'safe_yaml', source: 'https://gems.sutty.nl' gem 'safe_yaml'
gem 'jekyll', '~> 4.2' gem 'jekyll', '~> 4.2'
gem 'jekyll-data', source: 'https://gems.sutty.nl' gem 'jekyll-data'
gem 'jekyll-commonmark' gem 'jekyll-commonmark'
gem 'jekyll-images' gem 'jekyll-images'
gem 'jekyll-include-cache' gem 'jekyll-include-cache'
@ -64,7 +64,7 @@ gem 'rails-i18n'
gem 'rails_warden' gem 'rails_warden'
gem 'redis', require: %w[redis redis/connection/hiredis] gem 'redis', require: %w[redis redis/connection/hiredis]
gem 'redis-rails' gem 'redis-rails'
gem 'rollups', git: 'https://github.com/ankane/rollup.git', branch: 'master' gem 'rollups', git: 'https://github.com/fauno/rollup.git', branch: 'update'
gem 'rubyzip' gem 'rubyzip'
gem 'rugged' gem 'rugged'
gem 'concurrent-ruby-ext' gem 'concurrent-ruby-ext'
@ -99,18 +99,6 @@ gem 'net-ssh'
gem 'ed25519' gem 'ed25519'
gem 'bcrypt_pbkdf' gem 'bcrypt_pbkdf'
group :themes do
gem 'adhesiones-jekyll-theme', require: false
gem 'editorial-autogestiva-jekyll-theme', require: false
gem 'minima', require: false
gem 'sutty-minima', require: false
gem 'radios-comunitarias-jekyll-theme', require: false
gem 'share-to-fediverse-jekyll-theme', require: false
gem 'sutty-donaciones-jekyll-theme', require: false
gem 'sutty-jekyll-theme', require: false
gem 'recursero-jekyll-theme', require: false
end
group :production do group :production do
gem 'lograge' gem 'lograge'
end end

View file

@ -6,15 +6,6 @@ GIT
rails (>= 3.0) rails (>= 3.0)
rake (>= 0.8.7) rake (>= 0.8.7)
GIT
remote: https://github.com/ankane/rollup.git
revision: 0ab6c603450175eb1004f7793e86486943cb9f72
branch: master
specs:
rollups (0.1.3)
activesupport (>= 5.1)
groupdate (>= 5.2)
GIT GIT
remote: https://github.com/fauno/email_address remote: https://github.com/fauno/email_address
revision: 536b51f7071b68a55140c0c1726b4cd401d1c04d revision: 536b51f7071b68a55140c0c1726b4cd401d1c04d
@ -24,6 +15,15 @@ GIT
netaddr (>= 2.0.4, < 3) netaddr (>= 2.0.4, < 3)
simpleidn simpleidn
GIT
remote: https://github.com/fauno/rollup.git
revision: ddbb345aa57e63b4cfdf7557267efa89ba60caac
branch: update
specs:
rollups (0.1.3)
activesupport (>= 5.1)
groupdate (>= 5.2)
GEM GEM
remote: https://gems.sutty.nl/ remote: https://gems.sutty.nl/
specs: specs:
@ -88,15 +88,6 @@ GEM
zeitwerk (~> 2.3) zeitwerk (~> 2.3)
addressable (2.8.0) addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0) public_suffix (>= 2.0.2, < 5.0)
adhesiones-jekyll-theme (0.2.1)
jekyll (~> 4.0)
jekyll-data (~> 1.1)
jekyll-feed (~> 0.9)
jekyll-images (~> 0.2)
jekyll-include-cache (~> 0)
jekyll-locales (~> 0.1)
jekyll-relative-urls (~> 0.0)
jekyll-seo-tag (~> 2.1)
ast (2.4.2) ast (2.4.2)
autoprefixer-rails (10.3.3.0) autoprefixer-rails (10.3.3.0)
execjs (~> 2) execjs (~> 2)
@ -169,25 +160,6 @@ GEM
down (5.2.4) down (5.2.4)
addressable (~> 2.8) addressable (~> 2.8)
ed25519 (1.2.4-x86_64-linux-musl) ed25519 (1.2.4-x86_64-linux-musl)
editorial-autogestiva-jekyll-theme (0.3.4)
jekyll (~> 4)
jekyll-commonmark (~> 1.3)
jekyll-data (~> 1.1)
jekyll-dotenv (>= 0.2)
jekyll-feed (~> 0.15)
jekyll-hardlinks (~> 0)
jekyll-ignore-layouts (~> 0)
jekyll-images (~> 0.2)
jekyll-include-cache (~> 0)
jekyll-linked-posts (~> 0)
jekyll-locales (~> 0.1)
jekyll-order (~> 0)
jekyll-relative-urls (~> 0)
jekyll-seo-tag (~> 2)
jekyll-spree-client (~> 0)
jekyll-unique-urls (~> 0)
jekyll-write-and-commit-changes (~> 0)
sutty-liquid (~> 0)
em-websocket (0.5.3) em-websocket (0.5.3)
eventmachine (>= 0.12.9) eventmachine (>= 0.12.9)
http_parser.rb (~> 0) http_parser.rb (~> 0)
@ -214,8 +186,8 @@ GEM
ffi (~> 1.0) ffi (~> 1.0)
globalid (0.6.0) globalid (0.6.0)
activesupport (>= 5.0) activesupport (>= 5.0)
groupdate (5.2.2) groupdate (6.1.0)
activesupport (>= 5) activesupport (>= 5.2)
hairtrigger (0.2.24) hairtrigger (0.2.24)
activerecord (>= 5.0, < 7) activerecord (>= 5.0, < 7)
ruby2ruby (~> 2.4) ruby2ruby (~> 2.4)
@ -369,10 +341,6 @@ GEM
mini_magick (4.11.0) mini_magick (4.11.0)
mini_mime (1.1.2) mini_mime (1.1.2)
mini_portile2 (2.6.1) mini_portile2 (2.6.1)
minima (2.5.1)
jekyll (>= 3.5, < 5.0)
jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1)
minitest (5.14.4) minitest (5.14.4)
mobility (1.2.4) mobility (1.2.4)
i18n (>= 0.6.10, < 2) i18n (>= 0.6.10, < 2)
@ -415,17 +383,6 @@ GEM
rack rack
rack-test (1.1.0) rack-test (1.1.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
radios-comunitarias-jekyll-theme (0.1.5)
jekyll (~> 4.0)
jekyll-data (~> 1.1)
jekyll-feed (~> 0.9)
jekyll-images (~> 0.2)
jekyll-include-cache (~> 0)
jekyll-linked-posts (~> 0)
jekyll-locales (~> 0.1)
jekyll-relative-urls (~> 0.0)
jekyll-seo-tag (~> 2.1)
jekyll-turbolinks (~> 0)
rails (6.1.4.1) rails (6.1.4.1)
actioncable (= 6.1.4.1) actioncable (= 6.1.4.1)
actionmailbox (= 6.1.4.1) actionmailbox (= 6.1.4.1)
@ -462,24 +419,6 @@ GEM
rb-fsevent (0.11.0) rb-fsevent (0.11.0)
rb-inotify (0.10.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
recursero-jekyll-theme (0.2.0)
jekyll (~> 4)
jekyll-commonmark (~> 1.3)
jekyll-data (~> 1.1)
jekyll-dotenv (>= 0.2)
jekyll-feed (~> 0.15)
jekyll-ignore-layouts (~> 0)
jekyll-images (~> 0.2)
jekyll-include-cache (~> 0)
jekyll-linked-posts (~> 0)
jekyll-locales (~> 0.1)
jekyll-lunr (~> 0.1)
jekyll-order (~> 0)
jekyll-relative-urls (~> 0)
jekyll-seo-tag (~> 2)
jekyll-unique-urls (~> 0.1)
sutty-archives (~> 2.2)
sutty-liquid (~> 0)
redis (4.5.1) redis (4.5.1)
redis-actionpack (5.2.0) redis-actionpack (5.2.0)
actionpack (>= 5, < 7) actionpack (>= 5, < 7)
@ -552,14 +491,6 @@ GEM
rubyzip (>= 1.2.2) rubyzip (>= 1.2.2)
semantic_range (3.0.0) semantic_range (3.0.0)
sexp_processor (4.16.0) sexp_processor (4.16.0)
share-to-fediverse-jekyll-theme (0.1.4)
jekyll (~> 4.0)
jekyll-data (~> 1.1)
jekyll-feed (~> 0.9)
jekyll-images (~> 0.2)
jekyll-include-cache (~> 0)
jekyll-relative-urls (~> 0.0)
jekyll-seo-tag (~> 2.1)
simpleidn (0.2.1) simpleidn (0.2.1)
unf (~> 0.1.4) unf (~> 0.1.4)
sourcemap (0.1.1) sourcemap (0.1.1)
@ -583,30 +514,9 @@ GEM
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
sutty-archives (2.5.4) sutty-archives (2.5.4)
jekyll (>= 3.6, < 5.0) jekyll (>= 3.6, < 5.0)
sutty-donaciones-jekyll-theme (0.1.2)
jekyll (~> 4.0)
jekyll-data (~> 1.1)
jekyll-feed (~> 0.9)
jekyll-images (~> 0.2)
jekyll-include-cache (~> 0)
jekyll-locales (~> 0.1)
jekyll-relative-urls (~> 0.0)
jekyll-seo-tag (~> 2.1)
sutty-archives (~> 2.2)
sutty-jekyll-theme (0.1.2)
jekyll (~> 4.0)
jekyll-feed (~> 0.9)
jekyll-images (~> 0.2)
jekyll-include-cache (~> 0)
jekyll-relative-urls (~> 0.0)
jekyll-seo-tag (~> 2.1)
sutty-liquid (0.7.4) sutty-liquid (0.7.4)
fast_blank (~> 1.0) fast_blank (~> 1.0)
jekyll (~> 4) jekyll (~> 4)
sutty-minima (2.5.0)
jekyll (>= 3.5, < 5.0)
jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1)
symbol-fstring (1.0.2-x86_64-linux-musl) symbol-fstring (1.0.2-x86_64-linux-musl)
sysexits (1.2.0) sysexits (1.2.0)
temple (0.8.2) temple (0.8.2)
@ -654,7 +564,6 @@ PLATFORMS
x86_64-linux-musl x86_64-linux-musl
DEPENDENCIES DEPENDENCIES
adhesiones-jekyll-theme
bcrypt (~> 3.1.7) bcrypt (~> 3.1.7)
bcrypt_pbkdf bcrypt_pbkdf
blazer blazer
@ -672,7 +581,6 @@ DEPENDENCIES
dotenv-rails dotenv-rails
down down
ed25519 ed25519
editorial-autogestiva-jekyll-theme
email_address! email_address!
exception_notification exception_notification
factory_bot_rails factory_bot_rails
@ -691,7 +599,7 @@ DEPENDENCIES
jbuilder (~> 2.5) jbuilder (~> 2.5)
jekyll (~> 4.2) jekyll (~> 4.2)
jekyll-commonmark jekyll-commonmark
jekyll-data! jekyll-data
jekyll-images jekyll-images
jekyll-include-cache jekyll-include-cache
kaminari kaminari
@ -702,7 +610,6 @@ DEPENDENCIES
lograge lograge
memory_profiler memory_profiler
mini_magick mini_magick
minima
mobility mobility
net-ssh net-ssh
nokogiri nokogiri
@ -714,31 +621,25 @@ DEPENDENCIES
pundit pundit
rack-cors rack-cors
rack-mini-profiler rack-mini-profiler
radios-comunitarias-jekyll-theme
rails (~> 6) rails (~> 6)
rails-i18n rails-i18n
rails_warden rails_warden
recursero-jekyll-theme
redis redis
redis-rails redis-rails
rollups! rollups!
rubocop-rails rubocop-rails
rubyzip rubyzip
rugged rugged
safe_yaml! safe_yaml
sassc-rails sassc-rails
selenium-webdriver selenium-webdriver
share-to-fediverse-jekyll-theme
sourcemap sourcemap
spring spring
spring-watcher-listen (~> 2.0.0) spring-watcher-listen (~> 2.0.0)
sqlite3 sqlite3
stackprof stackprof
sucker_punch sucker_punch
sutty-donaciones-jekyll-theme
sutty-jekyll-theme
sutty-liquid (>= 0.7.3) sutty-liquid (>= 0.7.3)
sutty-minima
symbol-fstring symbol-fstring
terminal-table terminal-table
timecop timecop

View file

@ -5,3 +5,4 @@ blazer_1h: bundle exec rake blazer:run_checks SCHEDULE="1 hour"
blazer_1d: bundle exec rake blazer:run_checks SCHEDULE="1 day" blazer_1d: bundle exec rake blazer:run_checks SCHEDULE="1 day"
blazer: bundle exec rake blazer:send_failing_checks blazer: bundle exec rake blazer:send_failing_checks
prometheus: bundle exec prometheus_exporter -b 0.0.0.0 --prefix "sutty_" prometheus: bundle exec prometheus_exporter -b 0.0.0.0 --prefix "sutty_"
stats: bundle exec rake stats:process_all

View file

@ -126,6 +126,7 @@ ol.breadcrumb {
color: var(--foreground); color: var(--foreground);
} }
.table tr.sticky-top,
.form-control, .form-control,
.custom-file-label { .custom-file-label {
background-color: var(--background); background-color: var(--background);

View file

@ -3,6 +3,7 @@
# Forma de ingreso a Sutty # Forma de ingreso a Sutty
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
include ExceptionHandler include ExceptionHandler
include Pundit
protect_from_forgery with: :null_session, prepend: true protect_from_forgery with: :null_session, prepend: true
@ -10,6 +11,7 @@ class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller? before_action :configure_permitted_parameters, if: :devise_controller?
around_action :set_locale around_action :set_locale
rescue_from Pundit::NilPolicyError, with: :page_not_found
rescue_from ActionController::RoutingError, with: :page_not_found rescue_from ActionController::RoutingError, with: :page_not_found
rescue_from ActionController::ParameterMissing, with: :page_not_found rescue_from ActionController::ParameterMissing, with: :page_not_found
@ -33,7 +35,7 @@ class ApplicationController < ActionController::Base
def find_site def find_site
id = params[:site_id] || params[:id] id = params[:site_id] || params[:id]
unless (site = current_usuarie.sites.find_by_name(id)) unless (site = current_usuarie&.sites&.find_by_name(id))
raise SiteNotFound raise SiteNotFound
end end
@ -62,6 +64,21 @@ class ApplicationController < ActionController::Base
render 'application/page_not_found', status: :not_found render 'application/page_not_found', status: :not_found
end end
# Necesario para poder acceder a Blazer. Solo les usuaries de este
# sitio pueden acceder al panel.
def require_usuarie
site = find_site
authorize SiteBlazer.new(site)
# Necesario para los breadcrumbs.
ActionView::Base.include Loaf::ViewExtensions unless ActionView::Base.included_modules.include? Loaf::ViewExtensions
breadcrumb current_usuarie.email, main_app.edit_usuarie_registration_path
breadcrumb 'sites.index', main_app.sites_path, match: :exact
breadcrumb site.title, main_app.site_path(site), match: :exact
breadcrumb 'stats.index', root_path, match: :exact
end
protected protected
def configure_permitted_parameters def configure_permitted_parameters

View file

@ -0,0 +1,194 @@
# frozen_string_literal: true
# Modificaciones para Blazer
module BlazerDecorator
# No poder obtener información de la base de datos.
module DisableDatabaseInfo
extend ActiveSupport::Concern
included do
def docs; end
def tables; end
def schema; end
end
end
# Deshabilitar edición de consultas y chequeos.
module DisableEdits
extend ActiveSupport::Concern
included do
def create; end
def update; end
def destroy; end
def run; end
def refresh; end
def cancel; end
end
end
# Blazer hace un gran esfuerzo para ejecutar consultas de forma
# asincrónica pero termina enviándolas por JS.
module RunSync
extend ActiveSupport::Concern
included do
alias_method :original_show, :show
include Blazer::BaseHelper
def show
original_show
options = { user: blazer_user, query: @query, run_id: SecureRandom.uuid, async: false }
@data_source = Blazer.data_sources[@query.data_source]
@result = Blazer::RunStatement.new.perform(@data_source, @statement, options)
chart_data
end
private
# Solo mostrar las consultas de le usuarie
def set_queries(_ = nil)
@queries = (@current_usuarie || current_usuarie).blazer_queries
end
# blazer-2.4.2/app/views/blazer/queries/run.html.erb
def chart_type
case @result.chart_type
when /\Aline(2)?\z/
chart_options.merge! min: nil
when /\Abar(2)?\z/
chart_options.merge! library: { tooltips: { intersect: false, axis: 'x' } }
when 'pie'
chart_options
when 'scatter'
chart_options.merge! library: { tooltips: { intersect: false } }, xtitle: @result.columns[0],
ytitle: @result.columns[1]
when nil
else
if @result.column_types.size == 2
chart_options.merge! library: { tooltips: { intersect: false, axis: 'x' } }
else
chart_options.merge! library: { tooltips: { intersect: false } }
end
end
@result.chart_type
end
def chart_data
@chart_data ||=
case chart_type
when 'line'
@result.columns[1..-1].each_with_index.map do |k, i|
{
name: blazer_series_name(k),
data: @result.rows.map do |r|
[r[0], r[i + 1]]
end,
library: series_library[i]
}
end
when 'line2'
@result.rows.group_by do |r|
v = r[1]
(@result.boom[@result.columns[1]] || {})[v.to_s] || v
end.each_with_index.map do |(name, v), i|
{
name: blazer_series_name(name),
data: v.map do |v2|
[v2[0], v2[2]]
end,
library: series_library[i]
}
end
when 'pie'
@result.rows.map do |r|
[(@result.boom[@result.columns[0]] || {})[r[0].to_s] || r[0], r[1]]
end
when 'bar'
(@result.rows.first.size - 1).times.map do |i|
name = @result.columns[i + 1]
{
name: blazer_series_name(name),
data: @result.rows.first(20).map do |r|
[(@result.boom[@result.columns[0]] || {})[r[0].to_s] || r[0], r[i + 1]]
end
}
end
when 'bar2'
first_20 = @result.rows.group_by { |r| r[0] }.values.first(20).flatten(1)
labels = first_20.map { |r| r[0] }.uniq
series = first_20.map { |r| r[1] }.uniq
labels.each do |l|
series.each do |s|
first_20 << [l, s, 0] unless first_20.find { |r| r[0] == l && r[1] == s }
end
end
first_20.group_by do |r|
v = r[1]
(@result.boom[@result.columns[1]] || {})[v.to_s] || v
end.each_with_index.map do |(name, v), _i|
{
name: blazer_series_name(name),
data: v.sort_by do |r2|
labels.index(r2[0])
end.map do |v2|
v3 = v2[0]
[(@result.boom[@result.columns[0]] || {})[v3.to_s] || v3, v2[2]]
end
}
end
when 'scatter'
@result.rows
end
end
def target_index
@target_index ||= @result.columns.index do |k|
k.downcase == 'target'
end
end
def series_library
@series_library ||= {}.tap do |sl|
if target_index
color = '#109618'
sl[target_index - 1] = {
pointStyle: 'line',
hitRadius: 5,
borderColor: color,
pointBackgroundColor: color,
backgroundColor: color,
pointHoverBackgroundColor: color
}
end
end
end
def chart_options
@chart_options ||= { id: SecureRandom.hex }
end
end
end
end
classes = [Blazer::QueriesController, Blazer::ChecksController, Blazer::DashboardsController]
modules = [BlazerDecorator::DisableDatabaseInfo, BlazerDecorator::DisableEdits]
classes.each do |klass|
modules.each do |modul|
klass.include modul unless klass.included_modules.include? modul
end
end
Blazer::QueriesController.include BlazerDecorator::RunSync

View file

@ -2,9 +2,6 @@
# Controlador para artículos # Controlador para artículos
class PostsController < ApplicationController class PostsController < ApplicationController
include Pundit
rescue_from Pundit::NilPolicyError, with: :page_not_found
before_action :authenticate_usuarie! before_action :authenticate_usuarie!
before_action :service_for_direct_upload, only: %i[new edit] before_action :service_for_direct_upload, only: %i[new edit]
@ -38,6 +35,8 @@ class PostsController < ApplicationController
# Filtrar los posts que les invitades no pueden ver # Filtrar los posts que les invitades no pueden ver
@usuarie = site.usuarie? current_usuarie @usuarie = site.usuarie? current_usuarie
@site_stat = SiteStat.new(site)
end end
def show def show

View file

@ -6,8 +6,6 @@ class PrivateController < ApplicationController
# XXX: Permite ejecutar JS # XXX: Permite ejecutar JS
skip_forgery_protection skip_forgery_protection
include Pundit
# Enviar el archivo si existe, agregar una / al final siempre para no # Enviar el archivo si existe, agregar una / al final siempre para no
# romper las direcciones relativas. # romper las direcciones relativas.
def show def show

View file

@ -2,9 +2,6 @@
# Controlador de sitios # Controlador de sitios
class SitesController < ApplicationController class SitesController < ApplicationController
include Pundit
rescue_from Pundit::NilPolicyError, with: :page_not_found
before_action :authenticate_usuarie! before_action :authenticate_usuarie!
breadcrumb -> { current_usuarie.email }, :edit_usuarie_registration_path breadcrumb -> { current_usuarie.email }, :edit_usuarie_registration_path

View file

@ -8,6 +8,10 @@ class StatsController < ApplicationController
before_action :authenticate_usuarie! before_action :authenticate_usuarie!
before_action :authorize_stats before_action :authorize_stats
breadcrumb -> { current_usuarie.email }, :edit_usuarie_registration_path
breadcrumb 'sites.index', :sites_path, match: :exact
breadcrumb -> { site.title }, -> { site_posts_path(site, locale: locale) }, match: :exact
EXTRA_OPTIONS = { EXTRA_OPTIONS = {
builds: {}, builds: {},
space_used: { bytes: true }, space_used: { bytes: true },
@ -20,19 +24,53 @@ class StatsController < ApplicationController
policy.script_src :self, :unsafe_inline policy.script_src :self, :unsafe_inline
end end
# Parámetros por defecto
#
# @return [Hash]
def default_url_options
{ interval: 'day', period_start: Date.today.beginning_of_year, period_end: Date.today }
end
def index def index
@chart_params = { interval: interval } breadcrumb I18n.t('stats.index.title'), ''
params.with_defaults! default_url_options
@chart_params = {
interval: interval,
period_start: params[:period_start],
period_end: params[:period_end]
}
hostnames hostnames
last_stat last_stat
chart_options chart_options
normalized_urls normalized_urls
expires_in = Time.now.try(:"end_of_#{Stat.default_interval}") - Time.now
@columns = {}
Stat::COLUMNS.each do |column|
@columns[column] =
Rails.cache.fetch("stats/#{column}/#{site.id}", expires_in: expires_in) do
rollup_scope.where(interval: interval, name: "host|#{column}")
.where_dimensions(host: hostnames)
.group("dimensions->>'#{column}'")
.order('sum(value) desc')
.sum(:value)
.transform_values(&:to_i)
.transform_values do |v|
v * nodes
end
end
end
end end
# Genera un gráfico de visitas por dominio asociado a este sitio # Genera un gráfico de visitas por dominio asociado a este sitio
def host def host
return unless stale? [last_stat, hostnames, interval] return unless stale? [last_stat, hostnames, interval, period]
stats = Rollup.where_dimensions(host: hostnames).multi_series('host', interval: interval).tap do |series| stats = rollup_scope.where_dimensions(host: hostnames).multi_series('host', interval: interval).tap do |series|
series.each do |serie| series.each do |serie|
serie[:name] = serie.dig(:dimensions, 'host') serie[:name] = serie.dig(:dimensions, 'host')
serie[:data].transform_values! do |value| serie[:data].transform_values! do |value|
@ -45,23 +83,20 @@ class StatsController < ApplicationController
end end
def resources def resources
return unless stale? [last_stat, interval, resource] return unless stale? [last_stat, interval, resource, period]
options = { options = { interval: interval, dimensions: { site_id: site.id } }
interval: interval,
dimensions: {
deploy_id: @site.deploys.where(type: 'DeployLocal').pluck(:id).first
}
}
render json: Rollup.series(resource, **options) render json: rollup_scope.series(resource, **options)
end end
def uris def uris
return unless stale? [last_stat, hostnames, interval, normalized_urls] return unless stale? [last_stat, hostnames, interval, normalized_urls, period]
options = { host: hostnames, uri: normalized_paths } options = { host: hostnames, uri: normalized_paths }
stats = Rollup.where_dimensions(**options).multi_series('host|uri', interval: interval).tap do |series| # XXX: where_dimensions es más corto pero no aprovecha los índices
# de Rollup
stats = rollup_scope.where_dimensions(**options).multi_series('host|uri', interval: interval).tap do |series|
series.each do |serie| series.each do |serie|
serie[:name] = serie[:dimensions].slice('host', 'uri').values.join.sub('/index.html', '/') serie[:name] = serie[:dimensions].slice('host', 'uri').values.join.sub('/index.html', '/')
serie[:data].transform_values! do |value| serie[:data].transform_values! do |value|
@ -75,34 +110,44 @@ class StatsController < ApplicationController
private private
def rollup_scope
Rollup.where(time: period)
end
def last_stat def last_stat
@last_stat ||= Stat.last @last_stat ||= site.stats.last
end end
def authorize_stats def authorize_stats
@site = find_site authorize SiteStat.new(site)
authorize SiteStat.new(@site)
end end
# TODO: Eliminar cuando mergeemos referer-origin # TODO: Eliminar cuando mergeemos referer-origin
def hostnames def hostnames
@hostnames ||= [@site.hostname, @site.alternative_hostnames].flatten @hostnames ||= site.hostnames
end end
# Normalizar las URLs # Normalizar las URLs
# #
# @return [Array] # @return [Array]
def normalized_urls def normalized_urls
@normalized_urls ||= params.permit(:urls).try(:[], @normalized_urls ||=
:urls)&.split("\n")&.map(&:strip)&.select(&:present?)&.select do |uri| begin
urls = params[:urls].is_a?(Array) ? params[:urls] : params[:urls]&.split("\n")
urls = urls&.map(&:strip)&.select(&:present?)&.select do |uri|
uri.start_with? 'https://' uri.start_with? 'https://'
end&.map do |u| end
# XXX: Eliminar
urls ||= [site.url]
urls.map do |u|
# XXX: Eliminar al deployear
# @see {https://0xacab.org/sutty/containers/nginx/-/merge_requests/1} # @see {https://0xacab.org/sutty/containers/nginx/-/merge_requests/1}
next u unless u.end_with? '/' next u unless u.end_with? '/'
"#{u}index.html" "#{u}index.html"
end&.uniq || [@site.url, @site.urls].flatten.uniq end.uniq
end
end end
def normalized_paths def normalized_paths
@ -140,14 +185,15 @@ class StatsController < ApplicationController
def interval def interval
@interval ||= begin @interval ||= begin
i = params[:interval]&.to_sym i = params[:interval]&.to_sym
Stat::INTERVALS.include?(i) ? i : :day Stat::INTERVALS.include?(i) ? i : Stat::INTERVALS.first
end end
end end
# @return [Symbol]
def resource def resource
@resource ||= begin @resource ||= begin
r = params[:resource].to_sym r = params[:resource].to_sym
Stat::RESOURCES.include?(r) ? r : :builds Stat::RESOURCES.include?(r) ? r : Stat::RESOURCES.first
end end
end end
@ -165,4 +211,15 @@ class StatsController < ApplicationController
def nodes def nodes
@nodes ||= ENV.fetch('NODES', 1).to_i @nodes ||= ENV.fetch('NODES', 1).to_i
end end
def period
@period ||= begin
p = params.permit(:period_start, :period_end)
p[:period_start]..p[:period_end]
end
end
def site
@site ||= find_site
end
end end

View file

@ -20,7 +20,6 @@ function makeParentBlock(
}; };
} }
// TODO: añadir blockquote
// XXX: si agregás algo acá, probablemente le quieras hacer un botón // XXX: si agregás algo acá, probablemente le quieras hacer un botón
// en app/views/posts/attributes/_content.haml // en app/views/posts/attributes/_content.haml
export const parentBlocks: { [propName: string]: EditorNode } = { export const parentBlocks: { [propName: string]: EditorNode } = {

View file

@ -0,0 +1,60 @@
# frozen_string_literal: true
# Implementa rollups recursivos
module RecursiveRollup
extend ActiveSupport::Concern
included do
private
# Genera un rollup recursivo en base al período anterior y aplica una
# operación.
#
# @param :name [String]
# @param :interval_previous [String]
# @param :interval [String]
# @param :operation [Symbol]
# @param :dimensions [Hash]
# @param :beginning [Time]
# @return [Rollup]
def recursive_rollup(name:, interval_previous:, interval:, dimensions:, beginning:, operation: :sum)
Rollup.where(name: name, interval: interval_previous, dimensions: dimensions)
.where('time >= ?', beginning.try(:"beginning_of_#{interval}"))
.group(*dimensions_to_jsonb_query(dimensions))
.rollup(name, interval: interval, update: true) do |rollup|
rollup.try(operation, :value)
end
end
# Reducir las estadísticas calculadas aplicando un rollup sobre el
# intervalo más amplio.
#
# @param :name [String]
# @param :operation [Symbol]
# @param :dimensions [Hash]
# @return [nil]
def reduce_rollup(name:, dimensions:, operation: :sum)
Stat::INTERVALS.reduce do |previous, current|
recursive_rollup(name: name,
interval_previous: previous,
interval: current,
dimensions: dimensions,
beginning: beginning_of_interval,
operation: operation)
# Devolver el intervalo actual
current
end
nil
end
# @param :dimensions [Hash]
# @return [Array]
def dimensions_to_jsonb_query(dimensions)
dimensions.keys.map do |key|
"dimensions->'#{key}'"
end
end
end
end

View file

@ -52,4 +52,12 @@ class PeriodicJob < ApplicationJob
def beginning_of_interval def beginning_of_interval
@beginning_of_interval ||= last_stat.created_at.try(:"beginning_of_#{starting_interval}") @beginning_of_interval ||= last_stat.created_at.try(:"beginning_of_#{starting_interval}")
end end
def stop_file
@stop_file ||= Rails.root.join('tmp', self.class.to_s.tableize)
end
def stop?
File.exist? stop_file
end
end end

View file

@ -2,11 +2,15 @@
# Genera resúmenes de información para poder mostrar estadísticas y se # Genera resúmenes de información para poder mostrar estadísticas y se
# corre regularmente a sí misma. # corre regularmente a sí misma.
class StatCollectionJob < ApplicationJob class StatCollectionJob < PeriodicJob
include RecursiveRollup
STAT_NAME = 'stat_collection_job' STAT_NAME = 'stat_collection_job'
def perform(site_id:, once: true) def perform(site_id:, once: true)
@site = Site.find site_id @site = Site.find site_id
beginning = beginning_of_interval
stat = site.stats.create! name: STAT_NAME
scope.rollup('builds', **options) scope.rollup('builds', **options)
@ -18,44 +22,23 @@ class StatCollectionJob < ApplicationJob
rollup.average(:seconds) rollup.average(:seconds)
end end
# XXX: Es correcto promediar promedios? dimensions = { site_id: site_id }
Stat::INTERVALS.reduce do |previous, current|
rollup(name: 'builds', interval_previous: previous, interval: current)
rollup(name: 'space_used', interval_previous: previous, interval: current, operation: :average)
rollup(name: 'build_time', interval_previous: previous, interval: current, operation: :average)
current reduce_rollup(name: 'builds', operation: :sum, dimensions: dimensions)
end reduce_rollup(name: 'space_used', operation: :average, dimensions: dimensions)
reduce_rollup(name: 'build_time', operation: :average, dimensions: dimensions)
# Registrar que se hicieron todas las recolecciones
site.stats.create! name: STAT_NAME
stat.touch
run_again! unless once run_again! unless once
end end
private private
# Genera un rollup recursivo en base al período anterior y aplica una
# operación.
#
# @return [NilClass]
def rollup(name:, interval_previous:, interval:, operation: :sum)
Rollup.where(name: name, interval: interval_previous)
.where_dimensions(site_id: site.id)
.group("dimensions->'site_id'")
.rollup(name, interval: interval, update: true) do |rollup|
rollup.try(:operation, :value)
end
end
# Los registros a procesar # Los registros a procesar
# #
# @return [ActiveRecord::Relation] # @return [ActiveRecord::Relation]
def scope def scope
@scope ||= site.build_stats @scope ||= site.build_stats.jekyll.where('build_stats.created_at >= ?', beginning_of_interval).group(:site_id)
.jekyll
.where('created_at => ?', beginning_of_interval)
.group(:site_id)
end end
# Las opciones por defecto # Las opciones por defecto
@ -64,4 +47,8 @@ class StatCollectionJob < ApplicationJob
def options def options
@options ||= { interval: starting_interval, update: true } @options ||= { interval: starting_interval, update: true }
end end
def stat_name
STAT_NAME
end
end end

View file

@ -13,94 +13,160 @@
class UriCollectionJob < PeriodicJob class UriCollectionJob < PeriodicJob
# Ignoramos imágenes porque suelen ser demasiadas y no aportan a las # Ignoramos imágenes porque suelen ser demasiadas y no aportan a las
# estadísticas. # estadísticas.
IMAGES = %w[.png .jpg .jpeg .gif .webp].freeze IMAGES = %w[.png .jpg .jpeg .gif .webp .jfif].freeze
STAT_NAME = 'uri_collection_job' STAT_NAME = 'uri_collection_job'
def perform(site_id:, once: true) def perform(site_id:, once: true)
@site = Site.find site_id @site = Site.find site_id
hostnames.each do |hostname| # Obtener el principio del intervalo anterior
uris.each do |uri| beginning_of_interval
return if File.exist? Rails.root.join('tmp', 'uri_collection_job_stop')
AccessLog.where(host: hostname, uri: uri)
.where('created_at >= ?', beginning_of_interval)
.completed_requests
.non_robots
.group(:host, :uri)
.rollup('host|uri', interval: starting_interval, update: true)
# Reducir las estadísticas calculadas aplicando un rollup sobre el
# intervalo más amplio.
Stat::INTERVALS.reduce do |previous, current|
Rollup.where(name: 'host|uri', interval: previous)
.where_dimensions(host: hostname, uri: uri)
.group("dimensions->'host'", "dimensions->'uri'")
.rollup('host|uri', interval: current, update: true) do |rollup|
rollup.sum(:value)
end
# Devolver el intervalo actual
current
end
end
end
# Recordar la última vez que se corrió la tarea # Recordar la última vez que se corrió la tarea
site.stats.create! name: STAT_NAME stat = site.stats.create! name: STAT_NAME
# Columnas a agrupar
columns = Stat::COLUMNS.zip([nil]).to_h
# Las URIs son la fuente de verdad de las visitas, porque son las
# que indican las páginas y recursos descargables, el resto son
# imágenes, CSS, JS y tipografías que no nos aportan números
# significativos.
uri_dimensions = { host: site.hostnames, uri: uris }
host_dimensions = { host: site.hostnames }
# Recorremos todos los hostnames y uris posibles y luego agrupamos
# recursivamente para no tener que recalcular, asumiendo que es más
# rápido buscar en los rollups indexados que en la tabla en bruto.
#
# Los referers solo se agrupan por host.
columns.each_key do |column|
columns[column] = AccessLog.where(**host_dimensions).distinct(column).pluck(column)
end
# Cantidad de visitas por host
rollup(name: 'host', dimensions: host_dimensions, filter: uri_dimensions)
reduce_rollup(name: 'host', dimensions: host_dimensions, filter: uri_dimensions)
# Cantidad de visitas por página/recurso
rollup(name: 'host|uri', dimensions: uri_dimensions)
reduce_rollup(name: 'host|uri', dimensions: uri_dimensions)
# Cantidad de visitas host y parámetro
columns.each_pair do |column, values|
column_name = "host|#{column}"
column_dimensions = { host: site.hostnames }
column_dimensions[column] = values
rollup(name: column_name, dimensions: column_dimensions, filter: uri_dimensions)
reduce_rollup(name: column_name, dimensions: column_dimensions)
end
stat.touch
run_again! unless once run_again! unless once
end end
private private
# Generar un rollup de access logs
#
# @param :name [String]
# @param :beginning [Time]
# @param :dimensions [Hash]
# @param :filter [Hash]
# @return [nil]
def rollup(name:, dimensions:, interval: starting_interval, filter: nil)
AccessLog.where(**(filter || dimensions))
.where('created_at >= ?', beginning_of_interval)
.completed_requests
.non_robots
.group(*dimensions.keys)
.rollup(name, interval: interval, update: true)
end
# Generar rollups con el resto de la información
#
# @param :name [String]
# @param :dimensions [Hash]
# @param :filter [Hash]
# @return [nil]
def reduce_rollup(name:, dimensions:, filter: nil)
Stat::INTERVALS.reduce do |_previous, current|
rollup(name: name, dimensions: dimensions, filter: filter, interval: current)
current
end
nil
end
def stat_name def stat_name
STAT_NAME STAT_NAME
end end
# Obtiene todas las ubicaciones de archivos
#
# @return [String] # @return [String]
# #
# TODO: Cambiar al mergear origin-referer # TODO: Cambiar al mergear origin-referer
def destination def destinations
@destination ||= site.deploys.find_by(type: 'DeployLocal').destination @destinations ||= site.deploys.map(&:destination).compact.select do |d|
end File.directory?(d)
end.map do |d|
# TODO: Cambiar al mergear origin-referer File.realpath(d)
# end.uniq
# @return [Array]
def hostnames
@hostnames ||= site.deploys.map do |deploy|
case deploy
when DeployLocal
site.hostname
when DeployWww
deploy.fqdn
when DeployAlternativeDomain
deploy.hostname.dup.tap do |h|
h.replace(h.end_with?('.') ? h[0..-2] : "#{h}.#{Site.domain}")
end
when DeployHiddenService
deploy.onion
end
end.compact
end end
# Recolecta todas las URIs menos imágenes # Recolecta todas las URIs menos imágenes
# #
# TODO: Para los sitios con DeployLocalizedDomain estamos buscando
# URIs de más.
#
# @return [Array] # @return [Array]
def uris def uris
@uris ||= Dir.chdir destination do @uris ||=
(Dir.glob('**/*.html') + Dir.glob('public/**/*').reject do |p| destinations.map do |destination|
File.directory? p locales.map do |locale|
end.reject do |p| uri = "/#{locale}/".squeeze('/')
p = p.downcase dir = File.join(destination, locale)
IMAGES.any? do |i| next unless File.directory? dir
p.end_with? i
end files(dir).map do |f|
end).map do |uri| uri + f
"/#{uri}"
end end
end end
end.flatten(3).compact
end
# @return [Array]
def locales
@locales ||= ['', site.locales.map(&:to_s)].flatten(1)
end
# @param :dir [String]
# @return [Array]
def files(dir)
Dir.chdir(dir) do
pages = Dir.glob('**/*.html')
files = Dir.glob('public/**/*')
files = remove_directories files
files = remove_images files
[pages, files].flatten(1)
end
end
# @param :files [Array]
# @return [Array]
def remove_directories(files)
files.reject do |f|
File.directory? f
end
end
def remove_images(files)
files.reject do |f|
IMAGES.include? File.extname(f).downcase
end
end end
end end

View file

@ -28,6 +28,7 @@ class DeployLocal < Deploy
# Obtener el tamaño de todos los archivos y directorios (los # Obtener el tamaño de todos los archivos y directorios (los
# directorios son archivos :) # directorios son archivos :)
def size def size
@size ||= begin
paths = [destination, File.join(destination, '**', '**')] paths = [destination, File.join(destination, '**', '**')]
Dir.glob(paths).map do |file| Dir.glob(paths).map do |file|
@ -38,6 +39,7 @@ class DeployLocal < Deploy
end end
end.inject(:+) end.inject(:+)
end end
end
def destination def destination
File.join(Rails.root, '_deploy', site.hostname) File.join(Rails.root, '_deploy', site.hostname)
@ -91,11 +93,7 @@ class DeployLocal < Deploy
end end
def bundle def bundle
if Rails.env.production?
run %(bundle install --no-cache --path="#{gems_dir}") run %(bundle install --no-cache --path="#{gems_dir}")
else
run %(bundle install)
end
end end
def jekyll_build def jekyll_build

100
app/models/deploy_rsync.rb Normal file
View file

@ -0,0 +1,100 @@
# frozen_string_literal: true
# Sincroniza sitios a servidores remotos usando Rsync. El servidor
# remoto tiene que tener rsync instalado.
class DeployRsync < Deploy
store :values, accessors: %i[destination host_keys], coder: JSON
def deploy
ssh? && rsync
end
# El espacio remoto es el mismo que el local
#
# @return [Integer]
def size
deploy_local.size
end
# Devolver el destino o lanzar un error si no está configurado
def destination
values[:destination].tap do |d|
raise(ArgumentError, 'destination no está configurado') if d.blank?
end
end
private
# Verificar la conexión SSH implementando Trust On First Use
#
# TODO: Medir el tiempo que tarda en iniciarse la conexión
#
# @return [Boolean]
def ssh?
user, host = user_host
ssh_available = false
Net::SSH.start(host, user, verify_host_key: tofu, timeout: 5) do |ssh|
if values[:host_keys].blank?
# Guardar las llaves que se encontraron en la primera conexión
values[:host_keys] = ssh.transport.host_keys.map do |host_key|
"#{host_key.ssh_type} #{host_key.fingerprint}"
end
ssh_available = save
else
ssh_available = true
end
end
ssh_available
rescue Exception => e
ExceptionNotifier.notify_exception(e, data: { site: site.id, hostname: host, user: user })
false
end
def env
{
'HOME' => home_dir,
'PATH' => '/usr/bin',
'LANG' => ENV['LANG']
}
end
# Confiar en la primera llave que encontremos, fallar si cambian
#
# @return [Symbol]
def tofu
values[:host_keys].present? ? :always : :accept_new
end
# Devuelve el par user host
#
# @return [Array]
def user_host
destination.split(':', 2).first.split('@', 2).tap do |d|
next unless d.size == 1
d.insert(0, nil)
end
end
# Sincroniza hacia el directorio remoto
#
# @return [Boolean]
def rsync
run %(rsync -aviH --timeout=5 #{Shellwords.escape source}/ #{Shellwords.escape destination}/)
end
# El origen es el destino de la compilación
#
# @return [String]
def source
deploy_local.destination
end
def deploy_local
@deploy_local ||= site.deploys.find_by(type: 'DeployLocal')
end
end

View file

@ -54,10 +54,6 @@ class Site < ApplicationRecord
before_create :clone_skel! before_create :clone_skel!
# Elimina el directorio al destruir un sitio # Elimina el directorio al destruir un sitio
before_destroy :remove_directories! before_destroy :remove_directories!
# Carga el sitio Jekyll una vez que se inicializa el modelo o después
# de crearlo
after_initialize :load_jekyll
after_create :load_jekyll
# Cambiar el nombre del directorio # Cambiar el nombre del directorio
before_update :update_name! before_update :update_name!
before_save :add_private_key_if_missing! before_save :add_private_key_if_missing!
@ -101,6 +97,26 @@ class Site < ApplicationRecord
"https://#{hostname}#{slash ? '/' : ''}" "https://#{hostname}#{slash ? '/' : ''}"
end end
# TODO: Cambiar al mergear origin-referer
#
# @return [Array]
def hostnames
@hostnames ||= deploys.map do |deploy|
case deploy
when DeployLocal
hostname
when DeployWww
deploy.fqdn
when DeployAlternativeDomain
deploy.hostname.dup.tap do |h|
h.replace(h.end_with?('.') ? h[0..-2] : "#{h}.#{Site.domain}")
end
when DeployHiddenService
deploy.onion
end
end.compact
end
# Obtiene los dominios alternativos # Obtiene los dominios alternativos
# #
# @return Array # @return Array
@ -123,7 +139,9 @@ class Site < ApplicationRecord
# #
# @return Array # @return Array
def urls(slash: true) def urls(slash: true)
alternative_urls(slash: slash) << url(slash: slash) @urls ||= hostnames.map do |h|
"https://#{h}#{slash ? '/' : ''}"
end
end end
def invitade?(usuarie) def invitade?(usuarie)
@ -166,8 +184,10 @@ class Site < ApplicationRecord
end end
# Similar a site.i18n en jekyll-locales # Similar a site.i18n en jekyll-locales
#
# @return [Hash]
def i18n def i18n
data[I18n.locale.to_s] data[I18n.locale.to_s] || {}
end end
# Devuelve el idioma por defecto del sitio, el primero de la lista. # Devuelve el idioma por defecto del sitio, el primero de la lista.
@ -331,9 +351,18 @@ class Site < ApplicationRecord
status == 'building' status == 'building'
end end
def jekyll?
File.directory? path
end
def jekyll def jekyll
run_in_path do @jekyll ||=
@jekyll ||= Jekyll::Site.new(configuration) begin
install_gems
Jekyll::Site.new(configuration).tap do |site|
site.reader = JekyllData::Reader.new(site) if site.theme
end
end end
end end
@ -380,9 +409,6 @@ class Site < ApplicationRecord
@configuration[unneeded] = [] if @configuration.key? unneeded @configuration[unneeded] = [] if @configuration.key? unneeded
end end
# Eliminar el theme si no es una gema válida
@configuration.delete('theme') unless theme_available?
# Si estamos usando nuestro propio plugin de i18n, los posts están # Si estamos usando nuestro propio plugin de i18n, los posts están
# en "colecciones" # en "colecciones"
locales.map(&:to_s).each do |i| locales.map(&:to_s).each do |i|
@ -392,20 +418,6 @@ class Site < ApplicationRecord
@configuration @configuration
end end
# Lista los nombres de las plantillas disponibles como gemas,
# tomándolas dinámicamente de las que agreguemos en el grupo :themes
# del Gemfile.
def available_themes
@available_themes ||= Bundler.load.current_dependencies.select do |gem|
gem.groups.include? :themes
end.map(&:name)
end
# Detecta si el tema actual es una gema
def theme_available?
available_themes.include? design&.gem
end
# Devuelve el dominio actual # Devuelve el dominio actual
def self.domain def self.domain
ENV.fetch('SUTTY', 'sutty.nl') ENV.fetch('SUTTY', 'sutty.nl')
@ -444,7 +456,7 @@ class Site < ApplicationRecord
# Clona el esqueleto de Sutty para crear el sitio nuevo, no pasa nada # Clona el esqueleto de Sutty para crear el sitio nuevo, no pasa nada
# si el sitio ya existe # si el sitio ya existe
def clone_skel! def clone_skel!
return if File.directory? path return if jekyll?
Rugged::Repository.clone_at ENV['SKEL_SUTTY'], path Rugged::Repository.clone_at ENV['SKEL_SUTTY'], path
end end
@ -470,7 +482,7 @@ class Site < ApplicationRecord
config.theme = design.gem unless design.no_theme? config.theme = design.gem unless design.no_theme?
config.description = description config.description = description
config.title = title config.title = title
config.url = url config.url = url(slash: false)
config.hostname = hostname config.hostname = hostname
end end
@ -527,4 +539,11 @@ class Site < ApplicationRecord
def run_in_path(&block) def run_in_path(&block)
Dir.chdir path, &block Dir.chdir path, &block
end end
def install_gems
return unless persisted?
return if Rails.root.join('_storage', 'gems', name).directory?
deploys.find_by_type('DeployLocal').send(:bundle)
end
end end

View file

@ -0,0 +1,3 @@
# frozen_string_literal: true
SiteBlazer = Struct.new(:site)

View file

@ -3,8 +3,16 @@
# Registran cuándo fue la última recolección de datos. # Registran cuándo fue la última recolección de datos.
class Stat < ApplicationRecord class Stat < ApplicationRecord
# XXX: Los intervalos van en orden de mayor especificidad a menor # XXX: Los intervalos van en orden de mayor especificidad a menor
INTERVALS = %i[day month year].freeze INTERVALS = %i[day].freeze
RESOURCES = %i[builds space_used build_time].freeze RESOURCES = %i[builds space_used build_time].freeze
COLUMNS = %i[http_referer geoip2_data_country_name].freeze
belongs_to :site belongs_to :site
# El intervalo por defecto
#
# @return [Symbol]
def self.default_interval
INTERVALS.first
end
end end

View file

@ -13,6 +13,8 @@ class Usuarie < ApplicationRecord
has_many :roles has_many :roles
has_many :sites, through: :roles has_many :sites, through: :roles
has_many :blazer_audits, foreign_key: 'user_id', class_name: 'Blazer::Audit'
has_many :blazer_queries, foreign_key: 'creator_id', class_name: 'Blazer::Query'
def name def name
email.split('@', 2).first email.split('@', 2).first

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
# Les invitades no pueden ver las estadísticas (aun)
SiteBlazerPolicy = Struct.new(:usuarie, :site_blazer) do
def home?
site_blazer&.site&.usuarie? usuarie
end
alias_method :show?, :home?
end

View file

@ -9,6 +9,7 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
self.site = Site.new params self.site = Site.new params
add_role temporal: false, rol: 'usuarie' add_role temporal: false, rol: 'usuarie'
sync_nodes
I18n.with_locale(usuarie&.lang&.to_sym || I18n.default_locale) do I18n.with_locale(usuarie&.lang&.to_sym || I18n.default_locale) do
site.save && site.save &&
@ -144,4 +145,11 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
PostService.new(site: site, usuarie: usuarie, post: post, PostService.new(site: site, usuarie: usuarie, post: post,
params: params).update params: params).update
end end
# Crea los deploys necesarios para sincronizar a otros nodos de Sutty
def sync_nodes
Rails.application.nodes.each do |node|
site.deploys.build(type: 'DeployRsync', destination: "sutty@#{node}:#{site.hostname}")
end
end
end end

View file

@ -0,0 +1,5 @@
%ul
- @checks.each do |check|
%li
= check.query.name
= check.state

View file

@ -0,0 +1,30 @@
!!!
%html
%head
%meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
%body{:style => "font-family: 'Helvetica Neue', Arial, Helvetica; font-size: 14px; color: #333;"}
- if @error
%p= @error
- elsif @rows_count > 0 && @check_type == "bad_data"
%p
- if @rows_count <= 10
= pluralize(@rows_count, "row")
- else
Showing 10 of #{@rows_count} rows
%table{:style => "width: 100%; border-spacing: 0; border-collapse: collapse;"}
%thead
%tr
- @columns.first(5).each do |column|
%th{:style => "padding: 8px; line-height: 1.4; text-align: left; vertical-align: bottom; border-bottom: 2px solid #ddd; width: #{(100 / @columns.size).round(2)}%;"}
= column
%tbody
- @rows.first(10).each do |row|
%tr
- @columns.first(5).each_with_index do |column, i|
%td{:style => "padding: 8px; line-height: 1.4; vertical-align: top; border-top: 1px solid #ddd;"}
- value = row[i]
- if @column_types[i] == "time" && value.to_s.length > 10
- value = Time.parse(value).in_time_zone(Blazer.time_zone) rescue value
= value
- if @columns.size > 5
%p{:style => "color: #999;"} Only first 5 columns shown

View file

@ -0,0 +1,9 @@
#queries
%table.table
%tbody.list
- @queries.each do |query|
%tr
-#
Por alguna razón no tenemos acceso a query_path para poder
generar la URL según Rails
%td= link_to query[:name], "/sites/#{params[:site_id]}/stats/queries/#{query.to_param}"

View file

@ -0,0 +1,51 @@
- blazer_title @query.name
.container
.row
.col-12
%h1= @query.name
- if @query.description.present?
%p.lead= @query.description
- unless @result.chart_type.blank?
.col-12
- case @result.chart_type
- when 'line'
= line_chart @chart_data, **@chart_options
- when 'line2'
= line_chart @chart_data, **@chart_options
- when 'pie'
= pie_chart @chart_data, **@chart_options
- when 'bar'
= column_chart @chart_data, **@chart_options
- when 'bar2'
= column_chart @chart_data, **@chart_options
- when 'scatter'
= scatter_chart @chart_data, **@chart_options
.col-12
%table.table
%thead
%tr
- @result.columns.each do |key|
- next if key.include? 'ciphertext'
- next if key.include? 'encrypted'
%th.position-sticky.background-white{ style: 'top: 0' }= t("blazer.columns.#{key}", default: key.titleize)
%tbody
- @result.rows.each do |row|
%tr
- row.each_with_index do |v, i|
- k = @result.columns[i]
- next if k.include? 'ciphertext'
- next if k.include? 'encrypted'
%td
- if v.is_a?(Time)
- v = blazer_time_value(@data_source, k, v)
- unless v.nil?
- if v.is_a?(String) && v.empty?
%span.text-muted= t('.empty')
- elsif @data_source.linked_columns[k]
= link_to blazer_format_value(k, v), @data_source.linked_columns[k].gsub('{value}', u(v.to_s)), target: '_blank'
- else
= blazer_format_value(k, v)
- if (v2 = (@result.boom[k] || {})[v.nil? ? v : v.to_s])
%span.text-muted= v2

View file

@ -0,0 +1 @@
-# nada

View file

@ -12,7 +12,7 @@
- else - else
%span.line-clamp-1= link_to crumb.name, crumb.url %span.line-clamp-1= link_to crumb.name, crumb.url
- if current_usuarie - if @current_usuarie || current_usuarie
%ul.navbar-nav %ul.navbar-nav
- if @site&.tienda? - if @site&.tienda?
%li.nav-item %li.nav-item
@ -20,7 +20,7 @@
role: 'button', class: 'btn' role: 'button', class: 'btn'
%li.nav-item %li.nav-item
= link_to t('.logout'), destroy_usuarie_session_path, = link_to t('.logout'), main_app.destroy_usuarie_session_path,
method: :delete, role: 'button', class: 'btn' method: :delete, role: 'button', class: 'btn'
- else - else
- other_locale = I18n.available_locales.find { |locale| locale != I18n.locale } - other_locale = I18n.available_locales.find { |locale| locale != I18n.locale }

View file

@ -0,0 +1,14 @@
!!!
%html
%head
%meta{content: 'text/html; charset=UTF-8', 'http-equiv': 'Content-Type'}/
%title= blazer_title ? blazer_title : 'Sutty'
%meta{charset: 'utf-8'}/
= favicon_link_tag 'blazer/favicon.png'
= stylesheet_link_tag 'application'
= javascript_pack_tag 'blazer', 'data-turbolinks-track': 'reload'
= csrf_meta_tags
%body{ class: yield(:body) }
.container-fluid#sutty
= render 'layouts/breadcrumb'
= yield

View file

@ -1,7 +1,5 @@
.form-group .form-group
- if metadata.static_file - if metadata.static_file
= hidden_field_tag "#{base}[#{attribute}][path]", metadata.value['path']
- case metadata.static_file.blob.content_type - case metadata.static_file.blob.content_type
- when %r{\Avideo/} - when %r{\Avideo/}
= video_tag url_for(metadata.static_file), = video_tag url_for(metadata.static_file),
@ -14,13 +12,17 @@
- else - else
= link_to t('posts.attribute_ro.file.download'), = link_to t('posts.attribute_ro.file.download'),
url_for(metadata.static_file) url_for(metadata.static_file)
-# Mantener el valor si no enviamos ninguna imagen
= hidden_field_tag "#{base}[#{attribute}][path]", metadata.value['path']
-# Los archivos requeridos solo se pueden reemplazar
- unless metadata.required
.custom-control.custom-switch .custom-control.custom-switch
= check_box_tag "#{base}[#{attribute}][path]", '', false, id: "#{base}_#{attribute}_destroy", class: 'custom-control-input' = check_box_tag "#{base}[#{attribute}][path]", '', false, id: "#{base}_#{attribute}_destroy", class: 'custom-control-input'
= label_tag "#{base}_#{attribute}_destroy", t('posts.attributes.file.destroy'), class: 'custom-control-label' = label_tag "#{base}_#{attribute}_destroy", t('posts.attributes.file.destroy'), class: 'custom-control-label'
.custom-file .custom-file
= file_field(*field_name_for(base, attribute, :path), = file_field(*field_name_for(base, attribute, :path),
**field_options(attribute, metadata), **field_options(attribute, metadata, required: (metadata.required && !metadata.path?)),
class: "custom-file-input #{invalid(post, attribute)}", class: "custom-file-input #{invalid(post, attribute)}",
data: { preview: "#{attribute}-preview" }) data: { preview: "#{attribute}-preview" })
= label_tag "#{base}_#{attribute}_path", = label_tag "#{base}_#{attribute}_path",
@ -30,7 +32,7 @@
.form-group .form-group
= label_tag "#{base}_#{attribute}_description", = label_tag "#{base}_#{attribute}_description",
post_label_t(attribute, :description, post: post) post_label_t(attribute, :description, post: post, required: false)
= text_field(*field_name_for(base, attribute, :description), = text_field(*field_name_for(base, attribute, :description),
value: metadata.value['description'], value: metadata.value['description'],
dir: dir, lang: locale, dir: dir, lang: locale,

View file

@ -1,5 +1,5 @@
.form-group .form-group
- if metadata.uploaded? - if metadata.static_file
= image_tag url_for(metadata.static_file), = image_tag url_for(metadata.static_file),
alt: metadata.value['description'], alt: metadata.value['description'],
class: 'img-fluid', class: 'img-fluid',
@ -37,4 +37,3 @@
**field_options(attribute, metadata, required: false)) **field_options(attribute, metadata, required: false))
= render 'posts/attribute_feedback', = render 'posts/attribute_feedback',
post: post, attribute: [attribute, :description], metadata: metadata post: post, attribute: [attribute, :description], metadata: metadata

View file

@ -15,6 +15,9 @@
- else - else
%td= link_to t('posts.filter'), site_posts_path(@site, **@filter_params.merge(layout: layout.value)), class: 'btn btn-secondary btn-sm' %td= link_to t('posts.filter'), site_posts_path(@site, **@filter_params.merge(layout: layout.value)), class: 'btn btn-secondary btn-sm'
- if policy(@site_stat).index?
= link_to t('stats.index.title'), site_stats_path(@site), class: 'btn'
- if policy(@site).edit? - if policy(@site).edit?
= link_to t('sites.edit.btn', site: @site.title), edit_site_path(@site), class: 'btn' = link_to t('sites.edit.btn', site: @site.title), edit_site_path(@site), class: 'btn'
@ -71,8 +74,8 @@
%table.table{ data: { controller: 'reorder' } } %table.table{ data: { controller: 'reorder' } }
%caption.sr-only= t('posts.caption') %caption.sr-only= t('posts.caption')
%thead %thead
%tr %tr.sticky-top
%th.border-0.background-white.position-sticky{ style: 'top: 0; z-index: 2', colspan: '4' } %th.border-0{ colspan: '4' }
.d-flex.flex-row.justify-content-between .d-flex.flex-row.justify-content-between
%div %div
= submit_tag t('posts.reorder.submit'), class: 'btn' = submit_tag t('posts.reorder.submit'), class: 'btn'

View file

@ -14,7 +14,7 @@
%table.table.table-condensed %table.table.table-condensed
%tbody %tbody
- @sites.each do |site| - @sites.each do |site|
- next unless site.jekyll - next unless site.jekyll?
- rol = current_usuarie.rol_for_site(site) - rol = current_usuarie.rol_for_site(site)
-# -#
TODO: Solo les usuaries cachean porque tenemos que separar TODO: Solo les usuaries cachean porque tenemos que separar

View file

@ -6,32 +6,57 @@
%p %p
%small %small
= t('.last_update') = t('.last_update')
%time{ datetime: @last_stat.created_at } %time{ datetime: @last_stat.updated_at }
#{time_ago_in_words @last_stat.created_at}. #{time_ago_in_words @last_stat.updated_at}.
.mb-5 %form.mb-5.form-inline{ method: 'get' }
- Stat::INTERVALS.each do |interval| - Stat::INTERVALS.each do |interval|
= link_to t(".#{interval}"), site_stats_path(interval: interval, urls: params[:urls]), class: "btn #{'btn-primary active' if @interval == interval}" = link_to t(".#{interval}"), site_stats_path(interval: interval, urls: params[:urls], period_start: params[:period_start].to_date.try(:"beginning_of_#{interval}").to_date, period_end: params[:period_end]), class: "mb-0 btn #{'btn-primary active' if @interval == interval}"
%input.form-control{ type: 'date', name: :period_start, value: params[:period_start] }
%input.form-control{ type: 'date', name: :period_end, value: params[:period_end] }
%button.btn.mb-0{ type: 'submit' }= t('.filter')
.mb-5 .mb-5
%h2= t('.host.title', count: @hostnames.size) %h2= t('.host.title', count: @hostnames.size)
%p.lead= t('.host.description') %p.lead= t('.host.description')
= line_chart site_stats_host_path(@chart_params), **@chart_options = line_chart site_stats_host_path(@chart_params), **@chart_options
.mb-5 #custom-urls.mb-5
%h2= t('.urls.title') %h2= t('.urls.title')
%p.lead= t('.urls.description') %p.lead= t('.urls.description')
%form %form{ method: 'get', action: '#custom-urls' }
%input{ type: 'hidden', name: 'interval', value: @interval } %input{ type: 'hidden', name: 'interval', value: @interval }
.form-group .form-group
%label{ for: 'urls' }= t('.urls.label') %label{ for: 'urls' }= t('.urls.label')
%textarea#urls.form-control{ name: 'urls', autocomplete: 'on', required: true, rows: @normalized_urls.size, aria_describedby: 'help-urls' }= @normalized_urls.join("\n") %textarea#urls.form-control{ name: 'urls', autocomplete: 'on', required: true, rows: @normalized_urls.size + 1, aria_describedby: 'help-urls' }= @normalized_urls.join("\n")
%small#help-urls.feedback.form-text.text-muted= t('.urls.help') %small#help-urls.feedback.form-text.text-muted= t('.urls.help')
.form-group .form-group
%button.btn{ type: 'submit' }= t('.urls.submit') %button.btn{ type: 'submit' }= t('.urls.submit')
- if @normalized_urls.present? - if @normalized_urls.present?
= line_chart site_stats_uris_path(urls: params[:urls], **@chart_params), **@chart_options = line_chart site_stats_uris_path(urls: @normalized_urls, **@chart_params), **@chart_options
.row.mb-5.row-cols-1.row-cols-md-2
- @columns.each_pair do |column, values|
- next if values.blank?
.col.mb-5
%h2= t(".columns.#{column}.title")
%p.lead= t(".columns.#{column}.description")
%table.table
%colgroup
%col
%col
%thead
%tr.sticky-top
%th{ scope: 'col' }= t(".columns.#{column}.column")
%th{ scope: 'col' }= t('.columns.visits')
%tfoot
%tbody
- values.each_pair do |col, val|
%tr
%th{ scope: 'row', style: 'word-break: break-all' }= col.blank? ? t(".columns.#{column}.empty") : col
%td= val
.mb-5 .mb-5
%h2= t('.resources.title') %h2= t('.resources.title')
%p.lead= t('.resources.description') %p.lead= t('.resources.description')

15
bin/access_logs Executable file
View file

@ -0,0 +1,15 @@
#!/bin/sh
set -e
# Volcar y eliminar todos los access logs de dos días atrás
date="`dateadd today -1d`"
file="/srv/http/_storage/${date}.psql.gz"
test -n "${date}"
test ! -f "${file}"
psql -h postgresql "${DATABASE:-sutty}" sutty <<SQL | gzip > "${file}"
begin;
copy (select * from access_logs where created_at < '${date}') to stdout;
delete from access_logs where created_at < '${date}';
commit;
SQL

View file

@ -39,6 +39,7 @@ module Sutty
config.active_storage.variant_processor = :vips config.active_storage.variant_processor = :vips
config.to_prepare do config.to_prepare do
# Load application's model / class decorators
Dir.glob(File.join(File.dirname(__FILE__), '..', 'app', '**', '*_decorator.rb')).sort.each do |c| Dir.glob(File.join(File.dirname(__FILE__), '..', 'app', '**', '*_decorator.rb')).sort.each do |c|
Rails.configuration.cache_classes ? require(c) : load(c) Rails.configuration.cache_classes ? require(c) : load(c)
end end
@ -55,5 +56,9 @@ module Sutty
EmailAddress::Config.error_messages translations.transform_keys(&:to_s), locale.to_s EmailAddress::Config.error_messages translations.transform_keys(&:to_s), locale.to_s
end end
end end
def nodes
@nodes ||= ENV.fetch('SUTTY_NODES', '').split(',')
end
end end
end end

View file

@ -50,7 +50,7 @@ user_method: current_usuarie
user_name: email user_name: email
# custom before_action to use for auth # custom before_action to use for auth
# before_action_method: require_admin before_action_method: require_usuarie
# email to send checks from # email to send checks from
from_email: blazer@<%= ENV.fetch('SUTTY', 'sutty.nl') %> from_email: blazer@<%= ENV.fetch('SUTTY', 'sutty.nl') %>

View file

@ -37,6 +37,13 @@ end
# #
# TODO: Aplicar monkey patches en otro lado... # TODO: Aplicar monkey patches en otro lado...
module Jekyll module Jekyll
Site.class_eval do
def configure_theme
self.theme = nil
self.theme = Jekyll::Theme.new(config['theme'], self) unless config['theme'].nil?
end
end
Reader.class_eval do Reader.class_eval do
# No necesitamos otros posts # No necesitamos otros posts
def retrieve_posts(_); end def retrieve_posts(_); end
@ -69,6 +76,46 @@ module Jekyll
end end
end end
Theme.class_eval do
attr_reader :site
def initialize(name, site)
@name = name.downcase.strip
@site = site
end
def root
@root ||= begin
lockfile = Bundler::LockfileParser.new(File.read(site.in_source_dir('Gemfile.lock')))
spec = lockfile.specs.find do |spec|
spec.name == name
end
ruby_version = Gem::Version.new(RUBY_VERSION)
ruby_version.canonical_segments[2] = 0
base_path = Rails.root.join('_storage', 'gems', File.basename(site.source), 'ruby',
ruby_version.canonical_segments.join('.'))
File.realpath(
case spec.source
when Bundler::Source::Git
File.join(base_path, 'bundler', 'gems', spec.source.extension_dir_name)
when Bundler::Source::Rubygems
File.join(base_path, 'gems', spec.full_name)
end
)
end
end
def runtime_dependencies
[]
end
private
def gemspec; end
end
# No necesitamos los archivos de la plantilla # No necesitamos los archivos de la plantilla
ThemeAssetsReader.class_eval do ThemeAssetsReader.class_eval do
def read; end def read; end

View file

@ -102,6 +102,10 @@ en:
title: Alternative domain name title: Alternative domain name
success: Success! success: Success!
error: Error error: Error
deploy_rsync:
title: Synchronize to backup server
success: Success!
error: Error
help: You can contact us by replying to this e-mail help: You can contact us by replying to this e-mail
maintenance_mailer: maintenance_mailer:
notice: notice:
@ -254,7 +258,7 @@ en:
help: | help: |
These statistics show information about how your site is generated and These statistics show information about how your site is generated and
how many resources it uses. how many resources it uses.
last_update: 'Updated every hour. Last update on ' last_update: 'Updated daily. Last update on '
empty: 'There is no enough information yet. We invite you to come back in %{please_return_at}!' empty: 'There is no enough information yet. We invite you to come back in %{please_return_at}!'
loading: 'Loading...' loading: 'Loading...'
hour: 'Hourly' hour: 'Hourly'
@ -285,7 +289,19 @@ en:
description: 'Average storage space used by your site.' description: 'Average storage space used by your site.'
build_time: build_time:
title: 'Publication time' title: 'Publication time'
description: 'Average time your site takes to build.' description: 'Average time your site takes to build, from pressing "Publish changes" to actually being available on your site.'
columns:
visits: "Visits"
http_referer:
title: "Referers"
description: "Visits by origin"
column: "Referer"
empty: "(direct visit)"
geoip2_data_country_name:
title: "Countries"
description: "Visits by country"
column: "Country"
empty: "(couldn't detect country)"
sites: sites:
donations: donations:
url: 'https://donaciones.sutty.nl/en/' url: 'https://donaciones.sutty.nl/en/'
@ -612,3 +628,14 @@ en:
edit: 'Editing' edit: 'Editing'
usuaries: usuaries:
index: 'Users' index: 'Users'
stats:
index: 'Statistics'
blazer:
columns:
total: 'Total'
dia: 'Date'
date: 'Date'
visitas: 'Visits'
queries:
show:
empty: '(empty)'

View file

@ -102,6 +102,10 @@ es:
title: Dominio alternativo title: Dominio alternativo
success: ¡Éxito! success: ¡Éxito!
error: Hubo un error error: Hubo un error
deploy_rsync:
title: Sincronizar al servidor alternativo
success: ¡Éxito!
error: Hubo un error
help: Por cualquier duda, responde este correo para contactarte con nosotres. help: Por cualquier duda, responde este correo para contactarte con nosotres.
maintenance_mailer: maintenance_mailer:
notice: notice:
@ -259,7 +263,7 @@ es:
help: | help: |
Las estadísticas visibilizan información sobre cómo se genera y Las estadísticas visibilizan información sobre cómo se genera y
cuántos recursos utiliza tu sitio. cuántos recursos utiliza tu sitio.
last_update: 'Actualizadas cada hora. Última actualización hace ' last_update: 'Actualizadas diariamente. Última actualización hace '
empty: 'Todavía no hay información suficiente. Te invitamos a volver en %{please_return_at} :)' empty: 'Todavía no hay información suficiente. Te invitamos a volver en %{please_return_at} :)'
loading: 'Cargando...' loading: 'Cargando...'
hour: 'Por hora' hour: 'Por hora'
@ -290,7 +294,19 @@ es:
description: 'Espacio en disco que ocupa en promedio tu sitio.' description: 'Espacio en disco que ocupa en promedio tu sitio.'
build_time: build_time:
title: 'Tiempo de publicación' title: 'Tiempo de publicación'
description: 'Tiempo promedio que toma en publicarse tu sitio.' description: 'Tiempo que tarda el sitio en generarse, desde que usas el botón "Publicar cambios" hasta que los puedes ver en el sitio'
columns:
visits: "Visitas"
http_referer:
title: "Referencias"
description: "Orígenes de las visitas"
column: "Referencia"
empty: "(visita directa)"
geoip2_data_country_name:
title: "Países"
description: "Cantidad de visitas por país"
column: "País"
empty: "(no se pudo detectar el país)"
sites: sites:
donations: donations:
url: 'https://donaciones.sutty.nl/' url: 'https://donaciones.sutty.nl/'
@ -620,3 +636,14 @@ es:
edit: 'Editando' edit: 'Editando'
usuaries: usuaries:
index: 'Usuaries' index: 'Usuaries'
stats:
index: 'Estadísticas'
blazer:
columns:
total: 'Total'
dia: 'Fecha'
date: 'Fecha'
visitas: 'Visitas'
queries:
show:
empty: '(vacío)'

View file

@ -4,8 +4,6 @@ Rails.application.routes.draw do
devise_for :usuaries devise_for :usuaries
get '/.well-known/change-password', to: redirect('/usuaries/edit') get '/.well-known/change-password', to: redirect('/usuaries/edit')
mount Blazer::Engine, at: 'blazer'
root 'application#index' root 'application#index'
constraints(Constraints::ApiSubdomain.new) do constraints(Constraints::ApiSubdomain.new) do

View file

@ -3,8 +3,6 @@
# Blazer # Blazer
class InstallBlazer < ActiveRecord::Migration[6.0] class InstallBlazer < ActiveRecord::Migration[6.0]
def change def change
return unless Rails.env.production?
create_table :blazer_queries do |t| create_table :blazer_queries do |t|
t.references :creator t.references :creator
t.string :name t.string :name

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
# La recolección de estadísticas podría pertenecer a un sitio
class AddSiteToStats < ActiveRecord::Migration[6.1]
def change
add_belongs_to :stats, :site, index: true, null: true
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
# Agregarle un nombre a la estadística
class AddNameToStats < ActiveRecord::Migration[6.1]
def change
add_column :stats, :name, :string, null: false
add_index :stats, :name, using: 'hash'
end
end

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
# Agrega un DeployRsync hacia los servidores alternativos para cada
# sitio
class AddDeployRsyncToSites < ActiveRecord::Migration[6.1]
def up
Site.find_each do |site|
SiteService.new(site: site).send :sync_nodes
site.save
end
end
def down
DeployRsync.destroy_all
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2021_05_14_165639) do ActiveRecord::Schema.define(version: 2021_10_22_225449) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm" enable_extension "pg_trgm"
@ -64,6 +64,7 @@ ActiveRecord::Schema.define(version: 2021_05_14_165639) do
t.string "remote_user" t.string "remote_user"
t.boolean "crawler", default: false t.boolean "crawler", default: false
t.string "http_referer" t.string "http_referer"
t.datetime "created_at", precision: 6
t.index ["geoip2_data_city_name"], name: "index_access_logs_on_geoip2_data_city_name" t.index ["geoip2_data_city_name"], name: "index_access_logs_on_geoip2_data_city_name"
t.index ["geoip2_data_country_name"], name: "index_access_logs_on_geoip2_data_country_name" t.index ["geoip2_data_country_name"], name: "index_access_logs_on_geoip2_data_country_name"
t.index ["host"], name: "index_access_logs_on_host" t.index ["host"], name: "index_access_logs_on_host"
@ -303,6 +304,15 @@ ActiveRecord::Schema.define(version: 2021_05_14_165639) do
t.index ["usuarie_id"], name: "index_roles_on_usuarie_id" t.index ["usuarie_id"], name: "index_roles_on_usuarie_id"
end end
create_table "rollups", force: :cascade do |t|
t.string "name", null: false
t.string "interval", null: false
t.datetime "time", null: false
t.jsonb "dimensions", default: {}, null: false
t.float "value"
t.index ["name", "interval", "time", "dimensions"], name: "index_rollups_on_name_and_interval_and_time_and_dimensions", unique: true
end
create_table "sites", force: :cascade do |t| create_table "sites", force: :cascade do |t|
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
@ -324,6 +334,15 @@ ActiveRecord::Schema.define(version: 2021_05_14_165639) do
t.index ["name"], name: "index_sites_on_name", unique: true t.index ["name"], name: "index_sites_on_name", unique: true
end end
create_table "stats", force: :cascade do |t|
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.bigint "site_id"
t.string "name", null: false
t.index ["name"], name: "index_stats_on_name", using: :hash
t.index ["site_id"], name: "index_stats_on_site_id"
end
create_table "usuaries", force: :cascade do |t| create_table "usuaries", force: :cascade do |t|
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
@ -370,4 +389,10 @@ new.indexed_content := to_tsvector(('pg_catalog.' || new.dictionary)::regconfig,
SQL_ACTIONS SQL_ACTIONS
end end
create_trigger("access_logs_before_insert_row_tr", :compatibility => 1).
on("access_logs").
before(:insert) do
"new.created_at := to_timestamp(new.msec);"
end
end end

11
lib/tasks/stats.rake Normal file
View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
namespace :stats do
desc 'Process stats'
task process_all: :environment do
Site.all.pluck(:id).each do |site_id|
UriCollectionJob.perform_now site_id: site_id, once: true
StatCollectionJob.perform_now site_id: site_id, once: true
end
end
end

View file

@ -25,3 +25,13 @@ check program blazer
with path "/usr/local/bin/sutty blazer" with path "/usr/local/bin/sutty blazer"
every 61 cycles every 61 cycles
if status != 0 then alert if status != 0 then alert
check program access_logs
with path "/srv/http/bin/access_logs" as uid "app" and gid "www-data"
every "0 0 * * *"
if status != 0 then alert
check program stats
with path "/usr/bin/foreman run -f /srv/Procfile -d /srv stats" as uid "rails" gid "www-data"
every "0 1 * * *"
if status != 0 then alert

View file

@ -2257,7 +2257,7 @@ collection-visit@^1.0.0:
map-visit "^1.0.0" map-visit "^1.0.0"
object-visit "^1.0.0" object-visit "^1.0.0"
color-convert@^1.9.0, color-convert@^1.9.1: color-convert@^1.9.0, color-convert@^1.9.1, color-convert@^1.9.3:
version "1.9.3" version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@ -5029,6 +5029,11 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
moment@^2.10.2:
version "2.29.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
move-concurrently@^1.0.1: move-concurrently@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"