Compare commits

..

No commits in common. "f8c8d45fde9e3e54832e83403040b7a3a8e97a6a" and "rails" have entirely different histories.

40 changed files with 113 additions and 852 deletions

View file

@ -1,4 +1,3 @@
RAILS_GROUPS=assets
DELEGATE=athshe.sutty.nl DELEGATE=athshe.sutty.nl
HAINISH=../haini.sh/haini.sh HAINISH=../haini.sh/haini.sh
DATABASE= DATABASE=

View file

@ -1,7 +0,0 @@
pipeline:
chequear:
image: registry.nulo.in/sutty/haini.sh:root
commands:
- make bundle hain="sh -c"
- make brakeman hain="sh -c"
- make rubocop hain="sh -c"

View file

@ -2,7 +2,7 @@
# el mismo repositorio de trabajo. Cuando tengamos CI/CD algunas cosas # el mismo repositorio de trabajo. Cuando tengamos CI/CD algunas cosas
# como el tarball van a tener que cambiar porque ya vamos a haber hecho # como el tarball van a tener que cambiar porque ya vamos a haber hecho
# un clone/pull limpio. # un clone/pull limpio.
FROM alpine:3.13.6 AS build FROM alpine:3.13.5 AS build
MAINTAINER "f <f@sutty.nl>" MAINTAINER "f <f@sutty.nl>"
ARG RAILS_MASTER_KEY ARG RAILS_MASTER_KEY
@ -14,10 +14,10 @@ ENV SECRET_KEY_BASE solo_es_necesaria_para_correr_rake
ENV RAILS_ENV production ENV RAILS_ENV production
ENV RAILS_MASTER_KEY=$RAILS_MASTER_KEY ENV RAILS_MASTER_KEY=$RAILS_MASTER_KEY
RUN apk add --no-cache libxslt libxml2 tzdata ruby ruby-json ruby-bigdecimal ruby-rake RUN apk add --no-cache libxslt libxml2 tzdata ruby ruby-bundler ruby-json ruby-bigdecimal ruby-rake
RUN apk add --no-cache postgresql-libs git yarn brotli libssh2 python3 RUN apk add --no-cache postgresql-libs git yarn brotli libssh2 python3
RUN test "2.7.4" = `ruby -e 'puts RUBY_VERSION'` RUN test "2.7.3" = `ruby -e 'puts RUBY_VERSION'`
# https://github.com/rubygems/rubygems/issues/2918 # https://github.com/rubygems/rubygems/issues/2918
# https://gitlab.alpinelinux.org/alpine/aports/issues/10808 # https://gitlab.alpinelinux.org/alpine/aports/issues/10808
@ -29,7 +29,7 @@ RUN cd /usr/lib/ruby/2.7.0 && patch -Np 0 -i /tmp/rubygems-platform-musl.patch
RUN addgroup -g 82 -S www-data RUN addgroup -g 82 -S www-data
RUN adduser -s /bin/sh -G www-data -h /home/app -D app RUN adduser -s /bin/sh -G www-data -h /home/app -D app
RUN install -dm750 -o app -g www-data /home/app/sutty RUN install -dm750 -o app -g www-data /home/app/sutty
RUN gem install --no-document bundler:2.1.4 RUN gem install --no-document bundler
# Empezamos con la usuaria app # Empezamos con la usuaria app
USER app USER app
@ -39,8 +39,7 @@ WORKDIR /home/app/sutty
# Copiamos solo el Gemfile para poder instalar las gemas necesarias # Copiamos solo el Gemfile para poder instalar las gemas necesarias
COPY --chown=app:www-data ./Gemfile . COPY --chown=app:www-data ./Gemfile .
COPY --chown=app:www-data ./Gemfile.lock . COPY --chown=app:www-data ./Gemfile.lock .
RUN bundle config set no-cache true RUN bundle config set no-cache 'true'
RUN bundle config set specific_platform true
RUN bundle install --path=./vendor --without='test development' RUN bundle install --path=./vendor --without='test development'
# Vaciar la caché # Vaciar la caché
RUN rm vendor/ruby/2.7.0/cache/*.gem RUN rm vendor/ruby/2.7.0/cache/*.gem
@ -61,6 +60,10 @@ RUN mv ../sutty/.bundle ./.bundle
# Instalar secretos # Instalar secretos
COPY --chown=app:root ./config/credentials.yml.enc ./config/ COPY --chown=app:root ./config/credentials.yml.enc ./config/
# Eliminar la necesidad de un runtime JS en producción, porque los
# assets ya están pre-compilados.
RUN sed -re "/(sassc|uglifier|bootstrap|coffee-rails)/d" -i Gemfile
RUN bundle clean
RUN rm -rf ./node_modules ./tmp/cache ./.git ./test ./doc RUN rm -rf ./node_modules ./tmp/cache ./.git ./test ./doc
# Eliminar archivos innecesarios # Eliminar archivos innecesarios
USER root USER root
@ -68,7 +71,7 @@ RUN apk add --no-cache findutils
RUN find /home/app/checkout/vendor/ruby/2.7.0 -maxdepth 3 -type d -name test -o -name spec -o -name rubocop | xargs -r rm -rf RUN find /home/app/checkout/vendor/ruby/2.7.0 -maxdepth 3 -type d -name test -o -name spec -o -name rubocop | xargs -r rm -rf
# Contenedor final # Contenedor final
FROM registry.nulo.in/sutty/monit:3.13.6 FROM sutty/monit:latest
ENV RAILS_ENV production ENV RAILS_ENV production
# Pandoc # Pandoc
@ -76,13 +79,13 @@ RUN echo 'http://dl-cdn.alpinelinux.org/alpine/edge/testing' >> /etc/apk/reposit
# Instalar las dependencias, separamos la librería de base de datos para # Instalar las dependencias, separamos la librería de base de datos para
# poder reutilizar este primer paso desde otros contenedores # poder reutilizar este primer paso desde otros contenedores
RUN apk add --no-cache libxslt libxml2 tzdata ruby ruby-json ruby-bigdecimal ruby-rake ruby-irb ruby-io-console ruby-etc RUN apk add --no-cache libxslt libxml2 tzdata ruby ruby-bundler ruby-json ruby-bigdecimal ruby-rake ruby-irb
RUN apk add --no-cache postgresql-libs libssh2 file rsync git jpegoptim vips RUN apk add --no-cache postgresql-libs libssh2 file rsync git jpegoptim vips
RUN apk add --no-cache ffmpeg imagemagick pandoc tectonic oxipng jemalloc RUN apk add --no-cache ffmpeg imagemagick pandoc tectonic oxipng jemalloc
RUN apk add --no-cache git-lfs openssh-client patch RUN apk add --no-cache git-lfs openssh-client patch
# Chequear que la versión de ruby sea la correcta # Chequear que la versión de ruby sea la correcta
RUN test "2.7.4" = `ruby -e 'puts RUBY_VERSION'` RUN test "2.7.3" = `ruby -e 'puts RUBY_VERSION'`
# https://github.com/rubygems/rubygems/issues/2918 # https://github.com/rubygems/rubygems/issues/2918
# https://gitlab.alpinelinux.org/alpine/aports/issues/10808 # https://gitlab.alpinelinux.org/alpine/aports/issues/10808
@ -94,7 +97,7 @@ RUN apk add --no-cache patch && cd /usr/lib/ruby/2.7.0 && patch -Np 0 -i /tmp/ru
# principal # principal
RUN apk add --no-cache yarn RUN apk add --no-cache yarn
# Instalar foreman para poder correr los servicios # Instalar foreman para poder correr los servicios
RUN gem install --no-document --no-user-install bundler:2.1.4 foreman RUN gem install --no-document --no-user-install bundler foreman
# Agregar el grupo del servidor web y la usuaria # Agregar el grupo del servidor web y la usuaria
RUN addgroup -g 82 -S www-data RUN addgroup -g 82 -S www-data

20
Gemfile
View file

@ -11,16 +11,13 @@ gem 'dotenv-rails', require: 'dotenv/rails-now'
gem 'rails', '~> 6' gem 'rails', '~> 6'
# Use Puma as the app server # Use Puma as the app server
gem 'puma' gem 'puma'
# See https://github.com/rails/execjs#readme for more supported runtimes
# Solo incluir las gemas cuando estemos en desarrollo o compilando los # gem 'therubyracer', platforms: :ruby
# assets. No es necesario instalarlas en producción. # Use SCSS for stylesheets
# gem 'sassc-rails'
# XXX: Supuestamente Rails ya soporta RAILS_GROUPS, pero Bundler no. # Use Uglifier as compressor for JavaScript assets
if ENV['RAILS_GROUPS']&.split(',')&.include? 'assets' gem 'uglifier', '>= 1.3.0'
gem 'sassc-rails' gem 'bootstrap', '~> 4'
gem 'uglifier', '>= 1.3.0'
gem 'bootstrap', '~> 4'
end
# Turbolinks makes navigating your web application faster. Read more: # Turbolinks makes navigating your web application faster. Read more:
# https://github.com/turbolinks/turbolinks # https://github.com/turbolinks/turbolinks
@ -31,7 +28,6 @@ gem 'jbuilder', '~> 2.5'
# Use ActiveModel has_secure_password # Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7' gem 'bcrypt', '~> 3.1.7'
gem 'blazer' gem 'blazer'
gem 'chartkick'
gem 'commonmarker' gem 'commonmarker'
gem 'devise' gem 'devise'
gem 'devise-i18n' gem 'devise-i18n'
@ -62,7 +58,6 @@ 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 'rubyzip' gem 'rubyzip'
gem 'rugged' gem 'rugged'
gem 'concurrent-ruby-ext' gem 'concurrent-ruby-ext'
@ -72,7 +67,6 @@ gem 'terminal-table'
gem 'validates_hostname' gem 'validates_hostname'
gem 'webpacker' gem 'webpacker'
gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git' gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git'
gem 'kaminari'
# database # database
gem 'hairtrigger' gem 'hairtrigger'

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: 94ca777d54180c23e96ac4b4285cc9b405ccbd1a
branch: master
specs:
rollups (0.1.2)
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
@ -214,8 +205,6 @@ GEM
ffi (~> 1.0) ffi (~> 1.0)
globalid (0.4.2) globalid (0.4.2)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
groupdate (5.2.2)
activesupport (>= 5)
hairtrigger (0.2.24) hairtrigger (0.2.24)
activerecord (>= 5.0, < 7) activerecord (>= 5.0, < 7)
ruby2ruby (~> 2.4) ruby2ruby (~> 2.4)
@ -320,18 +309,6 @@ GEM
jekyll-write-and-commit-changes (0.1.2) jekyll-write-and-commit-changes (0.1.2)
jekyll (~> 4) jekyll (~> 4)
rugged (~> 1) rugged (~> 1)
kaminari (1.2.1)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.1)
kaminari-activerecord (= 1.2.1)
kaminari-core (= 1.2.1)
kaminari-actionview (1.2.1)
actionview
kaminari-core (= 1.2.1)
kaminari-activerecord (1.2.1)
activerecord
kaminari-core (= 1.2.1)
kaminari-core (1.2.1)
kramdown (2.3.1) kramdown (2.3.1)
rexml rexml
kramdown-parser-gfm (1.1.0) kramdown-parser-gfm (1.1.0)
@ -368,7 +345,6 @@ GEM
mini_histogram (0.3.1) mini_histogram (0.3.1)
mini_magick (4.11.0) mini_magick (4.11.0)
mini_mime (1.1.0) mini_mime (1.1.0)
mini_portile2 (2.5.3)
minima (2.5.1) minima (2.5.1)
jekyll (>= 3.5, < 5.0) jekyll (>= 3.5, < 5.0)
jekyll-feed (~> 0.9) jekyll-feed (~> 0.9)
@ -381,8 +357,7 @@ GEM
net-ssh (6.1.0) net-ssh (6.1.0)
netaddr (2.0.4) netaddr (2.0.4)
nio4r (2.5.7-x86_64-linux-musl) nio4r (2.5.7-x86_64-linux-musl)
nokogiri (1.11.7-x86_64-linux-musl) nokogiri (1.11.7-x86_64-linux)
mini_portile2 (~> 2.5.0)
racc (~> 1.4) racc (~> 1.4)
orm_adapter (0.5.0) orm_adapter (0.5.0)
parallel (1.20.1) parallel (1.20.1)
@ -660,7 +635,6 @@ DEPENDENCIES
bootstrap (~> 4) bootstrap (~> 4)
brakeman brakeman
capybara (~> 2.13) capybara (~> 2.13)
chartkick
commonmarker commonmarker
concurrent-ruby-ext concurrent-ruby-ext
database_cleaner database_cleaner
@ -693,7 +667,6 @@ DEPENDENCIES
jekyll-data! jekyll-data!
jekyll-images jekyll-images
jekyll-include-cache jekyll-include-cache
kaminari
letter_opener letter_opener
listen (>= 3.0.5, < 3.2) listen (>= 3.0.5, < 3.2)
loaf loaf
@ -719,7 +692,6 @@ DEPENDENCIES
recursero-jekyll-theme recursero-jekyll-theme
redis redis
redis-rails redis-rails
rollups!
rubocop-rails rubocop-rails
rubyzip rubyzip
rugged rugged

View file

@ -48,7 +48,7 @@ help: always ## Ayuda
@echo -e "\nArgumentos:\n" @echo -e "\nArgumentos:\n"
@grep -E "^[a-z\-]+ \?=.*##" Makefile | sed -re "s/(.*) \?=.*##(.*)/\1;\2/" | column -s ";" -t | sed -re "s/^([^ ]+) /\x1B[38;5;197m\1\x1B[0m/" @grep -E "^[a-z\-]+ \?=.*##" Makefile | sed -re "s/(.*) \?=.*##(.*)/\1;\2/" | column -s ";" -t | sed -re "s/^([^ ]+) /\x1B[38;5;197m\1\x1B[0m/"
assets: public/packs/manifest.json.br ## Compilar los assets assets: node_modules public/packs/manifest.json.br ## Compilar los assets
test: always ## Ejecutar los tests test: always ## Ejecutar los tests
$(MAKE) rake args="test RAILS_ENV=test $(args)" $(MAKE) rake args="test RAILS_ENV=test $(args)"
@ -108,6 +108,7 @@ ota-js: assets ## Actualizar Javascript en el nodo delegado
ota: ## Actualizar Rails en el nodo delegado ota: ## Actualizar Rails en el nodo delegado
umask 022; git format-patch $(commit) umask 022; git format-patch $(commit)
scp ./0*.patch $(delegate):/tmp/
ssh $(delegate) mkdir -p /tmp/patches-$(commit)/ ssh $(delegate) mkdir -p /tmp/patches-$(commit)/
scp ./0*.patch $(delegate):/tmp/patches-$(commit)/ scp ./0*.patch $(delegate):/tmp/patches-$(commit)/
scp ./ota.sh $(delegate):/tmp/ scp ./ota.sh $(delegate):/tmp/

View file

@ -2,13 +2,6 @@
box-sizing: border-box; box-sizing: border-box;
*, *::before, *::after { box-sizing: inherit; } *, *::before, *::after { box-sizing: inherit; }
// Arreglo temporal para que las cosas sean legibles en modo oscuro
--foreground: black;
--background: white;
--color: #f206f9;
background: var(--background);
color: var(--foreground);
h1, h2, h3, h4, h5, h6, p, li { h1, h2, h3, h4, h5, h6, p, li {
min-height: 1.5rem; min-height: 1.5rem;
} }

View file

@ -22,21 +22,21 @@ class PostsController < ApplicationController
# XXX: Cada vez que cambiamos un Post tocamos el sitio con lo que es # XXX: Cada vez que cambiamos un Post tocamos el sitio con lo que es
# más simple saber si hubo cambios. # más simple saber si hubo cambios.
return unless stale?([current_usuarie, site, filter_params]) if stale?([current_usuarie, site, filter_params])
# Todos los artículos de este sitio para el idioma actual
@posts = site.indexed_posts.where(locale: locale)
# De este tipo
@posts = @posts.where(layout: filter_params[:layout]) if filter_params[:layout]
# Que estén dentro de la categoría
@posts = @posts.in_category(filter_params[:category]) if filter_params[:category]
# Aplicar los parámetros de búsqueda
@posts = @posts.search(locale, filter_params[:q]) if filter_params[:q].present?
# A los que este usuarie tiene acceso
@posts = PostPolicy::Scope.new(current_usuarie, @posts).resolve
# Todos los artículos de este sitio para el idioma actual # Filtrar los posts que les invitades no pueden ver
@posts = site.indexed_posts.where(locale: locale) @usuarie = site.usuarie? current_usuarie
# De este tipo end
@posts = @posts.where(layout: filter_params[:layout]) if filter_params[:layout]
# Que estén dentro de la categoría
@posts = @posts.in_category(filter_params[:category]) if filter_params[:category]
# Aplicar los parámetros de búsqueda
@posts = @posts.search(locale, filter_params[:q]) if filter_params[:q].present?
# A los que este usuarie tiene acceso
@posts = PostPolicy::Scope.new(current_usuarie, @posts).resolve
# Filtrar los posts que les invitades no pueden ver
@usuarie = site.usuarie? current_usuarie
end end
def show def show
@ -54,7 +54,7 @@ class PostsController < ApplicationController
def new def new
authorize Post authorize Post
@post = site.posts(lang: locale).build(layout: params[:layout]) @post = site.posts.build(lang: locale, layout: params[:layout])
breadcrumb I18n.t('loaf.breadcrumbs.posts.new', layout: @post.layout.humanized_name.downcase), '' breadcrumb I18n.t('loaf.breadcrumbs.posts.new', layout: @post.layout.humanized_name.downcase), ''
end end
@ -154,9 +154,7 @@ class PostsController < ApplicationController
# #
# @return [Hash] # @return [Hash]
def filter_params def filter_params
@filter_params ||= params.permit(:q, :category, :layout).to_hash.select do |_, v| @filter_params ||= params.permit(:q, :category, :layout).to_h.select { |_, v| v.present? }
v.present?
end.transform_keys(&:to_sym)
end end
def site def site

View file

@ -3,166 +3,16 @@
# Estadísticas del sitio # Estadísticas del sitio
class StatsController < ApplicationController class StatsController < ApplicationController
include Pundit include Pundit
include ActionView::Helpers::DateHelper
before_action :authenticate_usuarie! before_action :authenticate_usuarie!
before_action :authorize_stats
EXTRA_OPTIONS = {
builds: {},
space_used: { bytes: true },
build_time: {}
}.freeze
# XXX: Permitir a Chart.js inyectar su propio CSS
content_security_policy only: :index do |policy|
policy.style_src :self, :unsafe_inline
policy.script_src :self, :unsafe_inline
end
def index def index
@chart_params = { interval: interval }
hostnames
last_stat
chart_options
normalized_urls
end
# Genera un gráfico de visitas por dominio asociado a este sitio
def host
return unless stale? [last_stat, hostnames, interval]
stats = Rollup.where_dimensions(host: hostnames).multi_series('host', interval: interval).tap do |series|
series.each do |serie|
serie[:name] = serie.dig(:dimensions, 'host')
serie[:data].transform_values! do |value|
value * nodes
end
end
end
render json: stats
end
def resources
return unless stale? [last_stat, interval, resource]
options = {
interval: interval,
dimensions: {
deploy_id: @site.deploys.where(type: 'DeployLocal').pluck(:id).first
}
}
render json: Rollup.series(resource, **options)
end
def uris
return unless stale? [last_stat, hostnames, interval, normalized_urls]
options = { host: hostnames, uri: normalized_paths }
stats = Rollup.where_dimensions(**options).multi_series('host|uri', interval: interval).tap do |series|
series.each do |serie|
serie[:name] = serie[:dimensions].slice('host', 'uri').values.join.sub('/index.html', '/')
serie[:data].transform_values! do |value|
value * nodes
end
end
end
render json: stats
end
private
def last_stat
@last_stat ||= Stat.last
end
def authorize_stats
@site = find_site @site = find_site
authorize SiteStat.new(@site) authorize SiteStat.new(@site)
end
# TODO: Eliminar cuando mergeemos referer-origin # Solo queremos el promedio de tiempo de compilación, no de
def hostnames # instalación de dependencias.
@hostnames ||= [@site.hostname, @site.alternative_hostnames].flatten stats = @site.build_stats.jekyll
end @build_avg = stats.average(:seconds).to_f.round(2)
@build_max = stats.maximum(:seconds).to_f.round(2)
# Normalizar las URLs
#
# @return [Array]
def normalized_urls
@normalized_urls ||= params.permit(:urls).try(:[],
:urls)&.split("\n")&.map(&:strip)&.select(&:present?)&.select do |uri|
uri.start_with? 'https://'
end&.map do |u|
# XXX: Eliminar
# @see {https://0xacab.org/sutty/containers/nginx/-/merge_requests/1}
next u unless u.end_with? '/'
"#{u}index.html"
end&.uniq || [@site.url, @site.urls].flatten.uniq
end
def normalized_paths
@normalized_paths ||= normalized_urls.map do |u|
"/#{u.split('/', 4).last}"
end.map do |u|
URI.decode_www_form_component u
end
end
# Opciones por defecto para los gráficos.
#
# La invitación a volver dentro de X tiempo es para dar un estimado de
# cuándo habrá información disponible, porque Rollup genera intervalos
# completos (¿aunque dice que no?)
#
# La diferencia se calcula sumando el intervalo a la hora de última
# toma de estadísticas y restando el tiempo que pasó desde ese
# momento.
def chart_options
time = (last_stat&.created_at || Time.now) + 1.try(interval)
please_return_at = { please_return_at: distance_of_time_in_words(Time.now, time) }
@chart_options ||= {
locale: I18n.locale,
empty: I18n.t('stats.index.empty', **please_return_at),
loading: I18n.t('stats.index.loading'),
html: %(<div id="%{id}" class="d-flex align-items-center justify-content-center" style="height: %{height}; width: %{width};">%{loading}</div>)
}
end
# Obtiene y valida los intervalos
#
# @return [Symbol]
def interval
@interval ||= begin
i = params[:interval]&.to_sym
Stat::INTERVALS.include?(i) ? i : :day
end
end
def resource
@resource ||= begin
r = params[:resource].to_sym
Stat::RESOURCES.include?(r) ? r : :builds
end
end
# Obtiene la cantidad de nodos de Sutty, para poder calcular la
# cantidad de visitas.
#
# Como repartimos las visitas por nodo rotando las IPs en el
# nameserver y los resolvedores de DNS eligen un nameserver
# aleatoriamente, la cantidad de visitas se reparte
# equitativamente.
#
# XXX: Remover cuando podamos centralizar los AccessLog
#
# @return [Integer]
def nodes
@nodes ||= ENV.fetch('NODES', 1).to_i
end end
end end

View file

@ -18,7 +18,6 @@ import 'etc'
import Rails from '@rails/ujs' import Rails from '@rails/ujs'
import Turbolinks from 'turbolinks' import Turbolinks from 'turbolinks'
import * as ActiveStorage from '@rails/activestorage' import * as ActiveStorage from '@rails/activestorage'
import 'chartkick/chart.js'
Rails.start() Rails.start()
Turbolinks.start() Turbolinks.start()

View file

@ -1,55 +0,0 @@
# frozen_string_literal: true
# Una tarea que se corre periódicamente
class PeriodicJob < ApplicationJob
class RunAgainException < StandardError; end
STARTING_INTERVAL = Stat::INTERVALS.first
# Tener el sitio a mano
attr_reader :site
# Descartar y notificar si pasó algo más.
#
# XXX: En realidad deberíamos seguir reintentando?
discard_on(StandardError) do |_, error|
ExceptionNotifier.notify_exception(error)
end
# Correr indefinidamente una vez por hora.
#
# XXX: El orden importa, si el descarte viene después, nunca se va a
# reintentar.
retry_on(PeriodicJob::RunAgainException, wait: 1.try(STARTING_INTERVAL), attempts: Float::INFINITY, jitter: 0)
private
# Las clases que implementen esta tienen que usar este método al
# terminar.
def run_again!
raise PeriodicJob::RunAgainException, 'Reintentando'
end
# El intervalo de inicio
#
# @return [Symbol]
def starting_interval
STARTING_INTERVAL
end
# La última recolección de estadísticas o empezar desde el principio
# de los tiempos.
#
# @return [Stat]
def last_stat
@last_stat ||= site.stats.where(name: stat_name).last ||
site.stats.build(created_at: Time.new(1970, 1, 1))
end
# Devuelve el comienzo del intervalo
#
# @return [Time]
def beginning_of_interval
@beginning_of_interval ||= last_stat.created_at.try(:"beginning_of_#{starting_interval}")
end
end

View file

@ -1,67 +0,0 @@
# frozen_string_literal: true
# Genera resúmenes de información para poder mostrar estadísticas y se
# corre regularmente a sí misma.
class StatCollectionJob < ApplicationJob
STAT_NAME = 'stat_collection_job'
def perform(site_id:, once: true)
@site = Site.find site_id
scope.rollup('builds', **options)
scope.rollup('space_used', **options) do |rollup|
rollup.average(:bytes)
end
scope.rollup('build_time', **options) do |rollup|
rollup.average(:seconds)
end
# XXX: Es correcto promediar promedios?
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
end
# Registrar que se hicieron todas las recolecciones
site.stats.create! name: STAT_NAME
run_again! unless once
end
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
#
# @return [ActiveRecord::Relation]
def scope
@scope ||= site.build_stats
.jekyll
.where('created_at => ?', beginning_of_interval)
.group(:site_id)
end
# Las opciones por defecto
#
# @return [Hash]
def options
@options ||= { interval: starting_interval, update: true }
end
end

View file

@ -1,106 +0,0 @@
# frozen_string_literal: true
# Procesar una lista de URIs para una lista de dominios. Esto nos
# permite procesar estadísticas a demanda.
#
# Hay varias cosas acá que van a convertirse en métodos propios, como la
# detección de URIs de un sitio (aunque la versión actual detecta todas
# las páginas y no solo las de posts como tenemos planeado, hay que
# resolver eso).
#
# Los hostnames de un sitio van a poder obtenerse a partir de
# Site#hostnames con la garantía de que son únicos.
class UriCollectionJob < PeriodicJob
# Ignoramos imágenes porque suelen ser demasiadas y no aportan a las
# estadísticas.
IMAGES = %w[.png .jpg .jpeg .gif .webp].freeze
STAT_NAME = 'uri_collection_job'
def perform(site_id:, once: true)
@site = Site.find site_id
hostnames.each do |hostname|
uris.each do |uri|
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
site.stats.create! name: STAT_NAME
run_again! unless once
end
private
def stat_name
STAT_NAME
end
# @return [String]
#
# TODO: Cambiar al mergear origin-referer
def destination
@destination ||= site.deploys.find_by(type: 'DeployLocal').destination
end
# TODO: Cambiar al mergear origin-referer
#
# @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
# Recolecta todas las URIs menos imágenes
#
# @return [Array]
def uris
@uris ||= Dir.chdir destination do
(Dir.glob('**/*.html') + Dir.glob('public/**/*').reject do |p|
File.directory? p
end.reject do |p|
p = p.downcase
IMAGES.any? do |i|
p.end_with? i
end
end).map do |uri|
"/#{uri}"
end
end
end
end

