Compare commits

..

99 commits

Author SHA1 Message Date
188e9dac0d CI: Actualizar haini.sh y correr rootless
Some checks failed
continuous-integration/woodpecker the build failed
2021-11-30 15:43:38 +00:00
d9f018d6f2 Makefile: no hardcodear haini.sh en rubocop
Some checks failed
continuous-integration/woodpecker the build failed
2021-11-30 15:28:15 +00:00
0037abcada CI: Preparar entorno de desarrollo para testear
Some checks failed
continuous-integration/woodpecker the build failed
2021-11-30 15:27:34 +00:00
a69a5cefb6 CI: Correr make test en vez de make tests
Some checks failed
continuous-integration/woodpecker the build failed
2021-11-30 15:17:14 +00:00
4fb53f1331 CI: Instalar dependencias en vendor/ para que persistan entre pasos 2021-11-30 15:17:02 +00:00
56eb3ba9e4 CI: Correr cosas paralelamente
Some checks failed
continuous-integration/woodpecker the build failed
2021-11-30 15:08:43 +00:00
64d5558996 CI: Correr tests
Some checks failed
continuous-integration/woodpecker the build failed
2021-11-30 15:04:17 +00:00
f8c8d45fde Agregar CI de auditoría
Some checks failed
continuous-integration/woodpecker the build failed
2021-11-30 15:02:13 +00:00
Maki
e9a9ec0364 Merge branch 'shorter-description' into 'rails'
permitir descripciones mas cortas

See merge request sutty/sutty!70
2021-11-08 10:27:33 +00:00
f
66ec526937 permitir descripciones mas cortas 2021-11-04 10:42:57 -03:00
385d943a2c Merge branch 'arreglar-editor-oscuro' into 'rails'
Editor: forzar modo claro

Closes #2135

See merge request sutty/sutty!69
2021-10-30 13:22:25 +00:00
21f650fc57 Editor: forzar modo claro
Esto hace que sea legible y más usable cuando el modo oscuro está
activado. https://0xacab.org/sutty/sutty/-/issues/2135

Idealmente, me gustaría tener modo oscuro real en el editor.
2021-10-28 16:15:33 -03:00
Maki
cebee29ac4 Merge branch 'permalinks-estaticos' into 'rails'
guardar y mostrar la url actual en el campo permalink

See merge request sutty/sutty!60
2021-10-28 16:22:53 +00:00
Maki
f0613aef25 Merge branch 'rollups' into 'rails'
Estadísticas

Closes #3228

See merge request sutty/sutty!61
2021-10-28 16:22:02 +00:00
58bb42e532 Merge branch 'reorder-with-locale' into 'rails'
enviar el idioma al reordenar!

See merge request sutty/sutty!67
2021-10-27 19:35:20 +00:00
f
25fe41bfa2 enviar el idioma al reordenar!
los tests envían el idioma pero no el panel
2021-10-27 10:12:28 -03:00
ad27a938f9 Merge branch 'assets' into 'rails'
no instalar las gemas de assets en producción

See merge request sutty/sutty!64
2021-10-26 19:24:02 +00:00
f
7a780188e4 asegurarse que siempre buscamos assets 2021-10-26 16:19:42 -03:00
f
c84462c4a8 recolectar estadísticas usando menos recursos 2021-10-26 11:33:15 -03:00
f
ab004fae70 decodificar las urls para poder buscarlas en el log 2021-10-22 18:21:47 -03:00
f
449829ff09 Merge branch 'rails' into rollups 2021-10-22 18:02:34 -03:00
Maki
05edb296d0 Merge branch 'change-licencias' into 'rails'
ignorar los cambios de licencia en idiomas no soportados

