Compare commits

...

16 Commits

Author SHA1 Message Date
Runar Ingebrigtsen
7b6e059da6 logo update
CI / scan_ruby (push) Successful in 16s
CI / scan_js (push) Successful in 13s
CI / lint (push) Successful in 21s
CI / test (push) Successful in 53s
2026-02-20 22:02:26 +01:00
Runar Ingebrigtsen
e9f8f03db2 URL update
CI / scan_ruby (push) Successful in 39s
CI / scan_js (push) Successful in 13s
CI / lint (push) Successful in 21s
CI / test (push) Successful in 48s
2026-02-20 21:43:40 +01:00
Runar Ingebrigtsen
b289cdc320 update + parallel tests 2026-02-20 21:43:25 +01:00
Runar Ingebrigtsen
8bb410dcfa yaml syntax
CI / scan_ruby (push) Successful in 18s
CI / scan_js (push) Successful in 14s
CI / lint (push) Successful in 21s
CI / test (push) Successful in 1m1s
2026-02-06 02:35:56 +01:00
Runar Ingebrigtsen
ce67776eec sanasto.wiki
CI / scan_ruby (push) Successful in 18s
CI / scan_js (push) Successful in 14s
CI / lint (push) Successful in 22s
CI / test (push) Successful in 50s
2026-02-06 02:34:44 +01:00
Runar Ingebrigtsen
9f71fe65e5 test corst, fix footer
CI / scan_ruby (push) Successful in 17s
CI / scan_js (push) Successful in 12s
CI / lint (push) Successful in 21s
CI / test (push) Successful in 48s
2026-02-06 02:01:57 +01:00
Runar Ingebrigtsen
e15835bda9 support browser cors request
CI / scan_ruby (push) Successful in 18s
CI / scan_js (push) Successful in 12s
CI / lint (push) Successful in 21s
CI / test (push) Successful in 47s
2026-02-05 23:59:11 +01:00
Runar Ingebrigtsen
83320d4c9a add CORS access for sanasto.app
CI / scan_ruby (push) Successful in 17s
CI / scan_js (push) Successful in 12s
CI / lint (push) Failing after 20s
CI / test (push) Successful in 48s
2026-02-05 23:52:21 +01:00
Runar Ingebrigtsen
a2008e2ae3 update vulnerability scan
CI / scan_ruby (push) Successful in 24s
CI / scan_js (push) Successful in 14s
CI / lint (push) Successful in 22s
CI / test (push) Successful in 52s
2026-02-04 09:02:38 +01:00
Runar Ingebrigtsen
b45a451748 fix pagination test
CI / scan_ruby (push) Failing after 15s
CI / scan_js (push) Successful in 14s
CI / lint (push) Successful in 22s
CI / test (push) Successful in 51s
2026-02-04 08:59:04 +01:00
Runar Ingebrigtsen
441caabb98 complete API swagger documentation
CI / scan_ruby (push) Failing after 29s
CI / scan_js (push) Successful in 13s
CI / lint (push) Successful in 21s
CI / test (push) Failing after 48s
2026-02-04 01:33:02 +01:00
Runar Ingebrigtsen
a139bde102 switch to pagy for pagination 2026-02-03 21:21:53 +01:00
Runar Ingebrigtsen
f35a09f07a lint 2026-02-01 05:32:30 +01:00
Runar Ingebrigtsen
1a10e3c784 api completion 2026-02-01 05:29:16 +01:00
Runar Ingebrigtsen
4fe95ca538 add sync API with swagger documentation at /api
CI / scan_ruby (push) Successful in 23s
CI / scan_js (push) Successful in 15s
CI / lint (push) Successful in 22s
CI / test (push) Successful in 47s
2026-01-31 22:39:12 +01:00
Runar Ingebrigtsen
fa36305244 document deployment 2026-01-31 16:05:44 +01:00
33 changed files with 756 additions and 89 deletions
+1
View File
@@ -0,0 +1 @@
markup: markdown
+8
View File
@@ -18,6 +18,8 @@ gem "turbo-rails"
gem "stimulus-rails"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"
gem "grape"
gem "rswag-ui"
gem "caxlsx"
gem "caxlsx_rails"
@@ -44,6 +46,9 @@ gem "thruster", require: false
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
gem "image_processing", "~> 1.2"
# Pagination [https://github.com/ddnexus/pagy]
gem "pagy", "~> 8.0"
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"
@@ -56,6 +61,9 @@ group :development, :test do
# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
gem "rubocop-rails-omakase", require: false
# Parallel test runner [https://github.com/grosser/parallel_tests]
gem "parallel_tests", require: false
end
group :development do
+83 -28
View File
@@ -84,9 +84,9 @@ GEM
benchmark (0.5.0)
bigdecimal (4.0.1)
bindex (0.8.1)
bootsnap (1.21.1)
bootsnap (1.23.0)
msgpack (~> 1.2)
brakeman (8.0.1)
brakeman (8.0.2)
racc
builder (3.3.0)
bundler-audit (0.9.3)
@@ -120,6 +120,26 @@ GEM
docile (1.4.1)
dotenv (3.2.0)
drb (2.2.3)
dry-configurable (1.3.0)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
dry-core (1.2.0)
concurrent-ruby (~> 1.0)
logger
zeitwerk (~> 2.6)
dry-inflector (1.3.1)
dry-logic (1.6.0)
bigdecimal
concurrent-ruby (~> 1.0)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
dry-types (1.9.1)
bigdecimal (>= 3.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0)
dry-inflector (~> 1.0)
dry-logic (~> 1.4)
zeitwerk (~> 2.6)
ed25519 (1.4.0)
erb (6.0.1)
erubi (1.13.1)
@@ -131,6 +151,13 @@ GEM
raabro (~> 1.4)
globalid (1.3.0)
activesupport (>= 6.1)
grape (3.1.1)
activesupport (>= 7.1)
dry-configurable
dry-types (>= 1.1)
mustermann-grape (~> 1.1.0)
rack (>= 2)
zeitwerk
htmlentities (4.4.2)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
@@ -142,14 +169,15 @@ GEM
activesupport (>= 6.0.0)
railties (>= 6.0.0)
io-console (0.8.2)
irb (1.16.0)
irb (1.17.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jbuilder (2.14.1)
actionview (>= 7.0.0)
activesupport (>= 7.0.0)
json (2.18.0)
json (2.18.1)
kamal (2.10.1)
activesupport (>= 7.0)
base64 (~> 0.2)
@@ -181,7 +209,11 @@ GEM
minitest (6.0.1)
prism (~> 1.5)
msgpack (1.8.0)
net-imap (0.6.2)
mustermann (3.0.4)
ruby2_keywords (~> 0.0.1)
mustermann-grape (1.1.0)
mustermann (>= 1.0.0)
net-imap (0.6.3)
date
net-protocol
net-pop (0.1.2)
@@ -196,11 +228,14 @@ GEM
net-protocol
net-ssh (7.3.0)
nio4r (2.7.5)
nokogiri (1.19.0-x86_64-linux-gnu)
nokogiri (1.19.1-x86_64-linux-gnu)
racc (~> 1.4)
ostruct (0.6.3)
pagy (8.6.3)
parallel (1.27.0)
parser (3.3.10.1)
parallel_tests (5.6.0)
parallel
parser (3.3.10.2)
ast (~> 2.4.1)
racc
pp (0.6.3)
@@ -219,7 +254,7 @@ GEM
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.2.4)
rack (3.2.5)
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
@@ -259,7 +294,7 @@ GEM
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.3.1)
rdoc (7.1.0)
rdoc (7.2.0)
erb
psych (>= 4.0.0)
tsort
@@ -273,7 +308,10 @@ GEM
logger (~> 1)
nokogiri (~> 1)
rubyzip (>= 3.0.0, < 4.0.0)
rubocop (1.84.0)
rswag-ui (2.17.0)
actionpack (>= 5.2, < 8.2)
railties (>= 5.2, < 8.2)
rubocop (1.84.2)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -305,9 +343,10 @@ GEM
ruby-vips (2.3.0)
ffi (~> 1.12)
logger
ruby2_keywords (0.0.5)
rubyzip (3.2.2)
securerandom (0.4.1)
selenium-webdriver (4.40.0)
selenium-webdriver (4.41.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
@@ -328,7 +367,7 @@ GEM
activejob (>= 7.2)
activerecord (>= 7.2)
railties (>= 7.2)
solid_queue (1.3.1)
solid_queue (1.3.2)
activejob (>= 7.1)
activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1)
@@ -347,7 +386,7 @@ GEM
railties (>= 6.0.0)
stringio (3.2.0)
thor (1.5.0)
thruster (0.1.17-x86_64-linux)
thruster (0.1.18-x86_64-linux)
timeout (0.6.0)
tsort (0.2.0)
turbo-rails (2.0.23)
@@ -372,7 +411,7 @@ GEM
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.4)
zeitwerk (2.7.5)
PLATFORMS
x86_64-linux-gnu
@@ -387,14 +426,18 @@ DEPENDENCIES
caxlsx
caxlsx_rails
debug
grape
image_processing (~> 1.2)
importmap-rails
jbuilder
kamal
pagy (~> 8.0)
parallel_tests
propshaft
puma (>= 5.0)
rails (~> 8.1.2)
roo (~> 3.0)
rswag-ui
rubocop-rails-omakase
selenium-webdriver
simplecov
@@ -429,8 +472,8 @@ CHECKSUMS
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e
bootsnap (1.21.1) sha256=9373acfe732da35846623c337d3481af8ce77c7b3a927fb50e9aa92b46dbc4c4
brakeman (8.0.1) sha256=c68ce0ac35a6295027c4eab8b4ac597d2a0bfc82f0d62dcd334bbf944d352f70
bootsnap (1.23.0) sha256=c1254f458d58558b58be0f8eb8f6eec2821456785b7cdd1e16248e2020d3f214
brakeman (8.0.2) sha256=7b02065ce8b1de93949cefd3f2ad78e8eb370e644b95c8556a32a912a782426a
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9
capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef
@@ -445,6 +488,11 @@ CHECKSUMS
docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
dry-configurable (1.3.0) sha256=882d862858567fc1210d2549d4c090f34370fc1bb7c5c1933de3fe792e18afa8
dry-core (1.2.0) sha256=0cc5a7da88df397f153947eeeae42e876e999c1e30900f3c536fb173854e96a1
dry-inflector (1.3.1) sha256=7fb0c2bb04f67638f25c52e7ba39ab435d922a3a5c3cd196120f63accb682dcc
dry-logic (1.6.0) sha256=da6fedbc0f90fc41f9b0cc7e6f05f5d529d1efaef6c8dcc8e0733f685745cea2
dry-types (1.9.1) sha256=baebeecdb9f8395d6c9d227b62011279440943e3ef2468fe8ccc1ba11467f178
ed25519 (1.4.0) sha256=16e97f5198689a154247169f3453ef4cfd3f7a47481fde0ae33206cdfdcac506
erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5
erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
@@ -452,14 +500,15 @@ CHECKSUMS
ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f
fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68
globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
grape (3.1.1) sha256=774f16782d917a90e69de0499dfaab571e5ad967569ac066a2b0b918af12de69
htmlentities (4.4.2) sha256=bbafbdf69f2eca9262be4efef7e43e6a1de54c95eb600f26984f71d2fe96c5c3
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb
importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a
io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
irb (1.16.0) sha256=2abe56c9ac947cdcb2f150572904ba798c1e93c890c256f8429981a7675b0806
irb (1.17.0) sha256=168c4ddb93d8a361a045c41d92b2952c7a118fa73f23fe14e55609eb7a863aae
jbuilder (2.14.1) sha256=4eb26376ff60ef100cb4fd6fd7533cd271f9998327e86adf20fd8c0e69fabb42
json (2.18.0) sha256=b10506aee4183f5cf49e0efc48073d7b75843ce3782c68dbeb763351c08fd505
json (2.18.1) sha256=fe112755501b8d0466b5ada6cf50c8c3f41e897fa128ac5d263ec09eedc9f986
kamal (2.10.1) sha256=53b7ecb4c33dd83b1aedfc7aacd1c059f835993258a552d70d584c6ce32b6340
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
@@ -472,7 +521,9 @@ CHECKSUMS
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
minitest (6.0.1) sha256=7854c74f48e2e975969062833adc4013f249a4b212f5e7b9d5c040bf838d54bb
msgpack (1.8.0) sha256=e64ce0212000d016809f5048b48eb3a65ffb169db22238fb4b72472fecb2d732
net-imap (0.6.2) sha256=08caacad486853c61676cca0c0c47df93db02abc4a8239a8b67eb0981428acc6
mustermann (3.0.4) sha256=85fadcb6b3c6493a8b511b42426f904b7f27b282835502233dd154daab13aa22
mustermann-grape (1.1.0) sha256=8d258a986004c8f01ce4c023c0b037c168a9ed889cf5778068ad54398fa458c5
net-imap (0.6.3) sha256=9bab75f876596d09ee7bf911a291da478e0cd6badc54dfb82874855ccc82f2ad
net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3
net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8
net-scp (4.1.0) sha256=a99b0b92a1e5d360b0de4ffbf2dc0c91531502d3d4f56c28b0139a7c093d1a5d
@@ -480,10 +531,12 @@ CHECKSUMS
net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736
net-ssh (7.3.0) sha256=172076c4b30ce56fb25a03961b0c4da14e1246426401b0f89cba1a3b54bf3ef0
nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
nokogiri (1.19.0-x86_64-linux-gnu) sha256=f482b95c713d60031d48c44ce14562f8d2ce31e3a9e8dd0ccb131e9e5a68b58c
nokogiri (1.19.1-x86_64-linux-gnu) sha256=1a4902842a186b4f901078e692d12257678e6133858d0566152fe29cdb98456a
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
pagy (8.6.3) sha256=537b2ee3119f237dd6c4a0d0a35c67a77b9d91ebb9d4f85e31407c2686774fb2
parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
parser (3.3.10.1) sha256=06f6a725d2cd91e5e7f2b7c32ba143631e1f7c8ae2fb918fc4cebec187e6a688
parallel_tests (5.6.0) sha256=b2d7af382d1c0289daba65308d9143b3ad82d817d500c0ad1b4bde468beb13af
parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357
pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
@@ -493,7 +546,7 @@ CHECKSUMS
puma (7.2.0) sha256=bf8ef4ab514a4e6d4554cb4326b2004eba5036ae05cf765cfe51aba9706a72a8
raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
rack (3.2.4) sha256=5d74b6f75082a643f43c1e76b419c40f0e5527fcfee1e669ac1e6b73c0ccb6f6
rack (3.2.5) sha256=4cbd0974c0b79f7a139b4812004a62e4c60b145cba76422e288ee670601ed6d3
rack-session (2.1.1) sha256=0b6dc07dea7e4b583f58a48e8b806d4c9f1c6c9214ebc202ec94562cbea2e4e9
rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
@@ -503,33 +556,35 @@ CHECKSUMS
railties (8.1.2) sha256=1289ece76b4f7668fc46d07e55cc992b5b8751f2ad85548b7da351b8c59f8055
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
rdoc (7.1.0) sha256=494899df0706c178596ca6e1d50f1b7eb285a9b2aae715be5abd742734f17363
rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192
regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
roo (3.0.0) sha256=6fdd7a9158d657c69768b4168754ff2110cc21fdc01a1bec1010820cb05c91b1
rubocop (1.84.0) sha256=88dec310153bb685a879f5a7cdb601f6287b8f0ee675d9dc63a17c7204c4190a
rswag-ui (2.17.0) sha256=5f707b9b5e8171ddf9f519f6e401e79e419bd1d07387508603e76124f2443212
rubocop (1.84.2) sha256=5692cea54168f3dc8cb79a6fe95c5424b7ea893c707ad7a4307b0585e88dbf5f
rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd
rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834
rubocop-rails (2.34.3) sha256=10d37989024865ecda8199f311f3faca990143fbac967de943f88aca11eb9ad2
rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
ruby-vips (2.3.0) sha256=e685ec02c13969912debbd98019e50492e12989282da5f37d05f5471442f5374
ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef
rubyzip (3.2.2) sha256=c0ed99385f0625415c8f05bcae33fe649ed2952894a95ff8b08f26ca57ea5b3c
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
selenium-webdriver (4.40.0) sha256=16ef7aa9853c1d4b9d52eac45aafa916e3934c5c83cb4facb03f250adfd15e5b
selenium-webdriver (4.41.0) sha256=cdc1173cd55cf186022cea83156cc2d0bec06d337e039b02ad25d94e41bedd22
simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
solid_cable (3.0.12) sha256=a168a54731a455d5627af48d8441ea3b554b8c1f6e6cd6074109de493e6b0460
solid_cache (1.0.10) sha256=bc05a2fb3ac78a6f43cbb5946679cf9db67dd30d22939ededc385cb93e120d41
solid_queue (1.3.1) sha256=d9580111180c339804ff1a810a7768f69f5dc694d31e86cf1535ff2cd7a87428
solid_queue (1.3.2) sha256=44a53047be4255f616ff13fa5d35980e7b3eee6e31d957eadb88fbe8e0db4509
sqlite3 (2.9.0-x86_64-linux-gnu) sha256=72fff9bd750070ba3af695511ba5f0e0a2d8a9206f84869640b3e99dfaf3d5a5
sshkit (1.25.0) sha256=c8c6543cdb60f91f1d277306d585dd11b6a064cb44eab0972827e4311ff96744
stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06
stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
thruster (0.1.17-x86_64-linux) sha256=77b8f335075bd4ece7631dc84a19a710a1e6e7102cbce147b165b45851bdfcd3
thruster (0.1.18-x86_64-linux) sha256=0ec1ff5f12289c1ac10cf8e28ce6b5266f4e73416b34a664b79d037c7d955c40
timeout (0.6.0) sha256=6d722ad619f96ee383a0c557ec6eb8c4ecb08af3af62098a0be5057bf00de1af
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
turbo-rails (2.0.23) sha256=ee0d90733aafff056cf51ff11e803d65e43cae258cc55f6492020ec1f9f9315f
@@ -543,7 +598,7 @@ CHECKSUMS
websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962
websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241
xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e
zeitwerk (2.7.4) sha256=2bef90f356bdafe9a6c2bd32bcd804f83a4f9b8bc27f3600fff051eb3edcec8b
zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd
BUNDLED WITH
4.0.4
+17 -7
View File
@@ -2,7 +2,7 @@
## Overview
"Sanasto Wiki" is a web-based dictionary application for simultaneous translators in the living Christianity. The application provides publicly accessible translations while restricting editing and commenting to invited contributors.
"Sanasto Wiki" is a web-based glossary for simultaneous translators in the living Christianity. The application provides publicly accessible translations while restricting editing and commenting to invited contributors.
---
@@ -188,15 +188,25 @@ See 'public/Kristillisyyden sanasto ver 23.5.2013.xlsx'
---
## API (Optional Future)
## API (Public Sync)
REST API for potential mobile app or integration:
Public JSON endpoint for syncing entries:
```
GET /api/entries
GET /api/entries/:id
GET /api/entries/search?q=:query&lang=:code
POST /api/entries (authenticated)
PATCH /api/entries/:id (authenticated)
GET /api/entries?since=2026-01-01T12:00:00Z
```
Responses include all language columns, category and `updated_at`. The optional `since`
parameter filters by `updated_at` (ISO8601).
Swagger docs:
```
GET /api/swagger
```
Swagger UI:
```
GET /api
```
## Deployment
+48
View File
@@ -0,0 +1,48 @@
require "grape"
require "ostruct"
class Api::Base < Grape::API
format :json
default_format :json
content_type :json, "application/json"
helpers do
def parse_since_param(raw_since)
return nil if raw_since.blank?
Time.iso8601(raw_since)
rescue ArgumentError
error!({ error: "Invalid since parameter. Use ISO8601 timestamp." }, 400)
end
end
resource :entries do
desc "Return public entries in all languages",
attributes: OpenStruct.new(success: nil, produces: nil)
params do
optional :since,
type: String,
desc: "ISO8601 timestamp. Returns entries updated after this time."
end
get do
since_time = parse_since_param(params[:since])
entries_scope = Entry.active_entries
entries_scope = entries_scope.where("updated_at > ?", since_time) if since_time
entries_scope
.order(:updated_at, :id)
.select(
:id,
:category,
:fi,
:en,
:sv,
:no,
:ru,
:de,
:updated_at
)
end
end
end
+119
View File
@@ -0,0 +1,119 @@
# config/routes.rb
# app/controllers/api/swagger_controller.rb
module Api
class SwaggerController < ApplicationController
def index
render json: {
openapi: "3.0.0",
info: {
title: "Sanasto Wiki API",
description: "Public sync API for Sanasto Wiki glossary entries.",
version: "1.0.0"
},
servers: [
{
url: "https://#{request.host}",
description: "Production server"
}
],
paths: {
"/api/entries": {
get: {
summary: "Return public entries in all languages",
description: "Retrieve all active glossary entries with optional filtering by update timestamp",
tags: [ "Entries" ],
parameters: [
{
name: "since",
in: "query",
description: "ISO8601 timestamp. Returns entries updated after this time.",
required: false,
schema: {
type: "string",
format: "date-time",
example: "2024-01-01T00:00:00Z"
}
}
],
responses: {
"200": {
description: "List of entries",
content: {
"application/json": {
schema: {
type: "array",
items: {
"$ref": "#/components/schemas/Entry"
}
}
}
}
},
"400": {
description: "Invalid since parameter",
content: {
"application/json": {
schema: {
type: "object",
properties: {
error: { type: "string" }
}
}
}
}
}
}
}
}
},
components: {
schemas: {
Entry: {
type: "object",
properties: {
id: {
type: "integer",
description: "Entry ID"
},
category: {
type: "string",
description: "Entry category"
},
fi: {
type: "string",
description: "Finnish translation"
},
en: {
type: "string",
description: "English translation"
},
sv: {
type: "string",
description: "Swedish translation"
},
no: {
type: "string",
description: "Norwegian translation"
},
ru: {
type: "string",
description: "Russian translation"
},
de: {
type: "string",
description: "German translation"
},
updated_at: {
type: "string",
format: "date-time",
description: "Last update timestamp (ISO8601)"
}
}
}
}
}
}
end
end
end
@@ -1,5 +1,6 @@
class ApplicationController < ActionController::Base
include BotBlocker
include Pagy::Backend
# Changes to the importmap will invalidate the etag for HTML responses
stale_when_importmap_changes
+2 -5
View File
@@ -6,8 +6,6 @@ class EntriesController < ApplicationController
@category = params[:category].presence
@query = params[:q].to_s.strip
@starts_with = params[:starts_with].presence
@page = [ params[:page].to_i, 1 ].max
@per_page = 25
entries_scope = Entry.active_entries
entries_scope = entries_scope.with_category(@category)
@@ -16,9 +14,8 @@ class EntriesController < ApplicationController
entries_scope = entries_scope.alphabetical_for(@language_code) if @query.blank? && @starts_with.blank? && @language_code.present?
entries_scope = entries_scope.order(created_at: :desc) if entries_scope.order_values.empty?
@total_entries = entries_scope.count
@total_pages = (@total_entries.to_f / @per_page).ceil
@entries = entries_scope.offset((@page - 1) * @per_page).limit(@per_page)
@pagy, @entries = pagy(entries_scope, items: 25)
@total_entries = @pagy.count
@entry_count = Entry.active_entries.count
@requested_count = Entry.requested.count
+2
View File
@@ -1,4 +1,6 @@
module ApplicationHelper
include Pagy::Frontend
def language_name(code)
supported_languages.find { |l| l.code == code }&.name
end
+12 -10
View File
@@ -86,17 +86,19 @@
</div>
<div class="flex items-center justify-between mt-4 text-sm text-slate-600">
<div>Page <%= @page %> of <%= [@total_pages, 1].max %></div>
<div><%= pagy_info(@pagy).html_safe %></div>
<div class="flex items-center gap-2">
<% previous_page = @page > 1 ? @page - 1 : nil %>
<% next_page = @page < @total_pages ? @page + 1 : nil %>
<% pagination_params = { q: @query.presence, category: @category.presence, language: @language_code.presence, starts_with: @starts_with.presence }.compact %>
<%= link_to "Previous", previous_page ? entries_path(pagination_params.merge(page: previous_page)) : "#",
class: "px-3 py-1.5 rounded-md border border-slate-200 #{previous_page ? 'hover:border-indigo-300' : 'text-slate-300 cursor-not-allowed'}",
data: { turbo_stream: true } %>
<%= link_to "Next", next_page ? entries_path(pagination_params.merge(page: next_page)) : "#",
class: "px-3 py-1.5 rounded-md border border-slate-200 #{next_page ? 'hover:border-indigo-300' : 'text-slate-300 cursor-not-allowed'}",
data: { turbo_stream: true } %>
<%
pagination_params = { q: @query.presence, category: @category.presence, language: @language_code.presence, starts_with: @starts_with.presence }.compact
prev_url = @pagy.prev ? entries_path(pagination_params.merge(page: @pagy.prev)) : nil
next_url = @pagy.next ? entries_path(pagination_params.merge(page: @pagy.next)) : nil
%>
<%= link_to "Previous", prev_url || "#",
class: "px-3 py-1.5 rounded-md border border-slate-200 #{'opacity-50 pointer-events-none' unless prev_url}",
data: (prev_url ? { turbo_stream: true } : {}) %>
<%= link_to "Next", next_url || "#",
class: "px-3 py-1.5 rounded-md border border-slate-200 #{'opacity-50 pointer-events-none' unless next_url}",
data: (next_url ? { turbo_stream: true } : {}) %>
</div>
</div>
</div>
+4 -4
View File
@@ -14,10 +14,10 @@
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="icon" href="/icon.png" type="image/png">
<link rel="apple-touch-icon" href="/icon.png">
<link rel="icon" href="/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="apple-touch-icon" href="/icon-512.png" />
<%# Includes all stylesheet files in app/assets/stylesheets %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
+2 -2
View File
@@ -2,12 +2,12 @@
"name": "SanastoWiki",
"icons": [
{
"src": "/icon.png",
"src": "/icon-512.png",
"type": "image/png",
"sizes": "512x512"
},
{
"src": "/icon.png",
"src": "/icon-512.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
+3
View File
@@ -1,6 +1,7 @@
require_relative "boot"
require "rails/all"
require_relative "../lib/middleware/sanasto_cors"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
@@ -24,5 +25,7 @@ module SanastoWiki
# config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras")
config.active_record.schema_format = :sql
config.middleware.insert_before 0, Middleware::SanastoCors
end
end
+1 -1
View File
@@ -23,7 +23,7 @@ development:
# Do not set this db to the same as development or production.
test:
<<: *default
database: storage/test.sqlite3
database: storage/test<%= ENV["TEST_ENV_NUMBER"] %>.sqlite3
# Store production database in the storage/ directory, which by default
# is mounted as a persistent Docker volume in config/deploy.yml.
+3 -1
View File
@@ -17,7 +17,9 @@ servers:
# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
proxy:
ssl: true
host: sanasto.rin.no
hosts:
- sanasto.rin.no
- sanasto.wiki
# Kamal proxy will forward to your app on port 3000
# Credentials for your image host.
+1
View File
@@ -37,6 +37,7 @@ Rails.application.configure do
# Make template changes take effect immediately.
config.action_mailer.perform_caching = false
# You can use Mailpit for SMTP in development - https://github.com/axllent/mailpit
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: "localhost",
+1 -1
View File
@@ -58,7 +58,7 @@ Rails.application.configure do
# config.action_mailer.raise_delivery_errors = false
# Set host to be used by links generated in mailer templates.
config.action_mailer.default_url_options = { host: "sanasto.rin.no" }
config.action_mailer.default_url_options = { host: "sanasto.wiki" }
# Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit.
config.action_mailer.smtp_settings = {
+6
View File
@@ -0,0 +1,6 @@
# Pagy Configuration
require "pagy/extras/overflow"
Pagy::DEFAULT[:items] = 25 # Match current 25 items per page
Pagy::DEFAULT[:page_param] = :page
Pagy::DEFAULT[:overflow] = :last_page
+3
View File
@@ -0,0 +1,3 @@
Rswag::Ui.configure do |config|
config.swagger_endpoint "/api/swagger", "Sanasto Wiki API"
end
+4
View File
@@ -1,6 +1,10 @@
Rails.application.routes.draw do
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
mount Api::Base => "/api"
get "/api/swagger", to: "api/swagger#index"
mount Rswag::Ui::Engine => "/api"
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
# Can be used by load balancers and uptime monitors to verify that the app is live.
get "up" => "rails/health#show", as: :rails_health_check
+75
View File
@@ -0,0 +1,75 @@
# Deployment (Kamal)
We deploy with Kamal. You do NOT need to manually set up web servers or Docker
on the VM. `kamal setup` provisions everything.
## Requirements
### Local machine
- Ruby + Bundler
- Docker (for building images)
- SSH key access to the VM
- Registry credentials (see `config/deploy.yml`)
### Remote VM
- Bare VM with SSH access
- Open ports: 22, 80, 443
- A domain name pointing at the VM (for SSL)
## Configure
Update `config/deploy.yml`:
```yaml
service: sanasto-wiki
image: your-registry/sanasto-wiki
servers:
web:
- your-server-ip
proxy:
ssl: true
host: sanasto.example.com
registry:
server: ghcr.io
username: your-github-username
ssh:
user: deploy
```
Set the registry password:
```bash
export KAMAL_REGISTRY_PASSWORD="your-token"
```
Make sure `config/master.key` is present locally.
## First deploy
```bash
bundle exec kamal setup
```
## Regular deploys
```bash
bundle exec kamal deploy
```
## Useful commands
```bash
bundle exec kamal app logs --follow
bundle exec kamal app details
bundle exec kamal app exec --interactive --reuse "bin/rails console"
bundle exec kamal rollback
```
## Initial app setup
Visit `https://your-domain/setup` once after the first deploy to create the
admin account.
+86
View File
@@ -0,0 +1,86 @@
module Middleware
class SanastoCors
ALLOWED_APP_ID = ENV.fetch("SANASTO_APP_ID", "app.sanasto").freeze
APP_ID_HEADER = "HTTP_X_SANASTO_APP"
def initialize(app)
@app = app
end
def call(env)
if allow_cors_for?(env)
if env["REQUEST_METHOD"] == "OPTIONS"
return preflight_response(env["HTTP_ORIGIN"], allowed_request_headers(env))
end
end
status, headers, body = @app.call(env)
if allow_cors_for?(env)
apply_cors_headers(headers, env["HTTP_ORIGIN"])
end
[ status, headers, body ]
end
private
def allow_cors_for?(env)
origin = env["HTTP_ORIGIN"].to_s
return false if origin.empty?
if env["REQUEST_METHOD"] == "OPTIONS"
return preflight_includes_app_id_header?(env)
end
app_id = env[APP_ID_HEADER].to_s
return false if app_id.empty?
app_id == ALLOWED_APP_ID
end
def preflight_includes_app_id_header?(env)
access_control_headers = env["HTTP_ACCESS_CONTROL_REQUEST_HEADERS"].to_s
return false if access_control_headers.empty?
access_control_headers
.split(",")
.map { |header_name| header_name.strip.downcase }
.include?("x-sanasto-app")
end
def allowed_request_headers(env)
access_control_headers = env["HTTP_ACCESS_CONTROL_REQUEST_HEADERS"].to_s
return default_allowed_headers if access_control_headers.empty?
sanitized = access_control_headers
.split(",")
.map { |header_name| header_name.strip }
.reject(&:empty?)
.join(", ")
sanitized.empty? ? default_allowed_headers : sanitized
end
def preflight_response(origin, allowed_headers)
headers = {}
apply_cors_headers(headers, origin, allowed_headers)
headers["Access-Control-Max-Age"] = "86400"
headers["Vary"] = [
headers["Vary"],
"Access-Control-Request-Headers",
"Access-Control-Request-Method"
].compact.join(", ")
[ 204, headers, [] ]
end
def apply_cors_headers(headers, origin, allowed_headers = default_allowed_headers)
headers["Access-Control-Allow-Origin"] = origin
headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, PATCH, DELETE, OPTIONS"
headers["Access-Control-Allow-Headers"] = allowed_headers
headers["Vary"] = [ headers["Vary"], "Origin, X-Sanasto-App" ].compact.join(", ")
end
def default_allowed_headers
"Origin, Content-Type, Accept, Authorization, X-Sanasto-App"
end
end
end
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

+122 -27
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 23 KiB

+2 -1
View File
@@ -41,7 +41,8 @@ class EntriesControllerTest < ActionDispatch::IntegrationTest
test "should paginate results" do
get entries_url, params: { page: 2 }
assert_response :success
assert_select "div", text: /Page 2 of/i
assert_select "a", text: "Previous"
assert_select "a", text: "Next"
end
test "should handle invalid language code" do
+63
View File
@@ -0,0 +1,63 @@
require "test_helper"
class ApiEntriesTest < ActionDispatch::IntegrationTest
test "returns active entries with language fields" do
entry = entries(:one)
get "/api/entries"
assert_response :success
payload = JSON.parse(response.body)
entry_payload = payload.find { |item| item["id"] == entry.id }
assert_not_nil entry_payload
assert_equal entry.fi, entry_payload["fi"]
assert_equal entry.en, entry_payload["en"]
assert_equal entry.sv, entry_payload["sv"]
assert_equal entry.no, entry_payload["no"]
assert_equal entry.ru, entry_payload["ru"]
assert_equal entry.de, entry_payload["de"]
end
test "filters by updated_at when since param is provided" do
older_entry = Entry.create!(
fi: "Older Entry",
category: :word,
status: :active
)
older_entry.update_column(:updated_at, 2.days.ago)
newer_entry = Entry.create!(
fi: "Newer Entry",
category: :word,
status: :active
)
newer_entry.update_column(:updated_at, 1.hour.ago)
get "/api/entries", params: { since: 1.day.ago.iso8601 }
assert_response :success
payload = JSON.parse(response.body)
returned_ids = payload.map { |item| item["id"] }
assert_includes returned_ids, newer_entry.id
assert_not_includes returned_ids, older_entry.id
end
test "returns bad request for invalid since param" do
get "/api/entries", params: { since: "not-a-time" }
assert_response :bad_request
payload = JSON.parse(response.body)
assert_equal "Invalid since parameter. Use ISO8601 timestamp.", payload["error"]
end
test "exposes swagger docs" do
get "/api/swagger"
assert_response :success
payload = JSON.parse(response.body)
assert_equal "Sanasto Wiki API", payload.dig("info", "title")
assert_includes payload.keys, "paths"
end
end
+30
View File
@@ -0,0 +1,30 @@
require "test_helper"
class CorsPreflightTest < ActionDispatch::IntegrationTest
setup do
@origin = "http://localhost:5173"
@app_id = "app.sanasto"
end
test "options preflight returns cors headers when app header is requested" do
options "/api/entries", headers: {
"Origin" => @origin,
"Access-Control-Request-Method" => "GET",
"Access-Control-Request-Headers" => "x-sanasto-app"
}
assert_response :no_content
assert_equal @origin, response.headers["Access-Control-Allow-Origin"]
assert_includes response.headers["Access-Control-Allow-Headers"], "x-sanasto-app"
end
test "get includes cors headers when app id is provided" do
get "/api/entries", headers: {
"Origin" => @origin,
"X-Sanasto-App" => @app_id
}
assert_response :success
assert_equal @origin, response.headers["Access-Control-Allow-Origin"]
end
end
+55
View File
@@ -0,0 +1,55 @@
require "test_helper"
class SanastoCorsTest < ActiveSupport::TestCase
def setup
@app = ->(_env) { [ 200, {}, [ "ok" ] ] }
@middleware = Middleware::SanastoCors.new(@app)
@origin = "http://localhost:5173"
@app_id = "app.sanasto"
end
test "adds cors headers for allowed get requests" do
env = Rack::MockRequest.env_for(
"/api/entries",
method: "GET",
"HTTP_ORIGIN" => @origin,
"HTTP_X_SANASTO_APP" => @app_id
)
status, headers, _body = @middleware.call(env)
assert_equal 200, status
assert_equal @origin, headers["Access-Control-Allow-Origin"]
assert_includes headers["Access-Control-Allow-Headers"], "X-Sanasto-App"
end
test "returns preflight response with cors headers when app header is requested" do
env = Rack::MockRequest.env_for(
"/api/entries",
method: "OPTIONS",
"HTTP_ORIGIN" => @origin,
"HTTP_ACCESS_CONTROL_REQUEST_METHOD" => "GET",
"HTTP_ACCESS_CONTROL_REQUEST_HEADERS" => "x-sanasto-app"
)
status, headers, _body = @middleware.call(env)
assert_equal 204, status
assert_equal @origin, headers["Access-Control-Allow-Origin"]
assert_includes headers["Access-Control-Allow-Headers"], "x-sanasto-app"
assert_includes headers["Vary"], "Access-Control-Request-Headers"
end
test "does not add cors headers when app id is missing" do
env = Rack::MockRequest.env_for(
"/api/entries",
method: "GET",
"HTTP_ORIGIN" => @origin
)
status, headers, _body = @middleware.call(env)
assert_equal 200, status
assert_nil headers["Access-Control-Allow-Origin"]
end
end
+1 -1
View File
@@ -102,7 +102,7 @@ class PublicBrowsingTest < ApplicationSystemTestCase
visit root_path
assert_selector ".entry-row", count: 25
assert_link "2"
assert_link "Next"
end
test "visitor sees entry statistics" do