View file

@ -1,12 +1,4 @@
# frozen_string_literal: true # frozen_string_literal: true
class AccessLog < ApplicationRecord class AccessLog < ApplicationRecord
# Las peticiones completas son las que terminaron bien y se
# respondieron con 200 OK o 304 Not Modified
#
# @see {https://en.wikipedia.org/wiki/List_of_HTTP_status_codes}
scope :completed_requests, -> { where(request_method: 'GET', request_completion: 'OK', status: [200, 304]) }
scope :non_robots, -> { where(crawler: false) }
scope :robots, -> { where(crawler: true) }
scope :pages, -> { where(sent_http_content_type: ['text/html', 'text/html; charset=utf-8', 'text/html; charset=UTF-8']) }
end end

View file

@ -25,19 +25,10 @@ class MetadataBoolean < MetadataTemplate
# * false # * false
# * true # * true
def value def value
case self[:value] return document.data.fetch(name.to_s, default_value) if self[:value].nil?
when NilClass return self[:value] unless self[:value].is_a? String
document.data.fetch(name.to_s, default_value)
when String
true_values.include? self[:value]
else
self[:value]
end
end
# Siempre guardar el valor de este campo a menos que sea nulo self[:value] = true_values.include? self[:value]
def empty?
value.nil?
end end
private private