See merge request sutty/sutty!65
2021-10-22 20:55:58 +00:00
f
84e543ac07 ignorar los cambios de licencia en idiomas no soportados 2021-10-22 17:13:26 -03:00
f
8bebe155f4 no hace falta subir los parches dos veces 2021-10-22 17:06:34 -03:00
f
570761fab5 actualizar contenedor con una versión específica de bundler 2021-10-21 20:55:40 -03:00
f
084bf8547f ya no es necesario hacer limpieza en docker 2021-10-21 20:54:50 -03:00
f
fd784919df rails ya corre yarn antes de compilar 2021-10-21 20:54:12 -03:00
f
7227ecfe2a solo instalar las gemas de assets en desarrollo 2021-10-21 20:53:25 -03:00
f
4456deff9c usar la traducción del idioma que viene del sitio 2021-10-21 15:02:07 -03:00
f
5d40907199 Merge branch 'rails' into rollups 2021-10-21 14:46:57 -03:00
f
9c4a0a86f3 deshabilitar el paginado temporalmente
no permite mover posts entre paginas!
2021-10-21 14:40:55 -03:00
f
69ef571bdb garantizar que sea una hash de simbolos 2021-10-21 14:25:05 -03:00
f
883182ffe2 rutas más específicas 2021-10-21 13:45:34 -03:00
f
e3d2213afc especificar el idioma de un post nuevo 2021-10-21 13:44:34 -03:00
f
3335d0d9eb usar una guard como sugiere rubocop 2021-10-21 13:25:05 -03:00
f
6349fd7b40 los url helpers necesitan hashes o params
y params.to_h devuelve un hashwithindifferentaccess, lo que rompia las
urls con locale
2021-10-21 13:23:35 -03:00
f
08e0a0ca9d traducir correctamente 2021-10-21 10:49:02 -03:00
f
d67d43ff1e hacer más claro el código 2021-10-21 10:48:34 -03:00
f
090b630525 la lógica estaba invertida y los valores nunca llegaban al archivo 2021-10-21 10:47:32 -03:00
f
7e16962041 no fallar si no hay stat
closes #3228
2021-10-20 18:48:30 -03:00
f
07e5baa378 Merge branch 'rails' into rollups 2021-10-20 16:29:45 -03:00
f
ba7d0c2e00 Merge branch 'paginacion' into rails 2021-10-20 16:29:25 -03:00
f
b7c2061f80 Merge branch 'rails' into rollups 2021-10-20 13:32:13 -03:00
f
8d38d0d2ae recolección de estadísticas con mejor performance 2021-10-20 13:14:19 -03:00
f
9cf7c62861 procesar uris a demanda 2021-10-20 13:08:21 -03:00
f
849ee4491c instalar las librerías en producción 2021-10-20 12:58:51 -03:00
f
1497113f73 usar menos intervalos
las horas y semanas generan demasiados rollups
2021-10-20 12:58:19 -03:00
f
cc3535097e todavía no empezar la recolección automática 2021-10-20 12:57:11 -03:00
f
245973b519 no fallar si todavía no hay stats 2021-10-20 12:56:37 -03:00
f
d06edc2f62 usar rollups desde git para poder hacer un rollup recursivo 2021-10-20 12:52:31 -03:00
f
aa86ead112 ruboyuta 2021-10-09 18:29:53 -03:00
f
356db85465 empezar a recolectar estadísticas cuando se inicia el panel 2021-10-09 18:28:38 -03:00
f
ec775847b9 sugerir algunas URLs 2021-10-09 18:27:42 -03:00
f
810f71e9c1 contar por qué mostramos los recursos usados 2021-10-09 18:25:43 -03:00
f
fd2e2509d0 sin comillas 2021-10-09 17:29:09 -03:00
f
5dad13bc3c más espacio entre los gráficos 2021-10-09 17:28:56 -03:00
f
7ab2ec5933 no fallar si el intervalo está vacío 2021-10-09 17:28:26 -03:00
f
6c485e5e18 obtener estadísticas de cualquier link 2021-10-09 17:27:45 -03:00
f
5ca8e3c923 usar guards 2021-10-09 15:52:18 -03:00
f
f36ea9629d centralizar valores 2021-10-09 15:50:38 -03:00
f
c927a56bef ejecutar una vez por hora exacto 2021-10-09 15:47:42 -03:00
f
11568e6ce8 no distinguir entre todas las uris
para poder buscar descargas de archivos binarios después
2021-10-09 15:36:14 -03:00
f
e35cbe79eb recolectar visitas a páginas 2021-10-09 14:46:58 -03:00
f
306d1ed983 recolectar estadísticas una vez por hora 2021-10-09 14:45:34 -03:00
f
0342455955 encontrar páginas y distinguir si fueron cyborgs o no 2021-10-08 18:42:34 -03:00
f
51c2fdf6d6 fixup! fixup! mostrar gráficos de recursos utilizados 2021-10-08 18:41:07 -03:00
f
86f1ac4504 fixup! mostrar gráficos de recursos utilizados 2021-10-08 18:40:52 -03:00
f
833213ec80 agregar opciones a cada recurso 2021-10-08 18:40:16 -03:00
f
224ea1ebc5 mostrar gráficos de recursos utilizados 2021-10-08 18:39:55 -03:00
f
8e9401036c cachear los resultados hasta la próxima actualización 2021-10-08 18:38:02 -03:00
f
ef0055db05 agrupar por año 2021-10-08 18:36:31 -03:00
f
d2b1220df6 informar cuando no hay datos 2021-10-08 18:35:40 -03:00
f
38090d7de7 resaltar el intervalo seleccionado 2021-10-08 18:34:16 -03:00
f
cfb2d7a61d todas las peticiones necesitan une usuarie 2021-10-08 18:33:31 -03:00
f
a2e4e0ab89 informar hace cuánto se actualizaron los datos 2021-10-08 18:32:35 -03:00
f
df2b66afe8 llevar el registro de las recolecciones de estadísticas 2021-10-08 18:21:09 -03:00
f
a7ae7f8e8d ver cantidad de visitas 2021-10-08 16:31:02 -03:00
f
930f88903e instalar chartkick para generar gráficos 2021-10-07 15:24:21 -03:00
f
56cb11dc49 Merge branch 'predefined-value' into rails 2021-10-05 09:49:19 -03:00
Maki
42896a5f33 Merge branch 'uniqueness' into 'rails'
Agregar índices únicos que pensábamos que teníamos

