Merge branch 'rails' into blazer

This commit is contained in:
f 2022-03-07 12:27:53 -03:00
commit 507e7ced84
44 changed files with 1194 additions and 394 deletions

View File

@ -1,5 +1,8 @@
RAILS_GROUPS=assets
DELEGATE=athshe.sutty.nl
HAINISH=../haini.sh/haini.sh
DATABASE=
RAILS_ENV=
RAILS_ENV=development
IMAP_SERVER=
DEFAULT_FROM=
EXCEPTION_TO=

View File

@ -2,7 +2,7 @@
# 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
# un clone/pull limpio.
FROM alpine:3.13.5 AS build
FROM alpine:3.13.6 AS build
MAINTAINER "f <f@sutty.nl>"
ARG RAILS_MASTER_KEY
@ -14,10 +14,10 @@ ENV SECRET_KEY_BASE solo_es_necesaria_para_correr_rake
ENV RAILS_ENV production
ENV RAILS_MASTER_KEY=$RAILS_MASTER_KEY
RUN apk add --no-cache libxslt libxml2 tzdata ruby ruby-bundler ruby-json ruby-bigdecimal ruby-rake
RUN apk add --no-cache libxslt libxml2 tzdata ruby ruby-json ruby-bigdecimal ruby-rake
RUN apk add --no-cache postgresql-libs git yarn brotli libssh2 python3
RUN test "2.7.3" = `ruby -e 'puts RUBY_VERSION'`
RUN test "2.7.4" = `ruby -e 'puts RUBY_VERSION'`
# https://github.com/rubygems/rubygems/issues/2918
# 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 adduser -s /bin/sh -G www-data -h /home/app -D app
RUN install -dm750 -o app -g www-data /home/app/sutty
RUN gem install --no-document bundler
RUN gem install --no-document bundler:2.1.4
# Empezamos con la usuaria app
USER app
@ -39,7 +39,8 @@ WORKDIR /home/app/sutty
# Copiamos solo el Gemfile para poder instalar las gemas necesarias
COPY --chown=app:www-data ./Gemfile .
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'
# Vaciar la caché
RUN rm vendor/ruby/2.7.0/cache/*.gem
@ -60,10 +61,6 @@ RUN mv ../sutty/.bundle ./.bundle
# Instalar secretos
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
# Eliminar archivos innecesarios
USER root
@ -71,7 +68,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
# Contenedor final
FROM sutty/monit:latest
FROM registry.nulo.in/sutty/monit:3.13.6
ENV RAILS_ENV production
# Pandoc
@ -79,13 +76,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
# poder reutilizar este primer paso desde otros contenedores
RUN apk add --no-cache libxslt libxml2 tzdata ruby ruby-bundler ruby-json ruby-bigdecimal ruby-rake ruby-irb
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 postgresql-libs libssh2 file rsync git jpegoptim vips
RUN apk add --no-cache ffmpeg imagemagick pandoc tectonic oxipng jemalloc
RUN apk add --no-cache git-lfs openssh-client patch
# Chequear que la versión de ruby sea la correcta
RUN test "2.7.3" = `ruby -e 'puts RUBY_VERSION'`
RUN test "2.7.4" = `ruby -e 'puts RUBY_VERSION'`
# https://github.com/rubygems/rubygems/issues/2918
# https://gitlab.alpinelinux.org/alpine/aports/issues/10808
@ -97,7 +94,7 @@ RUN apk add --no-cache patch && cd /usr/lib/ruby/2.7.0 && patch -Np 0 -i /tmp/ru
# principal
RUN apk add --no-cache yarn
# Instalar foreman para poder correr los servicios
RUN gem install --no-document --no-user-install bundler foreman
RUN gem install --no-document --no-user-install bundler:2.1.4 foreman
# Agregar el grupo del servidor web y la usuaria
RUN addgroup -g 82 -S www-data

22
Gemfile
View File

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

View File

@ -6,6 +6,15 @@ GIT
rails (>= 3.0)
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
remote: https://github.com/fauno/email_address
revision: 536b51f7071b68a55140c0c1726b4cd401d1c04d
@ -18,66 +27,66 @@ GIT
GEM
remote: https://gems.sutty.nl/
specs:
actioncable (6.1.3.2)
actionpack (= 6.1.3.2)
activesupport (= 6.1.3.2)
actioncable (6.1.4.1)
actionpack (= 6.1.4.1)
activesupport (= 6.1.4.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.1.3.2)
actionpack (= 6.1.3.2)
activejob (= 6.1.3.2)
activerecord (= 6.1.3.2)
activestorage (= 6.1.3.2)
activesupport (= 6.1.3.2)
actionmailbox (6.1.4.1)
actionpack (= 6.1.4.1)
activejob (= 6.1.4.1)
activerecord (= 6.1.4.1)
activestorage (= 6.1.4.1)
activesupport (= 6.1.4.1)
mail (>= 2.7.1)
actionmailer (6.1.3.2)
actionpack (= 6.1.3.2)
actionview (= 6.1.3.2)
activejob (= 6.1.3.2)
activesupport (= 6.1.3.2)
actionmailer (6.1.4.1)
actionpack (= 6.1.4.1)
actionview (= 6.1.4.1)
activejob (= 6.1.4.1)
activesupport (= 6.1.4.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.1.3.2)
actionview (= 6.1.3.2)
activesupport (= 6.1.3.2)
actionpack (6.1.4.1)
actionview (= 6.1.4.1)
activesupport (= 6.1.4.1)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.3.2)
actionpack (= 6.1.3.2)
activerecord (= 6.1.3.2)
activestorage (= 6.1.3.2)
activesupport (= 6.1.3.2)
actiontext (6.1.4.1)
actionpack (= 6.1.4.1)
activerecord (= 6.1.4.1)
activestorage (= 6.1.4.1)
activesupport (= 6.1.4.1)
nokogiri (>= 1.8.5)
actionview (6.1.3.2)
activesupport (= 6.1.3.2)
actionview (6.1.4.1)
activesupport (= 6.1.4.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.1.3.2)
activesupport (= 6.1.3.2)
activejob (6.1.4.1)
activesupport (= 6.1.4.1)
globalid (>= 0.3.6)
activemodel (6.1.3.2)
activesupport (= 6.1.3.2)
activerecord (6.1.3.2)
activemodel (= 6.1.3.2)
activesupport (= 6.1.3.2)
activestorage (6.1.3.2)
actionpack (= 6.1.3.2)
activejob (= 6.1.3.2)
activerecord (= 6.1.3.2)
activesupport (= 6.1.3.2)
activemodel (6.1.4.1)
activesupport (= 6.1.4.1)
activerecord (6.1.4.1)
activemodel (= 6.1.4.1)
activesupport (= 6.1.4.1)
activestorage (6.1.4.1)
actionpack (= 6.1.4.1)
activejob (= 6.1.4.1)
activerecord (= 6.1.4.1)
activesupport (= 6.1.4.1)
marcel (~> 1.0.0)
mini_mime (~> 1.0.2)
activesupport (6.1.3.2)
mini_mime (>= 1.1.0)
activesupport (6.1.4.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.7.0)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
adhesiones-jekyll-theme (0.2.1)
jekyll (~> 4.0)
@ -89,13 +98,13 @@ GEM
jekyll-relative-urls (~> 0.0)
jekyll-seo-tag (~> 2.1)
ast (2.4.2)
autoprefixer-rails (10.2.5.0)
execjs (< 2.8.0)
autoprefixer-rails (10.3.3.0)
execjs (~> 2)
bcrypt (3.1.16-x86_64-linux-musl)
bcrypt_pbkdf (1.1.0-x86_64-linux-musl)
benchmark-ips (2.8.4)
benchmark-ips (2.9.2)
bindex (0.8.1-x86_64-linux-musl)
blazer (2.4.2)
blazer (2.4.7)
activerecord (>= 5)
chartkick (>= 3.2)
railties (>= 5)
@ -104,7 +113,7 @@ GEM
autoprefixer-rails (>= 9.1.0)
popper_js (>= 1.14.3, < 2)
sassc-rails (>= 2.0.0)
brakeman (5.0.1)
brakeman (5.1.2)
builder (3.2.4)
capybara (2.18.0)
addressable
@ -113,15 +122,15 @@ GEM
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (>= 2.0, < 4.0)
chartkick (4.0.4)
childprocess (3.0.0)
chartkick (4.1.2)
childprocess (4.1.0)
coderay (1.1.3)
colorator (1.1.0)
commonmarker (0.21.2-x86_64-linux-musl)
ruby-enum (~> 0.5)
concurrent-ruby (1.1.8)
concurrent-ruby-ext (1.1.8-x86_64-linux-musl)
concurrent-ruby (= 1.1.8)
concurrent-ruby (1.1.9)
concurrent-ruby-ext (1.1.9-x86_64-linux-musl)
concurrent-ruby (= 1.1.9)
crass (1.0.6)
database_cleaner (2.0.1)
database_cleaner-active_record (~> 2.0.0)
@ -129,8 +138,8 @@ GEM
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
dead_end (1.1.7)
derailed_benchmarks (2.1.0)
dead_end (3.1.0)
derailed_benchmarks (2.1.1)
benchmark-ips (~> 2)
dead_end
get_process_mem (~> 0)
@ -148,8 +157,8 @@ GEM
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
devise-i18n (1.9.4)
devise (>= 4.7.1)
devise-i18n (1.10.1)
devise (>= 4.8.0)
devise_invitable (2.0.5)
actionmailer (>= 5.0)
devise (>= 4.6)
@ -157,8 +166,8 @@ GEM
dotenv-rails (2.7.6)
dotenv (= 2.7.6)
railties (>= 3.2)
down (5.2.1)
addressable (~> 2.5)
down (5.2.4)
addressable (~> 2.8)
ed25519 (1.2.4-x86_64-linux-musl)
editorial-autogestiva-jekyll-theme (0.3.4)
jekyll (~> 4)
@ -179,48 +188,50 @@ GEM
jekyll-unique-urls (~> 0)
jekyll-write-and-commit-changes (~> 0)
sutty-liquid (~> 0)
em-websocket (0.5.2)
em-websocket (0.5.3)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
http_parser.rb (~> 0)
errbase (0.2.1)
erubi (1.10.0)
eventmachine (1.2.7-x86_64-linux-musl)
exception_notification (4.4.3)
actionmailer (>= 4.0, < 7)
activesupport (>= 4.0, < 7)
execjs (2.7.0)
execjs (2.8.1)
factory_bot (6.2.0)
activesupport (>= 5.0.0)
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
fast_blank (1.0.0-x86_64-linux-musl)
fast_blank (1.0.1-x86_64-linux-musl)
fast_jsonparser (0.5.0-x86_64-linux-musl)
ffi (1.15.0-x86_64-linux-musl)
ffi (1.15.4-x86_64-linux-musl)
flamegraph (0.9.5)
forwardable-extended (2.6.0)
friendly_id (5.4.2)
activerecord (>= 4.0.0)
get_process_mem (0.2.7)
ffi (~> 1.0)
globalid (0.4.2)
activesupport (>= 4.2.0)
globalid (0.6.0)
activesupport (>= 5.0)
groupdate (5.2.2)
activesupport (>= 5)
hairtrigger (0.2.24)
activerecord (>= 5.0, < 7)
ruby2ruby (~> 2.4)
ruby_parser (~> 3.10)
haml (5.2.1)
haml (5.2.2)
temple (>= 0.8.0)
tilt
haml-lint (0.999.999)
haml_lint
haml_lint (0.37.0)
haml_lint (0.37.1)
haml (>= 4.0, < 5.3)
parallel (~> 1.10)
rainbow
rubocop (>= 0.50.0)
sysexits (~> 1.1)
hamlit (2.15.0-x86_64-linux-musl)
hamlit (2.15.1-x86_64-linux-musl)
temple (>= 0.8.2)
thor
tilt
@ -232,24 +243,24 @@ GEM
heapy (0.2.0)
thor
hiredis (0.6.3-x86_64-linux-musl)
http_parser.rb (0.6.0-x86_64-linux-musl)
http_parser.rb (0.8.0-x86_64-linux-musl)
httparty (0.18.1)
mime-types (~> 3.0)
multi_xml (>= 0.5.2)
i18n (1.8.10)
i18n (1.8.11)
concurrent-ruby (~> 1.0)
icalendar (2.7.1)
ice_cube (~> 0.16)
ice_cube (0.16.3)
ice_cube (0.16.4)
image_processing (1.12.1)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
inline_svg (1.7.2)
activesupport (>= 3.0)
nokogiri (>= 1.6)
jbuilder (2.11.2)
jbuilder (2.11.3)
activesupport (>= 5.0.0)
jekyll (4.2.0)
jekyll (4.2.1)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
@ -264,8 +275,8 @@ GEM
rouge (~> 3.0)
safe_yaml (~> 1.0)
terminal-table (~> 2.0)
jekyll-commonmark (1.3.1)
commonmarker (~> 0.14)
jekyll-commonmark (1.3.2)
commonmarker (~> 0.14, < 0.22)
jekyll (>= 3.7, < 5.0)
jekyll-data (1.1.2)
jekyll (>= 3.3, < 5.0.0)
@ -276,21 +287,19 @@ GEM
jekyll (>= 3.7, < 5.0)
jekyll-hardlinks (0.1.2)
jekyll (~> 4)
jekyll-ignore-layouts (0.1.0)
jekyll-ignore-layouts (0.1.2)
jekyll (~> 4)
jekyll-images (0.2.7)
jekyll-images (0.3.0)
jekyll (~> 4)
ruby-filemagic (~> 0.7)
ruby-vips (~> 2)
jekyll-include-cache (0.2.1)
jekyll (>= 3.7, < 5.0)
jekyll-linked-posts (0.2.0)
jekyll-linked-posts (0.4.2)
jekyll (~> 4)
jekyll-locales (0.1.12)
jekyll-lunr (0.2.0)
jekyll-locales (0.1.13)
jekyll-lunr (0.3.0)
loofah (~> 2.4)
jekyll-node-modules (0.1.0)
jekyll (~> 4)
jekyll-order (0.1.4)
jekyll-relative-urls (0.0.6)
jekyll (~> 4)
@ -298,9 +307,9 @@ GEM
sassc (> 2.0.1, < 3.0)
jekyll-seo-tag (2.7.1)
jekyll (>= 3.8, < 5.0)
jekyll-spree-client (0.1.14)
jekyll-spree-client (0.1.19)
fast_blank (~> 1)
spree-api-client (~> 0.2)
spree-api-client (>= 0.2.4)
jekyll-turbolinks (0.0.5)
jekyll (~> 4)
turbolinks-source (~> 5)
@ -308,9 +317,21 @@ GEM
jekyll (~> 4)
jekyll-watch (2.2.1)
listen (~> 3.0)
jekyll-write-and-commit-changes (0.1.2)
jekyll-write-and-commit-changes (0.2.1)
jekyll (~> 4)
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)
rexml
kramdown-parser-gfm (1.1.0)
@ -326,46 +347,46 @@ GEM
ruby_dep (~> 1.2)
loaf (0.10.0)
railties (>= 3.2)
lockbox (0.6.4)
lockbox (0.6.6)
lograge (0.11.2)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.9.1)
loofah (2.12.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
mini_mime (>= 0.1.1)
marcel (1.0.1)
marcel (1.0.2)
memory_profiler (1.0.0)
mercenary (0.4.0)
method_source (1.0.0)
mime-types (3.3.1)
mime-types (3.4.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2021.0225)
mime-types-data (3.2021.1115)
mini_histogram (0.3.1)
mini_magick (4.11.0)
mini_mime (1.0.3)
mini_portile2 (2.5.1)
mini_mime (1.1.2)
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)
mobility (1.1.2)
mobility (1.2.4)
i18n (>= 0.6.10, < 2)
request_store (~> 1.0)
multi_xml (0.6.0)
net-ssh (6.1.0)
netaddr (2.0.4)
nio4r (2.5.7-x86_64-linux-musl)
nokogiri (1.11.5-x86_64-linux-musl)
mini_portile2 (~> 2.5.0)
netaddr (2.0.5)
nio4r (2.5.8-x86_64-linux-musl)
nokogiri (1.12.5-x86_64-linux-musl)
mini_portile2 (~> 2.6.1)
racc (~> 1.4)
orm_adapter (0.5.0)
parallel (1.20.1)
parser (3.0.1.1)
parallel (1.21.0)
parser (3.0.2.0)
ast (~> 2.4.1)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
@ -374,27 +395,27 @@ GEM
activerecord (>= 5.2)
activesupport (>= 5.2)
popper_js (1.16.0)
prometheus_exporter (0.7.0)
prometheus_exporter (1.0.0)
webrick
pry (0.14.1)
coderay (~> 1.1)
method_source (~> 1.0)
public_suffix (4.0.6)
puma (5.3.1-x86_64-linux-musl)
puma (5.5.2-x86_64-linux-musl)
nio4r (~> 2.0)
pundit (2.1.0)
pundit (2.1.1)
activesupport (>= 3.0.0)
racc (1.5.2-x86_64-linux-musl)
racc (1.6.0-x86_64-linux-musl)
rack (2.2.3)
rack-cors (1.1.1)
rack (>= 2.0.0)
rack-mini-profiler (2.3.2)
rack-mini-profiler (2.3.3)
rack (>= 1.2.0)
rack-proxy (0.6.5)
rack-proxy (0.7.0)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
radios-comunitarias-jekyll-theme (0.1.4)
radios-comunitarias-jekyll-theme (0.1.5)
jekyll (~> 4.0)
jekyll-data (~> 1.1)
jekyll-feed (~> 0.9)
@ -405,65 +426,66 @@ GEM
jekyll-relative-urls (~> 0.0)
jekyll-seo-tag (~> 2.1)
jekyll-turbolinks (~> 0)
rails (6.1.3.2)
actioncable (= 6.1.3.2)
actionmailbox (= 6.1.3.2)
actionmailer (= 6.1.3.2)
actionpack (= 6.1.3.2)
actiontext (= 6.1.3.2)
actionview (= 6.1.3.2)
activejob (= 6.1.3.2)
activemodel (= 6.1.3.2)
activerecord (= 6.1.3.2)
activestorage (= 6.1.3.2)
activesupport (= 6.1.3.2)
rails (6.1.4.1)
actioncable (= 6.1.4.1)
actionmailbox (= 6.1.4.1)
actionmailer (= 6.1.4.1)
actionpack (= 6.1.4.1)
actiontext (= 6.1.4.1)
actionview (= 6.1.4.1)
activejob (= 6.1.4.1)
activemodel (= 6.1.4.1)
activerecord (= 6.1.4.1)
activestorage (= 6.1.4.1)
activesupport (= 6.1.4.1)
bundler (>= 1.15.0)
railties (= 6.1.3.2)
railties (= 6.1.4.1)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
rails-html-sanitizer (1.4.2)
loofah (~> 2.3)
rails-i18n (6.0.0)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 7)
rails_warden (0.6.0)
warden (>= 1.2.0)
railties (6.1.3.2)
actionpack (= 6.1.3.2)
activesupport (= 6.1.3.2)
railties (6.1.4.1)
actionpack (= 6.1.4.1)
activesupport (= 6.1.4.1)
method_source
rake (>= 0.8.7)
rake (>= 0.13)
thor (~> 1.0)
rainbow (3.0.0)
rake (13.0.3)
rake (13.0.6)
rb-fsevent (0.11.0)
rb-inotify (0.10.1)
ffi (~> 1.0)
recursero-jekyll-theme (0.1.3)
jekyll (~> 4.0)
recursero-jekyll-theme (0.2.0)
jekyll (~> 4)
jekyll-commonmark (~> 1.3)
jekyll-data (~> 1.1)
jekyll-feed (~> 0.9)
jekyll-dotenv (>= 0.2)
jekyll-feed (~> 0.15)
jekyll-ignore-layouts (~> 0)
jekyll-images (~> 0.2)
jekyll-include-cache (~> 0)
jekyll-linked-posts (~> 0.2)
jekyll-linked-posts (~> 0)
jekyll-locales (~> 0.1)
jekyll-lunr (~> 0.1)
jekyll-node-modules (~> 0.1)
jekyll-order (~> 0.1)
jekyll-relative-urls (~> 0.0)
jekyll-seo-tag (~> 2.1)
jekyll-turbolinks (~> 0)
jekyll-order (~> 0)
jekyll-relative-urls (~> 0)
jekyll-seo-tag (~> 2)
jekyll-unique-urls (~> 0.1)
sutty-archives (~> 2.2)
sutty-liquid (~> 0.1)
redis (4.2.5)
sutty-liquid (~> 0)
redis (4.5.1)
redis-actionpack (5.2.0)
actionpack (>= 5, < 7)
redis-rack (>= 2.1.0, < 3)
redis-store (>= 1.1.0, < 2)
redis-activesupport (5.2.0)
redis-activesupport (5.2.1)
activesupport (>= 3, < 7)
redis-store (>= 1.3, < 2)
redis-rack (2.1.3)
@ -482,19 +504,19 @@ GEM
actionpack (>= 5.0)
railties (>= 5.0)
rexml (3.2.5)
rouge (3.26.0)
rubocop (1.15.0)
rouge (3.26.1)
rubocop (1.23.0)
parallel (~> 1.10)
parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml
rubocop-ast (>= 1.5.0, < 2.0)
rubocop-ast (>= 1.12.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.5.0)
rubocop-ast (1.13.0)
parser (>= 3.0.1.1)
rubocop-rails (2.10.1)
rubocop-rails (2.12.4)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0)
@ -502,17 +524,17 @@ GEM
i18n
ruby-filemagic (0.7.2-x86_64-linux-musl)
ruby-progressbar (1.11.0)
ruby-statistics (2.1.3)
ruby-vips (2.1.2)
ruby-statistics (3.0.0)
ruby-vips (2.1.4)
ffi (~> 1.12)
ruby2ruby (2.4.4)
ruby_parser (~> 3.1)
sexp_processor (~> 4.6)
ruby_dep (1.5.0)
ruby_parser (3.15.1)
sexp_processor (~> 4.9)
rubyzip (2.3.0)
rugged (1.1.0-x86_64-linux-musl)
ruby_parser (3.18.1)
sexp_processor (~> 4.16)
rubyzip (2.3.2)
rugged (1.2.0-x86_64-linux-musl)
safe_yaml (1.0.6)
safely_block (0.3.0)
errbase (>= 0.1.1)
@ -524,11 +546,12 @@ GEM
sprockets (> 3.0)
sprockets-rails
tilt
selenium-webdriver (3.142.7)
childprocess (>= 0.5, < 4.0)
selenium-webdriver (4.1.0)
childprocess (>= 0.5, < 5.0)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2)
semantic_range (3.0.0)
sexp_processor (4.15.2)
sexp_processor (4.16.0)
share-to-fediverse-jekyll-theme (0.1.4)
jekyll (~> 4.0)
jekyll-data (~> 1.1)
@ -540,7 +563,7 @@ GEM
simpleidn (0.2.1)
unf (~> 0.1.4)
sourcemap (0.1.1)
spree-api-client (0.2.1)
spree-api-client (0.2.4)
fast_blank (~> 1)
httparty (~> 0.18.0)
spring (2.1.1)
@ -550,9 +573,9 @@ GEM
sprockets (4.0.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.2)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets-rails (3.4.1)
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
sqlite3 (1.4.2-x86_64-linux-musl)
stackprof (0.2.17-x86_64-linux-musl)
@ -577,14 +600,14 @@ GEM
jekyll-include-cache (~> 0)
jekyll-relative-urls (~> 0.0)
jekyll-seo-tag (~> 2.1)
sutty-liquid (0.7.3)
sutty-liquid (0.7.4)
fast_blank (~> 1.0)
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.0-x86_64-linux-musl)
symbol-fstring (1.0.2-x86_64-linux-musl)
sysexits (1.2.0)
temple (0.8.2)
terminal-table (2.0.0)
@ -601,30 +624,30 @@ GEM
execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.7-x86_64-linux-musl)
unicode-display_width (1.7.0)
unf_ext (0.0.8-x86_64-linux-musl)
unicode-display_width (1.8.0)
validates_hostname (1.0.11)
activerecord (>= 3.0)
activesupport (>= 3.0)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.1.0)
web-console (4.2.0)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webpacker (5.4.0)
webpacker (5.4.3)
activesupport (>= 5.2)
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
webrick (1.7.0)
websocket-driver (0.7.3-x86_64-linux-musl)
websocket-driver (0.7.5-x86_64-linux-musl)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.4.2)
zeitwerk (2.5.1)
PLATFORMS
ruby
@ -638,6 +661,7 @@ DEPENDENCIES
bootstrap (~> 4)
brakeman
capybara (~> 2.13)
chartkick
commonmarker
concurrent-ruby-ext
database_cleaner
@ -670,6 +694,7 @@ DEPENDENCIES
jekyll-data!
jekyll-images
jekyll-include-cache
kaminari
letter_opener
listen (>= 3.0.5, < 3.2)
loaf
@ -680,6 +705,7 @@ DEPENDENCIES
minima
mobility
net-ssh
nokogiri
pg
pg_search
prometheus_exporter
@ -695,6 +721,7 @@ DEPENDENCIES
recursero-jekyll-theme
redis
redis-rails
rollups!
rubocop-rails
rubyzip
rugged

192
Makefile
View File

@ -1,136 +1,121 @@
.SHELL := /bin/bash
# Incluir las variables de entorno
mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
root_dir := $(patsubst %/,%,$(dir $(mkfile_path)))
include $(root_dir)/.env
SHELL := /bin/bash
.DEFAULT_GOAL := help
delegate := athshe
# Copiar el archivo de configuración y avisar cuando hay que
# actualizarlo.
.env: .env.example
@test -f $@ || cp -v $< $@
@test -f $@ && echo "Revisa $@ para actualizarlo con respecto a $<"
@test -f $@ && diff -auN --color $@ $<
assets := package.json yarn.lock $(shell find app/assets/ app/javascript/ -type f)
include .env
alpine_version := 3.13
hain ?= ../haini.sh/haini.sh
export
env ?= staging
# XXX: El espacio antes del comentario cuenta como espacio
args ?=## Argumentos para Hain
commit ?= origin/rails## Commit desde el que actualizar
env ?= staging## Entorno del nodo delegado
sutty ?= $(SUTTY)## Dirección local
delegate ?= $(DELEGATE)## Cambia el nodo delegado
hain ?= ENV_FILE=.env $(HAINISH)## Ubicación de Hainish
# El nodo delegado tiene dos entornos, production y staging.
# Dependiendo del entorno que elijamos, se van a generar los assets y el
# contenedor y subirse a un servidor u otro. No utilizamos CI/CD (aún).
#
# Production es el entorno de panel.sutty.nl
ifeq ($(env),production)
container ?= sutty
## TODO: Cambiar a otra cosa
branch ?= rails
public ?= public
endif
# Staging es el entorno de panel.staging.sutty.nl
ifeq ($(env),staging)
container := staging
branch := staging
public := staging
endif
export
help: always ## Ayuda
@echo -e "Sutty\n" | sed -re "s/^.*/\x1B[38;5;197m&\x1B[0m/"
@echo -e "Servidor: https://panel.$(SUTTY_WITH_PORT)/\n"
@echo -e "Uso: make TAREA args=\"ARGUMENTOS\"\n"
@echo -e "Tareas:\n"
@grep -E "^[a-z\-]+:.*##" Makefile | sed -re "s/(.*):.*##(.*)/\1;\2/" | column -s ";" -t | sed -re "s/^([^ ]+) /\x1B[38;5;197m\1\x1B[0m/"
@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/"
public/packs/manifest.json.br: $(assets)
$(hain) 'cd /Sutty/sutty; PANEL_URL=https://panel.sutty.nl RAILS_ENV=production NODE_ENV=production bundle exec rake assets:precompile assets:clean'
assets: public/packs/manifest.json.br ## Compilar los assets
assets: public/packs/manifest.json.br
test: always ## Ejecutar los tests
$(MAKE) rake args="test RAILS_ENV=test $(args)"
tests := $(shell find test/ -name "*_test.rb")
$(tests): always
$(hain) 'cd /Sutty/sutty; bundle exec rake test TEST="$@" RAILS_ENV=test'
test: always
$(hain) 'cd /Sutty/sutty; RAILS_ENV=test bundle exec rake test'
postgresql: /etc/hosts
postgresql: /etc/hosts ## Iniciar la base de datos
pgrep postgres >/dev/null || $(hain) postgresql
serve: /etc/hosts postgresql
serve-js: /etc/hosts node_modules ## Iniciar el servidor de desarrollo de Javascript
$(hain) 'bundle exec ./bin/webpack-dev-server'
serve: /etc/hosts postgresql Gemfile.lock ## Iniciar el servidor de desarrollo de Rails
$(MAKE) rails args=server
# make rails args="db:migrate"
rails:
rails: ## Corre rails dentro del entorno de desarrollo (pasar argumentos con args=).
$(MAKE) bundle args="exec rails $(args)"
rake:
rake: ## Corre rake dentro del entorno de desarrollo (pasar argumentos con args=).
$(MAKE) bundle args="exec rake $(args)"
bundle:
$(hain) 'cd /Sutty/sutty; bundle $(args)'
bundle: ## Corre bundle dentro del entorno de desarrollo (pasar argumentos con args=).
$(hain) 'bundle $(args)'
yarn:
psql := psql -h $(PG_HOST) -U $(PG_USER) -p $(PG_PORT) -d sutty
copy-table:
test -n "$(table)"
echo "truncate $(table) $(cascade);" | $(psql)
ssh $(delegate) docker exec postgresql pg_dump -U sutty -d sutty -t $(table) | $(psql)
psql:
$(psql)
rubocop: ## Yutea el código que está por ser commiteado
git status --porcelain \
| grep -E "^(A|M)" \
| sed "s/^...//" \
| grep ".rb$$" \
| ../haini.sh/haini.sh "xargs -r ./bin/rubocop --auto-correct"
audit: ## Encuentra dependencias con vulnerabilidades
$(hain) 'gem install bundler-audit'
$(hain) 'bundle audit --update'
brakeman: ## Busca posibles vulnerabilidades en Sutty
$(MAKE) bundle args='exec brakeman'
yarn: ## Tareas de yarn
$(hain) 'yarn $(args)'
# Servir JS con el dev server.
# Esto acelera la compilación del javascript, tiene que correrse por separado
# de serve.
serve-js: /etc/hosts
$(hain) 'cd /Sutty/sutty; bundle exec ./bin/webpack-dev-server'
clean: ## Limpieza
rm -rf _sites/test-* _deploy/test-* log/*.log tmp/cache tmp/letter_opener tmp/miniprofiler tmp/storage
# Limpiar los archivos de testeo
clean:
rm -rf _sites/test-* _deploy/test-*
# Generar la imagen Docker
build: assets
build: Gemfile.lock ## Generar la imagen Docker
time docker build --build-arg="BRANCH=$(branch)" --build-arg="RAILS_MASTER_KEY=`cat config/master.key`" -t sutty/$(container) .
docker tag sutty/$(container):latest sutty:keep
@echo -e "\a"
save:
time docker save sutty/$(container):latest | ssh root@$(delegate).sutty.nl docker load
save: ## Subir la imagen Docker al nodo delegado
time docker save sutty/$(container):latest | ssh root@$(delegate) docker load
date +%F | xargs -I {} git tag -f $(container)-{}
@echo -e "\a"
# proyectos.
../gems/:
mkdir -p $@
ota-js: assets ## Actualizar Javascript en el nodo delegado
rsync -avi --delete-after --chown 1000:82 public/ root@$(delegate):/srv/sutty/srv/http/data/_$(public)/
ssh root@$(delegate) docker exec $(container) sh -c "cat /srv/http/tmp/puma.pid | xargs -r kill -USR2"
# Crear el directorio donde se almacenan las gemas binarias
# TODO: Mover a un proyecto propio, porque lo utilizamos en todos los
gem_dir := $(shell readlink -f ../gems)
gem_cache_dir := $(gem_dir)/cache
gem_binary_dir := $(gem_dir)/$(alpine_version)
ifeq ($(MAKECMDGOALS),build-gems)
gems := $(shell bundle show --paths | xargs -I {} sh -c 'find {}/ext/ -name extconf.rb &>/dev/null && basename {}')
gems := $(patsubst %-x86_64-linux,%,$(gems))
gems := $(patsubst %,$(gem_cache_dir)/%.gem,$(gems))
gems_musl := $(patsubst $(gem_cache_dir)/%.gem,$(gem_binary_dir)/%-x86_64-linux-musl.gem,$(gems))
endif
$(gem_binary_dir)/%-x86_64-linux-musl.gem:
@docker run \
-v $(gem_dir):/srv/gems \
-v `readlink -f ~/.ccache`:/home/builder/.ccache \
-e HTTP_BASIC_USER=$(HTTP_BASIC_USER) \
-e HTTP_BASIC_PASSWORD=$(HTTP_BASIC_PASSWORD) \
-e GEM=`echo $(notdir $*) | sed -re "s/-[^-]+$$//"` \
-e VERSION=`echo $(notdir $*) | sed -re "s/.*-([^-]+)$$/\1/"` \
-e JOBS=2 \
--rm -it \
sutty/gem-compiler:latest || echo "No se pudo compilar $*"
# Compilar todas las gemas binarias y subirlas a gems.sutty.nl para que
# al crear el contenedor no tengamos que compilarlas cada vez
build-gems: $(gems_musl)
cached_gems = $(wildcard $(gem_dir)/cache/*.gem)
rebuild_gems = $(patsubst $(gem_dir)/cache/%.gem,$(gem_dir)/$(alpine_version)/%-x86_64-linux-musl.gem,$(cached_gems))
rebuild-gems: $(rebuild_gems)
dirs := $(patsubst %,root/%,data sites deploy public)
$(dirs):
mkdir -p $@
ota: assets
sudo chgrp -R 82 public/
rsync -avi --delete-after public/ $(delegate):/srv/sutty/srv/http/data/_$(public)/
ssh $(delegate) docker exec $(container) sh -c "cat /srv/http/tmp/puma.pid | xargs -r kill -USR2"
# Hotfixes
#
# TODO: Reemplazar esto por git pull en el contenedor
commit ?= origin/rails
ota-rb:
ota: ## Actualizar Rails en el nodo delegado
umask 022; git format-patch $(commit)
scp ./0*.patch $(delegate):/tmp/
ssh $(delegate) mkdir -p /tmp/patches-$(commit)/
scp ./0*.patch $(delegate):/tmp/patches-$(commit)/
scp ./ota.sh $(delegate):/tmp/
@ -140,6 +125,19 @@ ota-rb:
ssh $(delegate) docker exec $(container) ota $(commit)
rm ./0*.patch
# Todos los archivos de assets. Si alguno cambia, se van a recompilar
# los assets que luego se suben al nodo delegado.
assets := package.json yarn.lock $(shell find app/assets/ app/javascript/ -type f)
public/packs/manifest.json.br: $(assets)
$(hain) 'PANEL_URL=https://panel.sutty.nl RAILS_ENV=production NODE_ENV=production bundle exec rake assets:precompile assets:clean'
# Correr un test en particular por ejemplo
# `make test/models/usuarie_test.rb`
tests := $(shell find test/ -name "*_test.rb")
$(tests): always
$(MAKE) test args="TEST=$@"
# Agrega las direcciones locales al sistema
/etc/hosts: always
@echo "Chequeando si es necesario agregar el dominio local $(SUTTY)"
@grep -q " $(SUTTY)$$" $@ || echo -e "127.0.0.1 $(SUTTY)\n::1 $(SUTTY)" | sudo tee -a $@
@ -147,4 +145,12 @@ ota-rb:
@grep -q " panel.$(SUTTY)$$" $@ || echo -e "127.0.0.1 panel.$(SUTTY)\n::1 panel.$(SUTTY)" | sudo tee -a $@
@grep -q " postgresql.$(SUTTY)$$" $@ || echo -e "127.0.0.1 postgresql.$(SUTTY)\n::1 postgresql.$(SUTTY)" | sudo tee -a $@
# Instala las dependencias de Javascript
node_modules: package.json
$(MAKE) yarn
# Instala las dependencias de Rails
Gemfile.lock: Gemfile
$(MAKE) bundle args=install
.PHONY: always

View File

@ -15,6 +15,17 @@ Este repositorio es la plataforma _Ruby on Rails_ para alojar el
Para más información visita el [sitio de Sutty](https://sutty.nl/).
### Desarrollar
Todas las tareas se gestionan con `make`, por favor instala GNU Make
antes de comenzar.
```bash
make help
```
[Leer la documentación](https://docs.sutty.nl/)
## English
Sutty is a platform for hosting safer, faster and more resilient
@ -25,3 +36,13 @@ This repository is the Ruby on Rails platform that hosts the
self-managed [panel](https://panel.sutty.nl/).
For more information, visit [Sutty's website](https://sutty.nl/en/).
### Development
Every task is run via `make`, please install GNU Make before developing.
```bash
make help
```
[Read the documentation](https://docs.sutty.nl/en/)

View File

@ -2,6 +2,13 @@
box-sizing: border-box;
*, *::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 {
min-height: 1.5rem;
}
@ -64,6 +71,10 @@
strong, em, del, u, sub, sup, small { background: #0002; }
a { background: #13fefe50; }
[data-editor-selected] { outline: #f206f9 solid thick; }
p[data-multimedia-inner] {
// Ignorar clicks en el párrafo placeholder
pointer-events: none;
}
}
*[data-editor-loading] {

View File

@ -19,21 +19,21 @@ class PostsController < ApplicationController
# XXX: Cada vez que cambiamos un Post tocamos el sitio con lo que es
# más simple saber si hubo cambios.
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
return unless stale?([current_usuarie, site, filter_params])
# Filtrar los posts que les invitades no pueden ver
@usuarie = site.usuarie? current_usuarie
end
# 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
# Filtrar los posts que les invitades no pueden ver
@usuarie = site.usuarie? current_usuarie
end
def show
@ -51,7 +51,7 @@ class PostsController < ApplicationController
def new
authorize Post
@post = site.posts.build(lang: locale, layout: params[:layout])
@post = site.posts(lang: locale).build(layout: params[:layout])
breadcrumb I18n.t('loaf.breadcrumbs.posts.new', layout: @post.layout.humanized_name.downcase), ''
end
@ -151,7 +151,9 @@ class PostsController < ApplicationController
#
# @return [Hash]
def filter_params
@filter_params ||= params.permit(:q, :category, :layout).to_h.select { |_, v| v.present? }
@filter_params ||= params.permit(:q, :category, :layout).to_hash.select do |_, v|
v.present?
end.transform_keys(&:to_sym)
end
def site

View File

@ -0,0 +1,168 @@
# frozen_string_literal: true
# Estadísticas del sitio
class StatsController < ApplicationController
include Pundit
include ActionView::Helpers::DateHelper
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
@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
authorize SiteStat.new(@site)
end
# TODO: Eliminar cuando mergeemos referer-origin
def hostnames
@hostnames ||= [@site.hostname, @site.alternative_hostnames].flatten
end
# 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

View File

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

View File

@ -5,13 +5,23 @@ class DeployJob < ApplicationJob
class DeployException < StandardError; end
# rubocop:disable Metrics/MethodLength
def perform(site, notify = true)
def perform(site, notify = true, time = Time.now)
ActiveRecord::Base.connection_pool.with_connection do
@site = Site.find(site)
# Si ya hay una tarea corriendo, aplazar esta
# Si ya hay una tarea corriendo, aplazar esta. Si estuvo
# esperando más de 10 minutos, recuperar el estado anterior.
#
# Como el trabajo actual se aplaza al siguiente, arrastrar la
# hora original para poder ir haciendo timeouts.
if @site.building?
DeployJob.perform_in(60, site, notify)
if 10.minutes.ago >= time
@site.update status: 'waiting'
raise DeployException,
"#{@site.name} la tarea estuvo más de 10 minutos esperando, volviendo al estado original"
end
DeployJob.perform_in(60, site, notify, time)
return
end
@ -29,8 +39,11 @@ class DeployJob < ApplicationJob
end
deploy_others
notify_usuaries if notify
# Volver a la espera
@site.update status: 'waiting'
notify_usuaries if notify
end
end
# rubocop:enable Metrics/MethodLength

55
app/jobs/periodic_job.rb Normal file
View File

@ -0,0 +1,55 @@
# 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

@ -0,0 +1,67 @@
# 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

@ -0,0 +1,106 @@
# 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,4 +1,12 @@
# frozen_string_literal: true
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

View File

@ -62,10 +62,15 @@ class DeployLocal < Deploy
'AIRBRAKE_PROJECT_ID' => site.id.to_s,
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
'JEKYLL_ENV' => Rails.env,
'LANG' => ENV['LANG']
'LANG' => ENV['LANG'],
'YARN_CACHE_FOLDER' => yarn_cache_dir
}
end
def yarn_cache_dir
Rails.root.join('_yarn_cache').to_s
end
def yarn_lock
File.join(site.path, 'yarn.lock')
end
@ -80,9 +85,9 @@ class DeployLocal < Deploy
# Corre yarn dentro del repositorio
def yarn
return unless yarn_lock?
return true unless yarn_lock?
run 'yarn'
run 'yarn install --production'
end
def bundle

View File

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

View File

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

View File

@ -0,0 +1,31 @@
# 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,6 +2,12 @@
# Este metadato permite generar rutas manuales.
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.sub(%r{\A/}, '') unless post.new?
end
# Los permalinks nunca pueden ser privados
def private?
false

View File

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

10
app/models/stat.rb Normal file
View File

@ -0,0 +1,10 @@
# 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

@ -0,0 +1,27 @@
# frozen_string_literal: true
# Política de acceso a las estadísticas
class SiteStatPolicy
attr_reader :site_stat, :usuarie
def initialize(usuarie, site_stat)
@usuarie = usuarie
@site_stat = site_stat
end
def index?
site_stat.site.usuarie? usuarie
end
def host?
index?
end
def resources?
index?
end
def uris?
index?
end
end

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
.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

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

View File

@ -38,4 +38,4 @@
- cache [metadata, I18n.locale] do
%section.editor{ id: attr, dir: dir }
= @post.public_send(attr).to_s.html_safe
= @post.public_send(attr).value.html_safe

View File

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

View File

@ -1,17 +1,43 @@
= render 'layouts/breadcrumb',
crumbs: [link_to(t('sites.index.title'), sites_path),
link_to(@site.name, site_path(@site)), t('.title')]
.row
.col
%h1= t('.title')
%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}.
%table.table.table-condensed
%tbody
%tr
%td= t('.build.average')
%td= distance_of_time_in_words_if_more_than_a_minute @build_avg
%tr
%td= t('.build.maximum')
%td= distance_of_time_in_words_if_more_than_a_minute @build_max
.mb-5
- 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}"
.mb-5
%h2= t('.host.title', count: @hostnames.size)
%p.lead= t('.host.description')
= line_chart site_stats_host_path(@chart_params), **@chart_options
.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,6 +11,8 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.singular 'licencias', 'licencia'
inflect.plural 'rol', 'roles'
inflect.singular 'roles', 'rol'
inflect.plural 'rollup', 'rollups'
inflect.singular 'rollups', 'rollup'
end
ActiveSupport::Inflector.inflections(:es) do |inflect|
@ -24,4 +26,6 @@ ActiveSupport::Inflector.inflections(:es) do |inflect|
inflect.singular 'roles', 'rol'
inflect.plural 'licencia', 'licencias'
inflect.singular 'licencias', 'licencia'
inflect.plural 'rollup', 'rollups'
inflect.singular 'rollups', 'rollup'
end

View File

@ -19,8 +19,8 @@ en:
remember_me: 'Keeps session open for %{remember_for}'
actions:
sr-help: "After this form you'll find links to recover your account and other actions."
_true: Yes
_false: No
_true: 'Yes'
_false: 'No'
svg:
sutty:
title: Sutty
@ -163,7 +163,7 @@ en:
signature: 'With love, Sutty'
breadcrumb:
title: 'Your location in Sutty'
logout: Exit
logout: Log out
mutual_aid: Mutual aid
collaborations:
collaborate:
@ -252,9 +252,38 @@ en:
help: |
These statistics show information about how your site is generated and
how many resources it uses.
build:
average: 'Average building time'
maximum: 'Maximum building time'
last_update: 'Updated every hour. Last update on '
empty: 'There is no enough information yet. We invite you to come back in %{please_return_at}!'
loading: 'Loading...'
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:
donations:
url: 'https://donaciones.sutty.nl/en/'
@ -376,6 +405,8 @@ en:
en: 'English'
ar: 'Arabic'
posts:
prev: Previous page
next: Next page
empty: "There are no results for those search parameters."
caption: Post list
attribute_ro:
@ -413,6 +444,8 @@ en:
destroy: Remove image
belongs_to:
empty: "(Empty)"
predefined_value:
empty: "(Empty)"
draft:
label: Draft
reorder:

View File

@ -19,8 +19,8 @@ es:
remember_me: 'Mantiene la sesión abierta por %{remember_for}'
actions:
sr-help: 'Después del formulario encontrarás vínculos para recuperar tu cuenta, entre otras acciones.'
_true:
_false: No
_true: 'Sí'
_false: 'No'
svg:
sutty:
title: Sutty
@ -163,7 +163,7 @@ es:
signature: 'Con cariño, Sutty'
breadcrumb:
title: 'Tu ubicación en Sutty'
logout: Salir
logout: Cerrar sesión
mutual_aid: Ayuda mutua
collaborations:
collaborate:
@ -257,9 +257,38 @@ es:
help: |
Las estadísticas visibilizan información sobre cómo se genera y
cuántos recursos utiliza tu sitio.
build:
average: 'Tiempo promedio de generación'
maximum: 'Tiempo máximo de generación'
last_update: 'Actualizadas cada hora. Última actualización hace '
empty: 'Todavía no hay información suficiente. Te invitamos a volver en %{please_return_at} :)'
loading: 'Cargando...'
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:
donations:
url: 'https://donaciones.sutty.nl/'
@ -384,6 +413,8 @@ es:
en: 'inglés'
ar: 'árabe'
posts:
prev: Página anterior
next: Página siguiente
empty: No hay artículos con estos parámetros de búsqueda.
caption: Lista de artículos
attribute_ro:
@ -421,6 +452,8 @@ es:
destroy: 'Eliminar imagen'
belongs_to:
empty: "(Vacío)"
predefined_value:
empty: "(Vacío)"
draft:
label: Borrador
reorder:

View File

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

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
# Agrega la columna de request_uri a la tabla de logs
class AddRequestUriToAccessLogs < ActiveRecord::Migration[6.1]
def change
return unless Rails.env.production?
add_column :access_logs, :request_uri, :string, default: ''
end
end

View File

@ -0,0 +1,15 @@
# 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

@ -0,0 +1,18 @@
# 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

@ -0,0 +1,10 @@
# 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

@ -0,0 +1,12 @@
# 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,8 @@
"@rails/ujs": "^6.1.3-1",
"@rails/webpacker": "5.2.1",
"babel-loader": "^8.2.2",
"chart.js": "2.9.3",
"chartkick": "3.2.1",
"chart.js": "^3.5.1",
"chartkick": "^4.0.5",
"circular-dependency-plugin": "^5.2.2",
"commonmark": "^0.29.0",
"fork-awesome": "^1.1.7",

View File

@ -2119,33 +2119,24 @@ chalk@^4.1.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chart.js@2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.3.tgz#ae3884114dafd381bc600f5b35a189138aac1ef7"
integrity sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw==
dependencies:
chartjs-color "^2.1.0"
moment "^2.10.2"
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-color-string@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
dependencies:
color-name "^1.0.0"
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==
chartjs-color@^2.1.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0"
integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
dependencies:
chartjs-color-string "^0.6.0"
color-convert "^1.9.3"
chartkick@3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/chartkick/-/chartkick-3.2.1.tgz#a80c2005ae353c5ae011d0a756b6f592fc8fc7a9"
integrity sha512-zV0kUeZNqrX28AmPt10QEDXHKadbVFOTAFkCMyJifHzGFkKzGCDXxVR8orZ0fC1HbePzRn5w6kLCOVxDQbMUCg==
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:
version "2.1.8"
@ -2772,6 +2763,11 @@ dashdash@^1.12.0:
dependencies:
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:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"