View file

@ -56,7 +56,7 @@ class MetadataContent < MetadataTemplate
uri = URI element['src'] uri = URI element['src']
# No permitimos recursos externos # No permitimos recursos externos
element.remove unless uri.scheme == 'https' && uri.hostname.end_with?(Site.domain) element.remove unless uri.hostname.end_with? Site.domain
rescue URI::Error rescue URI::Error
element.remove element.remove
end end

View file

@ -1,31 +0,0 @@
# frozen_string_literal: true
# Un campo numérico de punto flotante
class MetadataFloat < MetadataTemplate
# Nada
def default_value
super || nil
end
def save
return true unless changed?
self[:value] = value.to_f
self[:value] = encrypt(value) if private?
true
end
# Indicarle al navegador que acepte números decimales
#
# @return [Float]
def step
0.05
end
private
def decrypt(value)
super(value).to_f
end
end

View file

@ -2,12 +2,6 @@
# Este metadato permite generar rutas manuales. # Este metadato permite generar rutas manuales.
class MetadataPermalink < MetadataString class MetadataPermalink < MetadataString
# El valor por defecto una vez creado es la URL que le asigne Jekyll,
# de forma que nunca cambia aunque se cambie el título.
def default_value
document.url unless post.new?
end
# Los permalinks nunca pueden ser privados # Los permalinks nunca pueden ser privados
def private? def private?
false false