See merge request sutty/sutty!59
2021-10-04 17:53:29 +00:00
Maki
f8b1752c04 cambio texto cerrar sesion #2896 2021-10-04 14:49:14 -03:00
f
662b5eeec4 guardar y mostrar la url actual en el campo permalink 2021-10-04 14:48:19 -03:00
f
5c2e5fd62e Agregar índices únicos que pensábamos que teníamos
`unique: true` es un parámetro de `add_index` no de `add_column`.
2021-09-26 19:29:03 -03:00
f
0dece732aa Al buscar eliminar la paginación 2021-09-15 21:03:28 -03:00
f
f90c92dc26 Poder navegar páginas en la lista de artículos 2021-09-15 21:03:07 -03:00
f
8e1f5c5558 Paginación 2021-09-15 21:02:09 -03:00
f
859b8518c0 Mostrar el tipo de artículo 2021-09-15 21:00:48 -03:00
f
5a324ae71f No cortar las columnas 2021-09-15 21:00:10 -03:00
f
c601845a27 Garantizar que todas las lecturas se hacen dentro del directorio del sitio
fixes ##2667

fixes ##2655

fixes ##2640

fixes #2675

fixes #2653

fixes #2635

fixes #2624

fixes #2626

fixes #2627

fixes #2629

fixes #2634

fixes #2636

fixes #2637

fixes #2641

fixes #2642

fixes #2643

fixes #2644

fixes #2645

fixes #2646

fixes #2648

fixes #2649

fixes #2650

fixes #2651

fixes #2654

fixes #2657

fixes #2672

fixes #2676

fixes #2677

fixes #2678

fixes #2681

fixes #2682

fixes #2687

fixes #2688

fixes #2689

fixes #2691

fixes #2692

fixes #2693
2021-09-15 19:54:27 -03:00
f
47096f20b7 Traducción de valores vacíos 2021-09-11 19:43:27 -03:00
f
a242ceee68 Siempre guardar el valor de los campos booleanos 2021-09-11 17:26:44 -03:00
f
9b8c09cb00 Soportar campos númericos con decimales 2021-09-11 17:06:27 -03:00
f
1623ab73de Soportar un campo con una lista de valores predefinidos y elegir uno 2021-09-11 17:00:28 -03:00
f
4e4b5888a3 Peticiones completas 2021-09-03 15:55:30 -03:00
f
016da28529 Inflexiones para Rollup 2021-09-03 15:53:45 -03:00
f
604c16bfb8 Agregar created_at a access_logs para poder agrupar por fecha 2021-09-03 15:28:45 -03:00
f
094a8092de Agregar rollups 2021-09-03 15:25:41 -03:00
Maki
c1a9aaa037 Merge branch 'only-urls-allowed' into 'rails'
Solo permitir URLs web al sanitizar