View file

@ -1,24 +0,0 @@
# frozen_string_literal: true
# Un campo de texto seleccionado de una lista de valores posibles
class MetadataPredefinedValue < MetadataString
# Obtiene todos los valores desde el layout, en un formato compatible
# con options_for_select.
#
# @return [Hash]
def values
@values ||= layout.dig(:metadata, name, 'values', I18n.locale.to_s)&.invert || {}
end
private
# Solo permite almacenar los valores predefinidos.
#
# @return [String]
def sanitize(string)
v = super string
return '' unless values.values.include? v
v
end
end

View file

@ -26,7 +26,7 @@ class Site < ApplicationRecord
validates :design_id, presence: true validates :design_id, presence: true
validates_inclusion_of :status, in: %w[waiting enqueued building] validates_inclusion_of :status, in: %w[waiting enqueued building]
validates_presence_of :title validates_presence_of :title
validates :description, length: { in: 10..160 } validates :description, length: { in: 50..160 }
validate :deploy_local_presence validate :deploy_local_presence
validate :compatible_layouts, on: :update validate :compatible_layouts, on: :update
@ -37,7 +37,6 @@ class Site < ApplicationRecord
belongs_to :design belongs_to :design
belongs_to :licencia belongs_to :licencia
has_many :stats
has_many :log_entries, dependent: :destroy has_many :log_entries, dependent: :destroy
has_many :deploys, dependent: :destroy has_many :deploys, dependent: :destroy
has_many :build_stats, through: :deploys has_many :build_stats, through: :deploys
@ -66,6 +65,9 @@ class Site < ApplicationRecord
accepts_nested_attributes_for :deploys, allow_destroy: true accepts_nested_attributes_for :deploys, allow_destroy: true
# El sitio en Jekyll
attr_reader :jekyll
# XXX: Es importante incluir luego de los callbacks de :load_jekyll # XXX: Es importante incluir luego de los callbacks de :load_jekyll
include Site::Index include Site::Index
@ -178,28 +180,29 @@ class Site < ApplicationRecord
# Trae los datos del directorio _data dentro del sitio # Trae los datos del directorio _data dentro del sitio
def data def data
unless jekyll.data.present? unless @jekyll.data.present?
run_in_path do @jekyll.reader.read_data
jekyll.reader.read_data
jekyll.data['layouts'] ||= {} # Define los valores por defecto según la llave buscada
@jekyll.data.default_proc = proc do |data, key|
data[key] = case key
when 'layout' then {}
end
end end
end end
jekyll.data @jekyll.data
end end
# Traer las colecciones. Todos los artículos van a estar dentro de # Traer las colecciones. Todos los artículos van a estar dentro de
# colecciones. # colecciones.
def collections def collections
unless @read unless @read
run_in_path do @jekyll.reader.read_collections
jekyll.reader.read_collections
end
@read = true @read = true
end end
jekyll.collections @jekyll.collections
end end
# Traer la configuración de forma modificable # Traer la configuración de forma modificable
@ -287,9 +290,7 @@ class Site < ApplicationRecord
# #
# @return [Hash] # @return [Hash]
def theme_layouts def theme_layouts
run_in_path do @jekyll.reader.read_layouts
jekyll.reader.read_layouts
end
end end
# Trae todos los valores disponibles para un campo # Trae todos los valores disponibles para un campo
@ -331,12 +332,6 @@ class Site < ApplicationRecord
status == 'building' status == 'building'
end end
def jekyll
run_in_path do
@jekyll ||= Jekyll::Site.new(configuration)
end
end
# Cargar el sitio Jekyll # Cargar el sitio Jekyll
# #
# TODO: En lugar de leer todo junto de una vez, extraer la carga de # TODO: En lugar de leer todo junto de una vez, extraer la carga de
@ -350,7 +345,10 @@ class Site < ApplicationRecord
def reload_jekyll! def reload_jekyll!
reset reset
jekyll
Dir.chdir(path) do
@jekyll = Jekyll::Site.new(configuration)
end
end end
def reload def reload
@ -528,8 +526,4 @@ class Site < ApplicationRecord
errors.add(:design_id, errors.add(:design_id,
I18n.t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.error')) I18n.t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.error'))
end end
def run_in_path(&block)
Dir.chdir path, &block
end
end end

View file

@ -1,10 +0,0 @@
# frozen_string_literal: true
# Registran cuándo fue la última recolección de datos.
class Stat < ApplicationRecord
# XXX: Los intervalos van en orden de mayor especificidad a menor
INTERVALS = %i[day month year].freeze
RESOURCES = %i[builds space_used build_time].freeze
belongs_to :site
end

View file

@ -12,16 +12,4 @@ class SiteStatPolicy
def index? def index?
site_stat.site.usuarie? usuarie site_stat.site.usuarie? usuarie
end end
def host?
index?
end
def resources?
index?
end
def uris?
index?
end
end end

View file

@ -122,8 +122,6 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
# la búsqueda. # la búsqueda.
def change_licencias def change_licencias
site.locales.each do |locale| site.locales.each do |locale|
next unless I18n.available_locales.include? locale
Mobility.with_locale(locale) do Mobility.with_locale(locale) do
permalink = "#{I18n.t('activerecord.models.licencia').downcase}/" permalink = "#{I18n.t('activerecord.models.licencia').downcase}/"
post = site.posts(lang: locale).find_by(permalink: permalink) post = site.posts(lang: locale).find_by(permalink: permalink)

View file

@ -1,3 +0,0 @@
%tr{ id: attribute }
%th= post_label_t(attribute, post: post)
%td{ dir: dir, lang: locale }= metadata.value