Closes #2382

See merge request sutty/sutty!54
2021-08-16 15:36:30 +00:00
f
0bd8a2243e Solo permitir URLs web al sanitizar
fixes #2382
2021-08-11 10:25:05 -03:00
40 changed files with 873 additions and 114 deletions

View file

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

27
.woodpecker.yml Normal file
View file

@ -0,0 +1,27 @@
pipeline:
dependencias:
image: registry.nulo.in/sutty/haini.sh:rootless
commands:
- sudo chown suttier:suttier -R .
- bundle config set --local path 'vendor/'
- make bundle hain="sh -c"
brakeman:
group: audit
image: registry.nulo.in/sutty/haini.sh:rootless
commands:
- make brakeman hain="sh -c"
rubocup:
group: audit
image: registry.nulo.in/sutty/haini.sh:rootless
commands:
- make rubocop hain="sh -c"
tests:
group: audit
image: registry.nulo.in/sutty/haini.sh:rootless
commands:
- make postgresql hain="sh -c"
- echo -n z8p4KI/XRbGPdxPsNux8ys1gvL4+97DrrvPyt7gugJog3o3x/UEIyedkKUq9FWHOS9ltrsUN6NpN5Dsme+iHbMC/FrRjDmDvOoHpP/pqy924l6IgU8OK3m2Y28AU7eqiYvf6kJd5s4KmPJDiH9AQRx4QRy4jG5DfMHBew6EumqedgvRRFtAc3++GPH2qPnO8SYapRM4FXXUTjP3fNdRVD1Fqm7chUra4Qng1JhnzdMlOUhCPfD1Rmeh+X2TltzYhdPMFH3U3fJV7xCkitxu5PQgWfxMhb9FVF68Uvykbt/rod4IE6ZmAmPyyGktYuQSI2t1kkpAV4MOG4ag9aC/RLmi23rt+fVoYJREHga+NQ0YjVSGbBlINIDACr1iL+abtNmHhtfY+o9unlD7xy3UP0EdqTx6WncVJn02D--pfdBRF+zxL1uqoWs--4OJ7axQaFf9git6zUtUGOA== > config/credentials.yml.enc
- make rake args="db:prepare" hain="sh -c"
- make rake args="db:migrate" hain="sh -c"
- make rake args="db:seed" hain="sh -c"
- make test 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.5 AS build FROM alpine:3.13.6 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-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 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://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 RUN gem install --no-document bundler:2.1.4
# Empezamos con la usuaria app # Empezamos con la usuaria app
USER app USER app
@ -39,7 +39,8 @@ 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
@ -60,10 +61,6 @@ 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
@ -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 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 sutty/monit:latest FROM registry.nulo.in/sutty/monit:3.13.6
ENV RAILS_ENV production ENV RAILS_ENV production
# Pandoc # 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 # 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-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 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.3" = `ruby -e 'puts RUBY_VERSION'` RUN test "2.7.4" = `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
@ -97,7 +94,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 foreman RUN gem install --no-document --no-user-install bundler:2.1.4 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,13 +11,16 @@ 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
# gem 'therubyracer', platforms: :ruby # Solo incluir las gemas cuando estemos en desarrollo o compilando los
# Use SCSS for stylesheets # assets. No es necesario instalarlas en producción.
gem 'sassc-rails' #
# Use Uglifier as compressor for JavaScript assets # XXX: Supuestamente Rails ya soporta RAILS_GROUPS, pero Bundler no.
gem 'uglifier', '>= 1.3.0' if ENV['RAILS_GROUPS']&.split(',')&.include? 'assets'
gem 'bootstrap', '~> 4' gem 'sassc-rails'
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
@ -28,6 +31,7 @@ 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'
@ -58,6 +62,7 @@ gem 'rails-i18n'
gem 'rails_warden' gem 'rails_warden'
gem 'redis', require: %w[redis redis/connection/hiredis] gem 'redis', require: %w[redis redis/connection/hiredis]
gem 'redis-rails' gem 'redis-rails'
gem 'rollups', git: 'https://github.com/ankane/rollup.git', branch: 'master'
gem 'rubyzip' gem 'rubyzip'
gem 'rugged' gem 'rugged'
gem 'concurrent-ruby-ext' gem 'concurrent-ruby-ext'
@ -67,6 +72,7 @@ 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,6 +6,15 @@ 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
@ -205,6 +214,8 @@ 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)
@ -309,6 +320,18 @@ 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)
@ -345,6 +368,7 @@ 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)
@ -357,7 +381,8 @@ 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) nokogiri (1.11.7-x86_64-linux-musl)
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)
@ -635,6 +660,7 @@ 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
@ -667,6 +693,7 @@ 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
@ -692,6 +719,7 @@ 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: node_modules public/packs/manifest.json.br ## Compilar los assets assets: 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)"
@ -76,7 +76,7 @@ rubocop: ## Yutea el código que está por ser commiteado
| grep -E "^(A|M)" \ | grep -E "^(A|M)" \
| sed "s/^...//" \ | sed "s/^...//" \
| grep ".rb$$" \ | grep ".rb$$" \
| ../haini.sh/haini.sh "xargs -r ./bin/rubocop --auto-correct" | $(hain) "xargs -r ./bin/rubocop --auto-correct"
audit: ## Encuentra dependencias con vulnerabilidades audit: ## Encuentra dependencias con vulnerabilidades
$(hain) 'gem install bundler-audit' $(hain) 'gem install bundler-audit'
@ -108,7 +108,6 @@ 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,6 +2,13 @@
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,7 +22,8 @@ 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.
if stale?([current_usuarie, site, filter_params]) return unless stale?([current_usuarie, site, filter_params])
# Todos los artículos de este sitio para el idioma actual # Todos los artículos de este sitio para el idioma actual
@posts = site.indexed_posts.where(locale: locale) @posts = site.indexed_posts.where(locale: locale)
# De este tipo # De este tipo
@ -37,7 +38,6 @@ class PostsController < ApplicationController
# Filtrar los posts que les invitades no pueden ver # Filtrar los posts que les invitades no pueden ver
@usuarie = site.usuarie? current_usuarie @usuarie = site.usuarie? current_usuarie
end end
end
def show def show
authorize post authorize post
@ -54,7 +54,7 @@ class PostsController < ApplicationController
def new def new
authorize Post 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), '' breadcrumb I18n.t('loaf.breadcrumbs.posts.new', layout: @post.layout.humanized_name.downcase), ''
end end
@ -154,7 +154,9 @@ class PostsController < ApplicationController
# #
# @return [Hash] # @return [Hash]
def filter_params 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 end
def site def site

View file

@ -3,16 +3,166 @@
# 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
# Solo queremos el promedio de tiempo de compilación, no de # TODO: Eliminar cuando mergeemos referer-origin
# instalación de dependencias. def hostnames
stats = @site.build_stats.jekyll @hostnames ||= [@site.hostname, @site.alternative_hostnames].flatten
@build_avg = stats.average(:seconds).to_f.round(2) end
@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,6 +18,7 @@ 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()

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 # 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,10 +25,19 @@ class MetadataBoolean < MetadataTemplate
# * false # * false
# * true # * true
def value def value
return document.data.fetch(name.to_s, default_value) if self[:value].nil? case self[:value]
return self[:value] unless self[:value].is_a? String 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 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.hostname.end_with? Site.domain element.remove unless uri.scheme == 'https' && uri.hostname.end_with?(Site.domain)
rescue URI::Error rescue URI::Error
element.remove element.remove
end 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. # 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

@ -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 :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: 50..160 } validates :description, length: { in: 10..160 }
validate :deploy_local_presence validate :deploy_local_presence
validate :compatible_layouts, on: :update validate :compatible_layouts, on: :update
@ -37,6 +37,7 @@ 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
@ -65,9 +66,6 @@ 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
@ -180,29 +178,28 @@ 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?
@jekyll.reader.read_data run_in_path do
jekyll.reader.read_data
# Define los valores por defecto según la llave buscada jekyll.data['layouts'] ||= {}
@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
@jekyll.reader.read_collections run_in_path do
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
@ -290,7 +287,9 @@ class Site < ApplicationRecord
# #
# @return [Hash] # @return [Hash]
def theme_layouts def theme_layouts
@jekyll.reader.read_layouts run_in_path do
jekyll.reader.read_layouts
end
end end
# Trae todos los valores disponibles para un campo # Trae todos los valores disponibles para un campo
@ -332,6 +331,12 @@ 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
@ -345,10 +350,7 @@ 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
@ -526,4 +528,8 @@ 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

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