View file

@ -1,3 +0,0 @@
%tr{ id: attribute }
%th= post_label_t(attribute, post: post)
%td{ dir: dir, lang: locale }= metadata.value

View file

@ -1,6 +0,0 @@
.form-group
= label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
= number_field base, attribute, value: metadata.value, step: metadata.step,
**field_options(attribute, metadata)
= render 'posts/attribute_feedback',
post: post, attribute: attribute, metadata: metadata

View file

@ -1,7 +0,0 @@
.form-group
= label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
= select_tag(plain_field_name_for(base, attribute),
options_for_select(metadata.values, metadata.value),
**field_options(attribute, metadata), include_blank: t('.empty'))
= render 'posts/attribute_feedback',
post: post, attribute: attribute, metadata: metadata

View file

@ -40,7 +40,7 @@
%section.col %section.col
= render 'layouts/flash' = render 'layouts/flash'
.d-flex.justify-content-between.align-items-center.pl-2-plus.pr-2-plus.mb-2 .d-flex.justify-content-between.align-items-center.pl-2-plus.pr-2-plus.mb-2
%form{ action: site_posts_path } %form
- @filter_params.each do |param, value| - @filter_params.each do |param, value|
- next if param == 'q' - next if param == 'q'
%input{ type: 'hidden', name: param, value: value } %input{ type: 'hidden', name: param, value: value }
@ -51,7 +51,7 @@
- if @site.locales.size > 1 - if @site.locales.size > 1
%nav#locales %nav#locales
- @site.locales.each do |locale| - @site.locales.each do |locale|
= link_to @site.data.dig(locale.to_s, 'locale') || locale, site_posts_path(@site, **@filter_params.merge(locale: locale)), = link_to t("locales.#{locale}.name"), site_posts_path(@site, **@filter_params.merge(locale: locale)),
class: "mr-2 mt-2 mb-2 #{locale == @locale ? 'active font-weight-bold' : ''}" class: "mr-2 mt-2 mb-2 #{locale == @locale ? 'active font-weight-bold' : ''}"
.pl-2-plus .pl-2-plus
- @filter_params.each do |param, value| - @filter_params.each do |param, value|
@ -67,24 +67,19 @@
%h2= t('posts.empty') %h2= t('posts.empty')
- else - else
= form_tag site_posts_reorder_path, method: :post do = form_tag site_posts_reorder_path, method: :post do
%input{ type: 'hidden', name: 'post[lang]', value: @locale }
%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
%th.border-0.background-white.position-sticky{ style: 'top: 0; z-index: 2', colspan: '4' } %th.border-0.background-white.position-sticky{ style: 'top: 0; z-index: 2', colspan: '4' }
.d-flex.flex-row.justify-content-between = submit_tag t('posts.reorder.submit'), class: 'btn'
%div %button.btn{ data: { action: 'reorder#unselect' } }
= submit_tag t('posts.reorder.submit'), class: 'btn' = t('posts.reorder.unselect')
%button.btn{ data: { action: 'reorder#unselect' } } %span.badge{ data: { target: 'reorder.counter' } } 0
= t('posts.reorder.unselect') %button.btn{ data: { action: 'reorder#up' } }= t('posts.reorder.up')
%span.badge{ data: { target: 'reorder.counter' } } 0 %button.btn{ data: { action: 'reorder#down' } }= t('posts.reorder.down')
%button.btn{ data: { action: 'reorder#up' } }= t('posts.reorder.up') %button.btn{ data: { action: 'reorder#top' } }= t('posts.reorder.top')
%button.btn{ data: { action: 'reorder#down' } }= t('posts.reorder.down') %button.btn{ data: { action: 'reorder#bottom' } }= t('posts.reorder.bottom')
%button.btn{ data: { action: 'reorder#top' } }= t('posts.reorder.top')
%button.btn{ data: { action: 'reorder#bottom' } }= t('posts.reorder.bottom')
%div
%tbody %tbody
- dir = t("locales.#{@locale}.dir") - dir = t("locales.#{@locale}.dir")
- size = @posts.size - size = @posts.size
@ -109,19 +104,19 @@
%span{ lang: post.locale, dir: dir }= post.title %span{ lang: post.locale, dir: dir }= post.title
- if post.front_matter['draft'].present? - if post.front_matter['draft'].present?
%span.badge.badge-primary= I18n.t('posts.attributes.draft.label') %span.badge.badge-primary= I18n.t('posts.attributes.draft.label')
%br - if post.front_matter['categories'].present?
%small %br
= link_to @site.layouts[post.layout].humanized_name, site_posts_path(@site, **@filter_params.merge(layout: post.layout)) %small
- post.front_matter['categories']&.each do |category| - post.front_matter['categories'].each do |category|
= link_to site_posts_path(@site, **@filter_params.merge(category: category)) do = link_to site_posts_path(@site, **@filter_params.merge(category: category)) do
%span{ lang: post.locale, dir: dir }= category %span{ lang: post.locale, dir: dir }= category
= '/' unless post.front_matter['categories'].last == category = '/' unless post.front_matter['categories'].last == category
%td.text-nowrap %td
= post.created_at.strftime('%F') = post.created_at.strftime('%F')
%br/ %br/
= post.order = post.order
%td.text-nowrap %td
- if @usuarie || policy(post).edit? - if @usuarie || policy(post).edit?
= link_to t('posts.edit'), edit_site_post_path(@site, post.path), class: 'btn btn-block' = link_to t('posts.edit'), edit_site_post_path(@site, post.path), class: 'btn btn-block'
- if @usuarie || policy(post).destroy? - if @usuarie || policy(post).destroy?

View file

@ -39,7 +39,7 @@
%h2= f.label :description %h2= f.label :description
%p.lead= t('.help.description') %p.lead= t('.help.description')
= f.text_area :description, class: form_control(site, :description), = f.text_area :description, class: form_control(site, :description),
maxlength: 160, minlength: 10, required: true maxlength: 160, minlength: 50, required: true
- if invalid? site, :description - if invalid? site, :description
.invalid-feedback= site.errors.messages[:description].join(', ') .invalid-feedback= site.errors.messages[:description].join(', ')
%hr/ %hr/

View file