@ -12,4 +12,16 @@ 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,6 +122,8 @@ 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

@ -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 %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 %form{ action: site_posts_path }
- @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 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' : ''}" 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,11 +67,14 @@
%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
%div
= submit_tag t('posts.reorder.submit'), class: 'btn' = submit_tag t('posts.reorder.submit'), class: 'btn'
%button.btn{ data: { action: 'reorder#unselect' } } %button.btn{ data: { action: 'reorder#unselect' } }
= t('posts.reorder.unselect') = t('posts.reorder.unselect')
@ -80,6 +83,8 @@
%button.btn{ data: { action: 'reorder#down' } }= t('posts.reorder.down') %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#top' } }= t('posts.reorder.top')
%button.btn{ data: { action: 'reorder#bottom' } }= t('posts.reorder.bottom') %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
@ -104,19 +109,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')
- if post.front_matter['categories'].present?
%br %br
%small %small
- post.front_matter['categories'].each do |category| = 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 = 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 %td.text-nowrap
= post.created_at.strftime('%F') = post.created_at.strftime('%F')
%br/ %br/
= post.order = post.order
%td %td.text-nowrap
- 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: 50, required: true maxlength: 160, minlength: 10, 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,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 .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}.
%table.table.table-condensed .mb-5
%tbody - Stat::INTERVALS.each do |interval|
%tr = link_to t(".#{interval}"), site_stats_path(interval: interval, urls: params[:urls]), class: "btn #{'btn-primary active' if @interval == interval}"
%td= t('.build.average')
%td= distance_of_time_in_words_if_more_than_a_minute @build_avg .mb-5
%tr %h2= t('.host.title', count: @hostnames.size)
%td= t('.build.maximum') %p.lead= t('.host.description')
%td= distance_of_time_in_words_if_more_than_a_minute @build_max = 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.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|
@ -24,4 +26,6 @@ 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: Exit logout: Log out
mutual_aid: Mutual aid mutual_aid: Mutual aid
collaborations: collaborations:
collaborate: collaborate:
@ -252,9 +252,38 @@ 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.
build: last_update: 'Updated every hour. Last update on '
average: 'Average building time' empty: 'There is no enough information yet. We invite you to come back in %{please_return_at}!'
maximum: 'Maximum building time' 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: sites:
donations: donations:
url: 'https://donaciones.sutty.nl/en/' url: 'https://donaciones.sutty.nl/en/'
@ -376,6 +405,8 @@ 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:
@ -413,6 +444,8 @@ 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: _true: 'Sí'
_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: Salir logout: Cerrar sesión
mutual_aid: Ayuda mutua mutual_aid: Ayuda mutua
collaborations: collaborations:
collaborate: collaborate:
@ -257,9 +257,38 @@ 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.
build: last_update: 'Actualizadas cada hora. Última actualización hace '
average: 'Tiempo promedio de generación' empty: 'Todavía no hay información suficiente. Te invitamos a volver en %{please_return_at} :)'
maximum: 'Tiempo máximo de generación' 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: sites:
donations: donations:
url: 'https://donaciones.sutty.nl/' url: 'https://donaciones.sutty.nl/'
@ -384,6 +413,8 @@ 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:
@ -421,6 +452,8 @@ 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,9 +57,10 @@ Rails.application.routes.draw do
# Gestionar artículos según idioma # Gestionar artículos según idioma
nested do nested do
scope '(:locale)' do scope '/(:locale)', constraint: /[a-z]{2}/ 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
@ -75,5 +76,8 @@ 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

@ -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,6 +13,8 @@
"@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,6 +2119,25 @@ 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"
@ -2744,6 +2763,11 @@ 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"