@ -1,43 +1,17 @@
= render 'layouts/breadcrumb',
crumbs: [link_to(t('sites.index.title'), sites_path),
link_to(@site.name, site_path(@site)), t('.title')]
.row .row
.col .col
%h1= t('.title') %h1= t('.title')
%p.lead= t('.help') %p.lead= t('.help')
- if @last_stat
%p
%small
= t('.last_update')
%time{ datetime: @last_stat.created_at }
#{time_ago_in_words @last_stat.created_at}.
.mb-5 %table.table.table-condensed
- Stat::INTERVALS.each do |interval| %tbody
= link_to t(".#{interval}"), site_stats_path(interval: interval, urls: params[:urls]), class: "btn #{'btn-primary active' if @interval == interval}" %tr
%td= t('.build.average')
.mb-5 %td= distance_of_time_in_words_if_more_than_a_minute @build_avg
%h2= t('.host.title', count: @hostnames.size) %tr
%p.lead= t('.host.description') %td= t('.build.maximum')
= line_chart site_stats_host_path(@chart_params), **@chart_options %td= distance_of_time_in_words_if_more_than_a_minute @build_max
.mb-5
%h2= t('.urls.title')
%p.lead= t('.urls.description')
%form
%input{ type: 'hidden', name: 'interval', value: @interval }
.form-group
%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")
%small#help-urls.feedback.form-text.text-muted= t('.urls.help')
.form-group
%button.btn{ type: 'submit' }= t('.urls.submit')
- if @normalized_urls.present?
= line_chart site_stats_uris_path(urls: params[:urls], **@chart_params), **@chart_options
.mb-5
%h2= t('.resources.title')
%p.lead= t('.resources.description')
- Stat::RESOURCES.each do |resource|
.mb-5
%h3= t(".resources.#{resource}.title")
%p.lead= t(".resources.#{resource}.description")
= line_chart site_stats_resources_path(resource: resource, **@chart_params), **@chart_options.merge(StatsController::EXTRA_OPTIONS[resource])

View file

@ -11,8 +11,6 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.singular 'licencias', 'licencia' inflect.singular 'licencias', 'licencia'
inflect.plural 'rol', 'roles' inflect.plural 'rol', 'roles'
inflect.singular 'roles', 'rol' inflect.singular 'roles', 'rol'
inflect.plural 'rollup', 'rollups'
inflect.singular 'rollups', 'rollup'
end end
ActiveSupport::Inflector.inflections(:es) do |inflect| ActiveSupport::Inflector.inflections(:es) do |inflect|
@ -26,6 +24,4 @@ ActiveSupport::Inflector.inflections(:es) do |inflect|
inflect.singular 'roles', 'rol' inflect.singular 'roles', 'rol'
inflect.plural 'licencia', 'licencias' inflect.plural 'licencia', 'licencias'
inflect.singular 'licencias', 'licencia' inflect.singular 'licencias', 'licencia'
inflect.plural 'rollup', 'rollups'
inflect.singular 'rollups', 'rollup'
end end

View file

@ -19,8 +19,8 @@ en:
remember_me: 'Keeps session open for %{remember_for}' remember_me: 'Keeps session open for %{remember_for}'
actions: actions:
sr-help: "After this form you'll find links to recover your account and other actions." sr-help: "After this form you'll find links to recover your account and other actions."
_true: 'Yes' _true: Yes
_false: 'No' _false: No
svg: svg:
sutty: sutty:
title: Sutty title: Sutty
@ -163,7 +163,7 @@ en:
signature: 'With love, Sutty' signature: 'With love, Sutty'
breadcrumb: breadcrumb:
title: 'Your location in Sutty' title: 'Your location in Sutty'
logout: Log out logout: Exit
mutual_aid: Mutual aid mutual_aid: Mutual aid
collaborations: collaborations:
collaborate: collaborate:
@ -252,38 +252,9 @@ 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 ' build:
empty: 'There is no enough information yet. We invite you to come back in %{please_return_at}!' average: 'Average building time'
loading: 'Loading...' maximum: 'Maximum building time'
hour: 'Hourly'
day: 'Daily'
week: 'Weekly'
month: 'Monthly'
year: 'Yearly'
host:
title:
zero: 'Site visits'
one: 'Site visits'
other: 'Visits by domain name'
description: 'Counts visited pages on your site, grouped by domain names in use.'
urls:
title: 'Visits by URL'
description: 'Counts visits or downloads on any URL.'
label: 'URLs ("links")'
help: 'Copy and paste a single URL per line'
submit: 'Update graph'
resources:
title: 'Resource usage'
description: "In this section you can find statistics on your site's use of Sutty's shared resources"
builds:
title: 'Site publication'
description: 'Times you published your site.'
space_used:
title: 'Server disk usage'
description: 'Average storage space used by your site.'
build_time:
title: 'Publication time'
description: 'Average time your site takes to build.'
sites: sites:
donations: donations:
url: 'https://donaciones.sutty.nl/en/' url: 'https://donaciones.sutty.nl/en/'
@ -405,8 +376,6 @@ en:
en: 'English' en: 'English'
ar: 'Arabic' ar: 'Arabic'
posts: posts:
prev: Previous page
next: Next page
empty: "There are no results for those search parameters." empty: "There are no results for those search parameters."
caption: Post list caption: Post list
attribute_ro: attribute_ro:
@ -444,8 +413,6 @@ en:
destroy: Remove image destroy: Remove image
belongs_to: belongs_to:
empty: "(Empty)" empty: "(Empty)"
predefined_value:
empty: "(Empty)"
draft: draft:
label: Draft label: Draft
reorder: reorder:

View file

@ -19,8 +19,8 @@ es:
remember_me: 'Mantiene la sesión abierta por %{remember_for}' remember_me: 'Mantiene la sesión abierta por %{remember_for}'
actions: actions:
sr-help: 'Después del formulario encontrarás vínculos para recuperar tu cuenta, entre otras acciones.' sr-help: 'Después del formulario encontrarás vínculos para recuperar tu cuenta, entre otras acciones.'
_true: 'Sí' _true:
_false: 'No' _false: No
svg: svg:
sutty: sutty:
title: Sutty title: Sutty
@ -163,7 +163,7 @@ es:
signature: 'Con cariño, Sutty' signature: 'Con cariño, Sutty'
breadcrumb: breadcrumb:
title: 'Tu ubicación en Sutty' title: 'Tu ubicación en Sutty'
logout: Cerrar sesión logout: Salir
mutual_aid: Ayuda mutua mutual_aid: Ayuda mutua
collaborations: collaborations:
collaborate: collaborate:
@ -257,38 +257,9 @@ 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 ' build:
empty: 'Todavía no hay información suficiente. Te invitamos a volver en %{please_return_at} :)' average: 'Tiempo promedio de generación'
loading: 'Cargando...' maximum: 'Tiempo máximo de generación'
hour: 'Por hora'
day: 'Diarias'
week: 'Semanales'
month: 'Mensuales'
year: 'Anuales'
host:
title:
zero: 'Visitas del sitio'
one: 'Visitas del sitio'
other: 'Visitas agrupadas por nombre de dominio del sitio'
description: 'Cuenta la cantidad de páginas visitadas en tu sitio.'
urls:
title: 'Visitas por dirección'
description: 'Cantidad de visitas o descargas por dirección.'
label: 'Direcciones web (URL, "links", vínculos)'
help: 'Copia y pega una dirección por línea.'
submit: 'Actualizar gráfico'
resources:
title: 'Uso de recursos'
description: 'En esta sección podrás acceder a estadísticas del uso de recursos compartidos con otros sitios alojados en Sutty.'
builds:
title: 'Publicaciones del sitio'
description: 'Cantidad de veces que publicaste tu sitio.'
space_used:
title: 'Espacio utilizado en el servidor'
description: 'Espacio en disco que ocupa en promedio tu sitio.'
build_time:
title: 'Tiempo de publicación'
description: 'Tiempo promedio que toma en publicarse tu sitio.'
sites: sites:
donations: donations:
url: 'https://donaciones.sutty.nl/' url: 'https://donaciones.sutty.nl/'
@ -413,8 +384,6 @@ es:
en: 'inglés' en: 'inglés'
ar: 'árabe' ar: 'árabe'
posts: posts:
prev: Página anterior
next: Página siguiente
empty: No hay artículos con estos parámetros de búsqueda. empty: No hay artículos con estos parámetros de búsqueda.
caption: Lista de artículos caption: Lista de artículos
attribute_ro: attribute_ro:
@ -452,8 +421,6 @@ es:
destroy: 'Eliminar imagen' destroy: 'Eliminar imagen'
belongs_to: belongs_to:
empty: "(Vacío)" empty: "(Vacío)"
predefined_value:
empty: "(Vacío)"
draft: draft:
label: Borrador label: Borrador
reorder: reorder:

View file

@ -57,10 +57,9 @@ Rails.application.routes.draw do
# Gestionar artículos según idioma # Gestionar artículos según idioma
nested do nested do
scope '/(:locale)', constraint: /[a-z]{2}/ do scope '(:locale)' do
post :'posts/reorder', to: 'posts#reorder' post :'posts/reorder', to: 'posts#reorder'
resources :posts do resources :posts do
get 'p/:page', action: :index, on: :collection
get :preview, to: 'posts#preview' get :preview, to: 'posts#preview'
end end
end end
@ -76,8 +75,5 @@ Rails.application.routes.draw do
post 'reorder_posts', to: 'sites#reorder_posts' post 'reorder_posts', to: 'sites#reorder_posts'
resources :stats, only: [:index] resources :stats, only: [:index]
get :'stats/host', to: 'stats#host'
get :'stats/uris', to: 'stats#uris'
get :'stats/resources', to: 'stats#resources'
end end
end end

View file

@ -1,15 +0,0 @@
# frozen_string_literal: true
# Crear la tabla de Rollups
class CreateRollups < ActiveRecord::Migration[6.1]
def change
create_table :rollups do |t|
t.string :name, null: false
t.string :interval, null: false
t.datetime :time, null: false
t.jsonb :dimensions, null: false, default: {}
t.float :value
end
add_index :rollups, %i[name interval time dimensions], unique: true
end
end

View file

@ -1,18 +0,0 @@
# frozen_string_literal: true
# Cambia los msec a datetime para poder agregar por tiempos
class AddCreateAtToAccessLogs < ActiveRecord::Migration[6.1]
def up
add_column :access_logs, :created_at, :datetime, precision: 6
create_trigger(compatibility: 1).on(:access_logs).before(:insert) do
'new.created_at := to_timestamp(new.msec)'
end
ActiveRecord::Base.connection.execute('update access_logs set created_at = to_timestamp(msec);')
end
def down
remove_column :access_logs, :created_at
end
end

View file

@ -1,10 +0,0 @@
# frozen_string_literal: true
# Agrega índices únicos que pensábamos que ya existían.
class AddUniqueness < ActiveRecord::Migration[6.1]
def change
add_index :designs, :name, unique: true
add_index :designs, :gem, unique: true
add_index :licencias, :name, unique: true
end
end

View file

@ -1,12 +0,0 @@
# frozen_string_literal: true
# Una tabla que lleva el recuento de recolección de estadísticas, solo
# es necesario para saber cuándo se hicieron, si se hicieron y usar como
# caché.
class CreateStats < ActiveRecord::Migration[6.1]
def change
create_table :stats do |t|
t.timestamps
end
end
end

View file

@ -13,8 +13,6 @@
"@rails/ujs": "^6.1.3-1", "@rails/ujs": "^6.1.3-1",
"@rails/webpacker": "5.2.1", "@rails/webpacker": "5.2.1",
"babel-loader": "^8.2.2", "babel-loader": "^8.2.2",
"chart.js": "^3.5.1",
"chartkick": "^4.0.5",
"circular-dependency-plugin": "^5.2.2", "circular-dependency-plugin": "^5.2.2",
"commonmark": "^0.29.0", "commonmark": "^0.29.0",
"fork-awesome": "^1.1.7", "fork-awesome": "^1.1.7",

View file

@ -2119,25 +2119,6 @@ chalk@^4.1.0:
ansi-styles "^4.1.0" ansi-styles "^4.1.0"
supports-color "^7.1.0" supports-color "^7.1.0"
chart.js@>=3.0.2, chart.js@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.5.1.tgz#73e24d23a4134a70ccdb5e79a917f156b6f3644a"
integrity sha512-m5kzt72I1WQ9LILwQC4syla/LD/N413RYv2Dx2nnTkRS9iv/ey1xLTt0DnPc/eWV4zI+BgEgDYBIzbQhZHc/PQ==
chartjs-adapter-date-fns@>=2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz#5e53b2f660b993698f936f509c86dddf9ed44c6b"
integrity sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==
chartkick@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/chartkick/-/chartkick-4.0.5.tgz#310a60c931e8ceedc39adee2ef8e9d1e474cb0e6"
integrity sha512-xKak4Fsgfvp1hj/LykRKkniDMaZASx2A4TdVc/sfsiNFFNf1m+D7PGwP1vgj1UsbsCjOCSfGWWyJpOYxkUCBug==
optionalDependencies:
chart.js ">=3.0.2"
chartjs-adapter-date-fns ">=2.0.0"
date-fns ">=2.0.0"
chokidar@^2.1.8: chokidar@^2.1.8:
version "2.1.8" version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@ -2763,11 +2744,6 @@ dashdash@^1.12.0:
dependencies: dependencies:
assert-plus "^1.0.0" assert-plus "^1.0.0"
date-fns@>=2.0.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.24.0.tgz#7d86dc0d93c87b76b63d213b4413337cfd1c105d"
integrity sha512-6ujwvwgPID6zbI0o7UbURi2vlLDR9uP26+tW6Lg+Ji3w7dd0i3DOcjcClLjLPranT60SSEFBwdSyYwn/ZkPIuw==
debug@2.6.9, debug@^2.2.0, debug@^2.3.3: debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
version "2.6.9" version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"